Compare commits
	
		
			47 Commits
		
	
	
		
			4a27794ccc
			...
			refactor/a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f6f0703cb3 | |||
| 3d47b4e44e | |||
| 71fe2a30e7 | |||
| d8f57161ae | |||
| 3caa79b9a7 | |||
| 49beb17925 | |||
| bd8e13f25d | |||
| 1128c9a0ba | |||
| 8dfe201afe | |||
| c1016e496a | |||
| 091097a858 | |||
| 5c97733b3e | |||
| 4ee387ab76 | |||
| 19bf17200d | |||
| be6d97ec85 | |||
| 9d282b26f3 | |||
| dbc2c54ab0 | |||
| aa062932cf | |||
| 812dd03e85 | |||
| 06d639a114 | |||
| 74f51036b1 | |||
| 8308325b73 | |||
| fa7010db3d | |||
| 89320fc540 | |||
| 5ec8d89563 | |||
| 0eeafb5352 | |||
| ab2bdcc7ca | |||
| c2b49e6642 | |||
| 1a89c48790 | |||
| 8dddfe77cd | |||
| 8e8b011fdd | |||
| abd346bb97 | |||
| 6386ec8caa | |||
| ad062828ff | |||
| 92e4988114 | |||
| f9269d7558 | |||
| fa01b7027a | |||
| eaa3a9c297 | |||
| 6cedda9307 | |||
| 942ca73f8d | |||
| da3f58f2ec | |||
| 4a8521d59d | |||
| d7ad84e199 | |||
| 52430c19a5 | |||
| 9492b6cac6 | |||
| 5f324a2348 | |||
| 7452b14817 | 
							
								
								
									
										3
									
								
								.aspire/settings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.aspire/settings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "appHostPath": "../DysonNetwork.Control/DysonNetwork.Control.csproj" | ||||
| } | ||||
							
								
								
									
										35
									
								
								.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.env
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| # Default container port for ring | ||||
| RING_PORT=8080 | ||||
|  | ||||
| # Default container port for pass | ||||
| PASS_PORT=8080 | ||||
|  | ||||
| # Default container port for drive | ||||
| DRIVE_PORT=8080 | ||||
|  | ||||
| # Default container port for sphere | ||||
| SPHERE_PORT=8080 | ||||
|  | ||||
| # Default container port for develop | ||||
| DEVELOP_PORT=8080 | ||||
|  | ||||
| # Parameter cache-password | ||||
| CACHE_PASSWORD=KS3jSPaU9e | ||||
|  | ||||
| # Parameter queue-password | ||||
| QUEUE_PASSWORD=8xEECa4ckz | ||||
|  | ||||
| # Container image name for ring | ||||
| RING_IMAGE=ring:latest | ||||
|  | ||||
| # Container image name for pass | ||||
| PASS_IMAGE=pass:latest | ||||
|  | ||||
| # Container image name for drive | ||||
| DRIVE_IMAGE=drive:latest | ||||
|  | ||||
| # Container image name for sphere | ||||
| SPHERE_IMAGE=sphere:latest | ||||
|  | ||||
| # Container image name for develop | ||||
| DEVELOP_IMAGE=develop:latest | ||||
							
								
								
									
										197
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										197
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,13 +1,13 @@ | ||||
|  name: Build and Push Microservices | ||||
| name: Aspire Publish Workflow | ||||
|  | ||||
|  on: | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
|   workflow_dispatch: | ||||
|  | ||||
|  jobs: | ||||
|    build-sphere: | ||||
| jobs: | ||||
|   publish: | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       contents: read | ||||
| @@ -15,175 +15,46 @@ | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Setup .NET | ||||
|         uses: actions/setup-dotnet@v3 | ||||
|         with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|           dotnet-version: "9.0.x" | ||||
|  | ||||
|       - name: Log in to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Sphere Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Sphere/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-sphere:latest | ||||
|            platforms: linux/amd64 | ||||
|  | ||||
|    build-pass: | ||||
|      runs-on: ubuntu-latest | ||||
|      permissions: | ||||
|        contents: read | ||||
|        packages: write | ||||
|      steps: | ||||
|        - name: Checkout repository | ||||
|          uses: actions/checkout@v3 | ||||
|          with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|        - name: Log in to GitHub Container Registry | ||||
|          uses: docker/login-action@v3 | ||||
|          with: | ||||
|            registry: ghcr.io | ||||
|            username: ${{ github.actor }} | ||||
|            password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Pass Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Pass/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pass:latest | ||||
|            platforms: linux/amd64 | ||||
|       - name: Install Aspire CLI | ||||
|         run: dotnet tool install -g Aspire.Cli --prerelease | ||||
|  | ||||
|    build-pusher: | ||||
|      runs-on: ubuntu-latest | ||||
|      permissions: | ||||
|        contents: read | ||||
|        packages: write | ||||
|      steps: | ||||
|        - name: Checkout repository | ||||
|          uses: actions/checkout@v3 | ||||
|          with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|        - name: Log in to GitHub Container Registry | ||||
|          uses: docker/login-action@v3 | ||||
|          with: | ||||
|            registry: ghcr.io | ||||
|            username: ${{ github.actor }} | ||||
|            password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Pusher Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Pusher/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pusher:latest | ||||
|            platforms: linux/amd64 | ||||
|       - name: Build and Publish Aspire Application | ||||
|         run: aspire publish --project ./DysonNetwork.Control/DysonNetwork.Control.csproj --output publish | ||||
|  | ||||
|    build-drive: | ||||
|      runs-on: ubuntu-latest | ||||
|      permissions: | ||||
|        contents: read | ||||
|        packages: write | ||||
|      steps: | ||||
|        - name: Checkout repository | ||||
|          uses: actions/checkout@v3 | ||||
|          with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|        - name: Log in to GitHub Container Registry | ||||
|          uses: docker/login-action@v3 | ||||
|          with: | ||||
|            registry: ghcr.io | ||||
|            username: ${{ github.actor }} | ||||
|            password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Drive Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Drive/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-drive:latest | ||||
|            platforms: linux/amd64 | ||||
|       - name: Tag and Push Images | ||||
|         run: | | ||||
|           IMAGES=( "sphere" "pass" "ring" "drive" "develop" ) | ||||
|  | ||||
|    build-gateway: | ||||
|      runs-on: ubuntu-latest | ||||
|      permissions: | ||||
|        contents: read | ||||
|        packages: write | ||||
|      steps: | ||||
|        - name: Checkout repository | ||||
|          uses: actions/checkout@v3 | ||||
|          with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|        - name: Log in to GitHub Container Registry | ||||
|          uses: docker/login-action@v3 | ||||
|          with: | ||||
|            registry: ghcr.io | ||||
|            username: ${{ github.actor }} | ||||
|            password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Gateway Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Gateway/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-gateway:latest | ||||
|            platforms: linux/amd64 | ||||
|           for image in "${IMAGES[@]}"; do | ||||
|             IMAGE_NAME="ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-$image:alpha" | ||||
|             SOURCE_IMAGE_NAME="$image:latest" # Aspire's default local image name | ||||
|  | ||||
|    build-develop: | ||||
|      runs-on: ubuntu-latest | ||||
|      permissions: | ||||
|        contents: read | ||||
|        packages: write | ||||
|      steps: | ||||
|        - name: Checkout repository | ||||
|          uses: actions/checkout@v3 | ||||
|          with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|        - name: Log in to GitHub Container Registry | ||||
|          uses: docker/login-action@v3 | ||||
|          with: | ||||
|            registry: ghcr.io | ||||
|            username: ${{ github.actor }} | ||||
|            password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Develop Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Develop/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-develop:latest | ||||
|            platforms: linux/amd64 | ||||
|             echo "Tagging and pushing $SOURCE_IMAGE_NAME to $IMAGE_NAME..." | ||||
|             docker tag $SOURCE_IMAGE_NAME $IMAGE_NAME | ||||
|             docker push $IMAGE_NAME | ||||
|           done | ||||
|  | ||||
|       - name: Upload Aspire Publish Directory | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: aspire-publish-output | ||||
|           path: ./publish/ | ||||
|  | ||||
|       - name: Upload Docker Compose file | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: docker-compose-output | ||||
|           path: ./publish/docker-compose.yml | ||||
|   | ||||
							
								
								
									
										77
									
								
								DysonNetwork.Control/AppHost.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								DysonNetwork.Control/AppHost.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| using Aspire.Hosting.Yarp.Transforms; | ||||
|  | ||||
| var builder = DistributedApplication.CreateBuilder(args); | ||||
|  | ||||
| // Database was configured separately in each service. | ||||
| // var database = builder.AddPostgres("database"); | ||||
|  | ||||
| var cache = builder.AddRedis("cache"); | ||||
| var queue = builder.AddNats("queue").WithJetStream(); | ||||
|  | ||||
| var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring") | ||||
|     .WithReference(queue) | ||||
|     .WithHttpHealthCheck() | ||||
|     .WithEndpoint(5001, 5001, "https", name: "grpc"); | ||||
| var passService = builder.AddProject<Projects.DysonNetwork_Pass>("pass") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(queue) | ||||
|     .WithReference(ringService) | ||||
|     .WithHttpHealthCheck() | ||||
|     .WithEndpoint(5001, 5001, "https", name: "grpc"); | ||||
| var driveService = builder.AddProject<Projects.DysonNetwork_Drive>("drive") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(queue) | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService) | ||||
|     .WithHttpHealthCheck() | ||||
|     .WithEndpoint(5001, 5001, "https", name: "grpc"); | ||||
| var sphereService = builder.AddProject<Projects.DysonNetwork_Sphere>("sphere") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(queue) | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService) | ||||
|     .WithHttpHealthCheck() | ||||
|     .WithEndpoint(5001, 5001, "https", name: "grpc"); | ||||
| var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService) | ||||
|     .WithHttpHealthCheck() | ||||
|     .WithEndpoint(5001, 5001, "https", name: "grpc"); | ||||
|  | ||||
| // Extra double-ended references | ||||
| ringService.WithReference(passService); | ||||
|  | ||||
| builder.AddYarp("gateway") | ||||
|     .WithHostPort(5000) | ||||
|     .WithConfiguration(yarp => | ||||
|     { | ||||
|         var ringCluster = yarp.AddCluster(ringService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/ws", ringCluster); | ||||
|         yarp.AddRoute("/ring/{**catch-all}", ringCluster) | ||||
|             .WithTransformPathRemovePrefix("/ring") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var passCluster = yarp.AddCluster(passService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/.well-known/openid-configuration", passCluster); | ||||
|         yarp.AddRoute("/.well-known/jwks", passCluster); | ||||
|         yarp.AddRoute("/id/{**catch-all}", passCluster) | ||||
|             .WithTransformPathRemovePrefix("/id") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var driveCluster = yarp.AddCluster(driveService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/api/tus", driveCluster); | ||||
|         yarp.AddRoute("/drive/{**catch-all}", driveCluster) | ||||
|             .WithTransformPathRemovePrefix("/drive") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var sphereCluster = yarp.AddCluster(sphereService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/sphere/{**catch-all}", sphereCluster) | ||||
|             .WithTransformPathRemovePrefix("/sphere") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var developCluster = yarp.AddCluster(developService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/develop/{**catch-all}", developCluster) | ||||
|             .WithTransformPathRemovePrefix("/develop") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|     }); | ||||
|  | ||||
| builder.AddDockerComposeEnvironment("docker-compose"); | ||||
|  | ||||
| builder.Build().Run(); | ||||
							
								
								
									
										30
									
								
								DysonNetwork.Control/DysonNetwork.Control.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								DysonNetwork.Control/DysonNetwork.Control.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <Sdk Name="Aspire.AppHost.Sdk" Version="9.4.2"/> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <OutputType>Exe</OutputType> | ||||
|         <TargetFramework>net9.0</TargetFramework> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId> | ||||
|         <RootNamespace>DysonNetwork.Control</RootNamespace> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="Aspire.Hosting.AppHost" Version="9.4.2"/> | ||||
|         <PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" /> | ||||
|         <PackageReference Include="Aspire.Hosting.Nats" Version="9.4.2" /> | ||||
|         <PackageReference Include="Aspire.Hosting.Redis" Version="9.4.2" /> | ||||
|         <PackageReference Include="Aspire.Hosting.Yarp" Version="9.4.2-preview.1.25428.12" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										29
									
								
								DysonNetwork.Control/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								DysonNetwork.Control/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| { | ||||
|   "$schema": "https://json.schemastore.org/launchsettings.json", | ||||
|   "profiles": { | ||||
|     "https": { | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": true, | ||||
|       "applicationUrl": "https://localhost:17025;http://localhost:15057", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development", | ||||
|         "DOTNET_ENVIRONMENT": "Development", | ||||
|         "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21175", | ||||
|         "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22189" | ||||
|       } | ||||
|     }, | ||||
|     "http": { | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": true, | ||||
|       "applicationUrl": "http://localhost:15057", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development", | ||||
|         "DOTNET_ENVIRONMENT": "Development", | ||||
|         "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163", | ||||
|         "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								DysonNetwork.Control/appsettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								DysonNetwork.Control/appsettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| { | ||||
|   "Logging": { | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|       "Microsoft.AspNetCore": "Warning" | ||||
|     } | ||||
|   }, | ||||
|   "ConnectionStrings": { | ||||
|     "cache": "localhost:6379" | ||||
|   } | ||||
| } | ||||
| @@ -31,6 +31,7 @@ | ||||
|     </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||
|     <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||
|   </ItemGroup> | ||||
|   | ||||
|   | ||||
| @@ -1,17 +1,16 @@ | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using DysonNetwork.Develop.Startup; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| builder.AddServiceDefaults(); | ||||
|  | ||||
| builder.ConfigureAppKestrel(builder.Configuration); | ||||
|  | ||||
| builder.Services.AddRegistryService(builder.Configuration); | ||||
| builder.Services.AddStreamConnection(builder.Configuration); | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| builder.Services.AddAppSwagger(); | ||||
| @@ -22,6 +21,8 @@ builder.Services.AddDriveService(); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| app.MapDefaultEndpoints(); | ||||
|  | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
|     var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||
|   | ||||
| @@ -3,6 +3,7 @@ using Microsoft.OpenApi.Models; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| @@ -19,19 +20,16 @@ public static class ServiceCollectionExtensions | ||||
|         services.AddDbContext<AppDatabase>(); | ||||
|         services.AddSingleton<IClock>(SystemClock.Instance); | ||||
|         services.AddHttpContextAccessor(); | ||||
|         services.AddSingleton<IConnectionMultiplexer>(_ => | ||||
|         { | ||||
|             var connection = configuration.GetConnectionString("FastRetrieve")!; | ||||
|             return ConnectionMultiplexer.Connect(connection); | ||||
|         }); | ||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); | ||||
|  | ||||
|         services.AddHttpClient(); | ||||
|  | ||||
|         services.AddControllers().AddJsonOptions(options => | ||||
|         { | ||||
|             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|              | ||||
|             options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||
|         }); | ||||
|  | ||||
|   | ||||
| @@ -10,10 +10,7 @@ | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60", | ||||
|     "FastRetrieve": "localhost:6379", | ||||
|     "Etcd": "etcd.orb.local:2379", | ||||
|     "Stream": "nats.orb.local:4222" | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" | ||||
|   }, | ||||
|   "KnownProxies": [ | ||||
|     "127.0.0.1", | ||||
| @@ -24,8 +21,6 @@ | ||||
|   }, | ||||
|   "Service": { | ||||
|     "Name": "DysonNetwork.Develop", | ||||
|     "Url": "https://localhost:7192", | ||||
|     "ClientCert": "../Certificates/client.crt", | ||||
|     "ClientKey": "../Certificates/client.key" | ||||
|     "Url": "https://localhost:7192" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -31,7 +31,6 @@ public class AppDatabase( | ||||
|             opt => opt | ||||
|                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) | ||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) | ||||
|                 .UseNetTopologySuite() | ||||
|                 .UseNodaTime() | ||||
|         ).UseSnakeCaseNamingConvention(); | ||||
|  | ||||
|   | ||||
| @@ -35,7 +35,6 @@ | ||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" /> | ||||
|         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" /> | ||||
| @@ -67,6 +66,7 @@ | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|   | ||||
							
								
								
									
										403
									
								
								DysonNetwork.Drive/Migrations/20250907070034_RemoveNetTopo.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										403
									
								
								DysonNetwork.Drive/Migrations/20250907070034_RemoveNetTopo.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,403 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using NodaTime; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250907070034_RemoveNetTopo")] | ||||
|     partial class RemoveNetTopo | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "9.0.7") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("AccountId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<string>("Description") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<Instant?>("ExpiredAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expired_at"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<long>("Quota") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("quota"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_quota_records"); | ||||
|  | ||||
|                     b.ToTable("quota_records", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||
|                 { | ||||
|                     b.Property<string>("Id") | ||||
|                         .HasMaxLength(32) | ||||
|                         .HasColumnType("character varying(32)") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("AccountId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<Guid?>("BundleId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("bundle_id"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<string>("Description") | ||||
|                         .HasMaxLength(4096) | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<Instant?>("ExpiredAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expired_at"); | ||||
|  | ||||
|                     b.Property<Dictionary<string, object>>("FileMeta") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("file_meta"); | ||||
|  | ||||
|                     b.Property<bool>("HasCompression") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("has_compression"); | ||||
|  | ||||
|                     b.Property<bool>("HasThumbnail") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("has_thumbnail"); | ||||
|  | ||||
|                     b.Property<string>("Hash") | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("hash"); | ||||
|  | ||||
|                     b.Property<bool>("IsEncrypted") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_encrypted"); | ||||
|  | ||||
|                     b.Property<bool>("IsMarkedRecycle") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_marked_recycle"); | ||||
|  | ||||
|                     b.Property<string>("MimeType") | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("mime_type"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<Guid?>("PoolId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("pool_id"); | ||||
|  | ||||
|                     b.Property<List<ContentSensitiveMark>>("SensitiveMarks") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("sensitive_marks"); | ||||
|  | ||||
|                     b.Property<long>("Size") | ||||
|                         .HasColumnType("bigint") | ||||
|                         .HasColumnName("size"); | ||||
|  | ||||
|                     b.Property<string>("StorageId") | ||||
|                         .HasMaxLength(32) | ||||
|                         .HasColumnType("character varying(32)") | ||||
|                         .HasColumnName("storage_id"); | ||||
|  | ||||
|                     b.Property<string>("StorageUrl") | ||||
|                         .HasMaxLength(4096) | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("storage_url"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("UploadedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("uploaded_at"); | ||||
|  | ||||
|                     b.Property<Dictionary<string, object>>("UserMeta") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("user_meta"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_files"); | ||||
|  | ||||
|                     b.HasIndex("BundleId") | ||||
|                         .HasDatabaseName("ix_files_bundle_id"); | ||||
|  | ||||
|                     b.HasIndex("PoolId") | ||||
|                         .HasDatabaseName("ix_files_pool_id"); | ||||
|  | ||||
|                     b.ToTable("files", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("ExpiredAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expired_at"); | ||||
|  | ||||
|                     b.Property<string>("FileId") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(32) | ||||
|                         .HasColumnType("character varying(32)") | ||||
|                         .HasColumnName("file_id"); | ||||
|  | ||||
|                     b.Property<string>("ResourceId") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("resource_id"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<string>("Usage") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("usage"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_file_references"); | ||||
|  | ||||
|                     b.HasIndex("FileId") | ||||
|                         .HasDatabaseName("ix_file_references_file_id"); | ||||
|  | ||||
|                     b.ToTable("file_references", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("AccountId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<string>("Description") | ||||
|                         .HasMaxLength(8192) | ||||
|                         .HasColumnType("character varying(8192)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<Instant?>("ExpiredAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expired_at"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<string>("Passcode") | ||||
|                         .HasMaxLength(256) | ||||
|                         .HasColumnType("character varying(256)") | ||||
|                         .HasColumnName("passcode"); | ||||
|  | ||||
|                     b.Property<string>("Slug") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("slug"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_bundles"); | ||||
|  | ||||
|                     b.HasIndex("Slug") | ||||
|                         .IsUnique() | ||||
|                         .HasDatabaseName("ix_bundles_slug"); | ||||
|  | ||||
|                     b.ToTable("bundles", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid?>("AccountId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<BillingConfig>("BillingConfig") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("billing_config"); | ||||
|  | ||||
|                     b.Property<Instant>("CreatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("created_at"); | ||||
|  | ||||
|                     b.Property<Instant?>("DeletedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<string>("Description") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(8192) | ||||
|                         .HasColumnType("character varying(8192)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<bool>("IsHidden") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_hidden"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<PolicyConfig>("PolicyConfig") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("policy_config"); | ||||
|  | ||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("storage_config"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_pools"); | ||||
|  | ||||
|                     b.ToTable("pools", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FileBundle", "Bundle") | ||||
|                         .WithMany("Files") | ||||
|                         .HasForeignKey("BundleId") | ||||
|                         .HasConstraintName("fk_files_bundles_bundle_id"); | ||||
|  | ||||
|                     b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("PoolId") | ||||
|                         .HasConstraintName("fk_files_pools_pool_id"); | ||||
|  | ||||
|                     b.Navigation("Bundle"); | ||||
|  | ||||
|                     b.Navigation("Pool"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") | ||||
|                         .WithMany("References") | ||||
|                         .HasForeignKey("FileId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_file_references_files_file_id"); | ||||
|  | ||||
|                     b.Navigation("File"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||
|                 { | ||||
|                     b.Navigation("References"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => | ||||
|                 { | ||||
|                     b.Navigation("Files"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class RemoveNetTopo : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AlterDatabase() | ||||
|                 .OldAnnotation("Npgsql:PostgresExtension:postgis", ",,"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AlterDatabase() | ||||
|                 .Annotation("Npgsql:PostgresExtension:postgis", ",,"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -24,7 +24,6 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                 .HasAnnotation("ProductVersion", "9.0.7") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => | ||||
|   | ||||
| @@ -5,18 +5,18 @@ using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using DysonNetwork.Shared.PageData; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using tusdotnet.Stores; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| builder.AddServiceDefaults(); | ||||
|  | ||||
| // Configure Kestrel and server options | ||||
| builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue); | ||||
|  | ||||
| // Add application services | ||||
| builder.Services.AddRegistryService(builder.Configuration); | ||||
| builder.Services.AddStreamConnection(builder.Configuration); | ||||
|  | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
| builder.Services.AddAppRateLimiting(); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| @@ -39,6 +39,8 @@ builder.Services.AddTransient<IPageDataProvider, VersionPageData>(); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| app.MapDefaultEndpoints(); | ||||
|  | ||||
| // Run database migrations | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
| @@ -51,8 +53,6 @@ var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>(); | ||||
| // Configure application middleware pipeline | ||||
| app.ConfigureAppMiddleware(tusDiskStore, builder.Environment.ContentRootPath); | ||||
|  | ||||
| app.MapGatewayProxy(); | ||||
|  | ||||
| app.MapPages(Path.Combine(app.Environment.WebRootPath, "dist", "index.html")); | ||||
|  | ||||
| // Configure gRPC | ||||
|   | ||||
| @@ -3,6 +3,8 @@ using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NATS.Client.Core; | ||||
| using NATS.Client.JetStream.Models; | ||||
| using NATS.Net; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Startup; | ||||
|  | ||||
| @@ -14,12 +16,23 @@ public class BroadcastEventHandler( | ||||
| { | ||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|     { | ||||
|         await foreach (var msg in nats.SubscribeAsync<byte[]>(AccountDeletedEvent.Type, cancellationToken: stoppingToken)) | ||||
|         var js = nats.CreateJetStreamContext(); | ||||
|  | ||||
|         await js.EnsureStreamCreated("account_events", [AccountDeletedEvent.Type]); | ||||
|          | ||||
|         var consumer = await js.CreateOrUpdateConsumerAsync("account_events", | ||||
|             new ConsumerConfig("drive_account_deleted_handler"), cancellationToken: stoppingToken); | ||||
|  | ||||
|         await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken)) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var evt = JsonSerializer.Deserialize<AccountDeletedEvent>(msg.Data); | ||||
|                 if (evt == null) continue; | ||||
|                 if (evt == null) | ||||
|                 { | ||||
|                     await msg.AckAsync(cancellationToken: stoppingToken); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 logger.LogInformation("Account deleted: {AccountId}", evt.AccountId); | ||||
|  | ||||
| @@ -46,10 +59,13 @@ public class BroadcastEventHandler( | ||||
|                     await transaction.RollbackAsync(cancellationToken: stoppingToken); | ||||
|                     throw; | ||||
|                 } | ||||
|  | ||||
|                 await msg.AckAsync(cancellationToken: stoppingToken); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 logger.LogError(ex, "Error processing AccountDeleted"); | ||||
|                 await msg.NakAsync(cancellationToken: stoppingToken); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using System.Threading.RateLimiting; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.AspNetCore.RateLimiting; | ||||
| @@ -16,11 +17,6 @@ public static class ServiceCollectionExtensions | ||||
|     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddDbContext<AppDatabase>(); // Assuming you'll have an AppDatabase | ||||
|         services.AddSingleton<IConnectionMultiplexer>(_ => | ||||
|         { | ||||
|             var connection = configuration.GetConnectionString("FastRetrieve")!; | ||||
|             return ConnectionMultiplexer.Connect(connection); | ||||
|         }); | ||||
|         services.AddSingleton<IClock>(SystemClock.Instance); | ||||
|         services.AddHttpContextAccessor(); | ||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); // Uncomment if you have CacheServiceRedis | ||||
| @@ -40,6 +36,7 @@ public static class ServiceCollectionExtensions | ||||
|  | ||||
|         services.AddControllers().AddJsonOptions(options => | ||||
|         { | ||||
|             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|  | ||||
|   | ||||
| @@ -10,10 +10,7 @@ | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_drive;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60", | ||||
|     "FastRetrieve": "localhost:6379", | ||||
|     "Etcd": "etcd.orb.local:2379", | ||||
|     "Stream": "nats.orb.local:4222" | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_drive;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" | ||||
|   }, | ||||
|   "Authentication": { | ||||
|     "Schemes": { | ||||
| @@ -131,8 +128,6 @@ | ||||
|   ], | ||||
|   "Service": { | ||||
|     "Name": "DysonNetwork.Drive", | ||||
|     "Url": "https://localhost:7092", | ||||
|     "ClientCert": "../Certificates/client.crt", | ||||
|     "ClientKey": "../Certificates/client.key" | ||||
|     "Url": "https://localhost:7092" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,108 +0,0 @@ | ||||
| using System.Text; | ||||
| using System.Text.Json.Serialization; | ||||
| using dotnet_etcd.interfaces; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Yarp.ReverseProxy.Configuration; | ||||
|  | ||||
| namespace DysonNetwork.Gateway.Controllers; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/.well-known")] | ||||
| public class WellKnownController( | ||||
|     IConfiguration configuration, | ||||
|     IProxyConfigProvider proxyConfigProvider, | ||||
|     IEtcdClient etcdClient) | ||||
|     : ControllerBase | ||||
| { | ||||
|     public class IpCheckResponse | ||||
|     { | ||||
|         [JsonPropertyName("remote_ip")] public string? RemoteIp { get; set; } | ||||
|         [JsonPropertyName("x_forwarded_for")] public string? XForwardedFor { get; set; } | ||||
|         [JsonPropertyName("x_forwarded_proto")] public string? XForwardedProto { get; set; } | ||||
|         [JsonPropertyName("x_forwarded_host")] public string? XForwardedHost { get; set; } | ||||
|         [JsonPropertyName("x_real_ip")] public string? XRealIp { get; set; } | ||||
|     } | ||||
|      | ||||
|     [HttpGet("ip-check")] | ||||
|     public ActionResult<IpCheckResponse> GetIpCheck() | ||||
|     { | ||||
|         var ip = HttpContext.Connection.RemoteIpAddress?.ToString(); | ||||
|  | ||||
|         var xForwardedFor = Request.Headers["X-Forwarded-For"].FirstOrDefault(); | ||||
|         var xForwardedProto = Request.Headers["X-Forwarded-Proto"].FirstOrDefault(); | ||||
|         var xForwardedHost = Request.Headers["X-Forwarded-Host"].FirstOrDefault(); | ||||
|         var realIp = Request.Headers["X-Real-IP"].FirstOrDefault(); | ||||
|  | ||||
|         return Ok(new IpCheckResponse | ||||
|         { | ||||
|             RemoteIp = ip, | ||||
|             XForwardedFor = xForwardedFor, | ||||
|             XForwardedProto = xForwardedProto, | ||||
|             XForwardedHost = xForwardedHost, | ||||
|             XRealIp = realIp | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     [HttpGet("domains")] | ||||
|     public IActionResult GetDomainMappings() | ||||
|     { | ||||
|         var domainMappings = configuration.GetSection("DomainMappings").GetChildren() | ||||
|             .ToDictionary(x => x.Key, x => x.Value); | ||||
|         return Ok(domainMappings); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("services")] | ||||
|     public IActionResult GetServices() | ||||
|     { | ||||
|         var local = configuration.GetValue<bool>("LocalMode"); | ||||
|         var response = etcdClient.GetRange("/services/"); | ||||
|         var kvs = response.Kvs; | ||||
|  | ||||
|         var serviceMap = kvs.ToDictionary( | ||||
|             kv => Encoding.UTF8.GetString(kv.Key.ToByteArray()).Replace("/services/", ""), | ||||
|             kv => Encoding.UTF8.GetString(kv.Value.ToByteArray()) | ||||
|         ); | ||||
|  | ||||
|         if (local) return Ok(serviceMap); | ||||
|          | ||||
|         var domainMappings = configuration.GetSection("DomainMappings").GetChildren() | ||||
|             .ToDictionary(x => x.Key, x => x.Value); | ||||
|         foreach (var (key, _) in serviceMap.ToList()) | ||||
|         { | ||||
|             if (!domainMappings.TryGetValue(key, out var domain)) continue; | ||||
|             if (domain is not null) | ||||
|                 serviceMap[key] = "https://" + domain; | ||||
|         } | ||||
|  | ||||
|         return Ok(serviceMap); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("routes")] | ||||
|     public IActionResult GetProxyRules() | ||||
|     { | ||||
|         var config = proxyConfigProvider.GetConfig(); | ||||
|         var rules = config.Routes.Select(r => new | ||||
|         { | ||||
|             r.RouteId, | ||||
|             r.ClusterId, | ||||
|             Match = new | ||||
|             { | ||||
|                 r.Match.Path, | ||||
|                 Hosts = r.Match.Hosts != null ? string.Join(", ", r.Match.Hosts) : null | ||||
|             }, | ||||
|             Transforms = r.Transforms?.Select(t => t.Select(kv => $"{kv.Key}: {kv.Value}").ToList()) | ||||
|         }).ToList(); | ||||
|  | ||||
|         var clusters = config.Clusters.Select(c => new | ||||
|         { | ||||
|             c.ClusterId, | ||||
|             Destinations = c.Destinations?.Select(d => new | ||||
|             { | ||||
|                 d.Key, | ||||
|                 d.Value.Address | ||||
|             }).ToList() | ||||
|         }).ToList(); | ||||
|  | ||||
|         return Ok(new { Rules = rules, Clusters = clusters }); | ||||
|     } | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base | ||||
| USER $APP_UID | ||||
| WORKDIR /app | ||||
| EXPOSE 8080 | ||||
| EXPOSE 8081 | ||||
|  | ||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| WORKDIR /src | ||||
| COPY ["DysonNetwork.Gateway/DysonNetwork.Gateway.csproj", "DysonNetwork.Gateway/"] | ||||
| RUN dotnet restore "DysonNetwork.Gateway/DysonNetwork.Gateway.csproj" | ||||
| COPY . . | ||||
| WORKDIR "/src/DysonNetwork.Gateway" | ||||
| RUN dotnet build "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/build | ||||
|  | ||||
| FROM build AS publish | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| RUN dotnet publish "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false | ||||
|  | ||||
| FROM base AS final | ||||
| WORKDIR /app | ||||
| COPY --from=publish /app/publish . | ||||
| ENTRYPOINT ["dotnet", "DysonNetwork.Gateway.dll"] | ||||
| @@ -1,23 +0,0 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net9.0</TargetFramework> | ||||
|     <Nullable>enable</Nullable> | ||||
|     <ImplicitUsings>enable</ImplicitUsings> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="dotnet-etcd" Version="8.0.1" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" /> | ||||
|     <PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115"> | ||||
|       <PrivateAssets>all</PrivateAssets> | ||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|     </PackageReference> | ||||
|     <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
| @@ -1,36 +0,0 @@ | ||||
| using DysonNetwork.Gateway.Startup; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using Microsoft.AspNetCore.HttpOverrides; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| builder.Host.UseContentRoot(Directory.GetCurrentDirectory()); | ||||
| builder.WebHost.ConfigureKestrel(options => | ||||
| { | ||||
|     options.Limits.MaxRequestBodySize = long.MaxValue; | ||||
|     options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2); | ||||
|     options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30); | ||||
| }); | ||||
|  | ||||
| // Add services to the container. | ||||
| builder.Services.AddGateway(builder.Configuration); | ||||
| builder.Services.AddControllers(); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| app.ConfigureForwardedHeaders(app.Configuration); | ||||
|  | ||||
| app.UseRequestTimeouts(); | ||||
| app.UseCors(opts => | ||||
|     opts.SetIsOriginAllowed(_ => true) | ||||
|         .WithExposedHeaders("*") | ||||
|         .WithHeaders("*") | ||||
|         .AllowCredentials() | ||||
|         .AllowAnyHeader() | ||||
|         .AllowAnyMethod() | ||||
| ); | ||||
|  | ||||
| app.MapControllers(); | ||||
| app.MapReverseProxy(); | ||||
|  | ||||
| app.Run(); | ||||
| @@ -1,23 +0,0 @@ | ||||
| { | ||||
|   "$schema": "https://json.schemastore.org/launchsettings.json", | ||||
|   "profiles": { | ||||
|     "http": { | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "http://localhost:5094", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
|     }, | ||||
|     "https": { | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "https://localhost:7034;http://0.0.0.0:5094", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,239 +0,0 @@ | ||||
| using System.Text; | ||||
| using dotnet_etcd.interfaces; | ||||
| using Yarp.ReverseProxy.Configuration; | ||||
| using Yarp.ReverseProxy.Forwarder; | ||||
|  | ||||
| namespace DysonNetwork.Gateway; | ||||
|  | ||||
| public class RegistryProxyConfigProvider : IProxyConfigProvider, IDisposable | ||||
| { | ||||
|     private readonly object _lock = new(); | ||||
|     private readonly IEtcdClient _etcdClient; | ||||
|     private readonly IConfiguration _configuration; | ||||
|     private readonly ILogger<RegistryProxyConfigProvider> _logger; | ||||
|     private readonly CancellationTokenSource _watchCts = new(); | ||||
|     private CancellationTokenSource _cts; | ||||
|     private IProxyConfig _config; | ||||
|  | ||||
|     public RegistryProxyConfigProvider( | ||||
|         IEtcdClient etcdClient, | ||||
|         IConfiguration configuration, | ||||
|         ILogger<RegistryProxyConfigProvider> logger | ||||
|     ) | ||||
|     { | ||||
|         _etcdClient = etcdClient; | ||||
|         _configuration = configuration; | ||||
|         _logger = logger; | ||||
|         _cts = new CancellationTokenSource(); | ||||
|         _config = LoadConfig(); | ||||
|  | ||||
|         // Watch for changes in etcd | ||||
|         _etcdClient.WatchRange("/services/", _ => | ||||
|         { | ||||
|             _logger.LogInformation("Etcd configuration changed. Reloading proxy config."); | ||||
|             ReloadConfig(); | ||||
|         }, cancellationToken: _watchCts.Token); | ||||
|     } | ||||
|  | ||||
|     public IProxyConfig GetConfig() => _config; | ||||
|  | ||||
|     private void ReloadConfig() | ||||
|     { | ||||
|         lock (_lock) | ||||
|         { | ||||
|             var oldCts = _cts; | ||||
|             _cts = new CancellationTokenSource(); | ||||
|             _config = LoadConfig(); | ||||
|             oldCts.Cancel(); | ||||
|             oldCts.Dispose(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private IProxyConfig LoadConfig() | ||||
|     { | ||||
|         _logger.LogInformation("Generating new proxy config."); | ||||
|         var response = _etcdClient.GetRange("/services/"); | ||||
|         var kvs = response.Kvs; | ||||
|  | ||||
|         var serviceMap = kvs.ToDictionary( | ||||
|             kv => Encoding.UTF8.GetString(kv.Key.ToByteArray()).Replace("/services/", ""), | ||||
|             kv => Encoding.UTF8.GetString(kv.Value.ToByteArray()) | ||||
|         ); | ||||
|  | ||||
|         var clusters = new List<ClusterConfig>(); | ||||
|         var routes = new List<RouteConfig>(); | ||||
|  | ||||
|         var domainMappings = _configuration.GetSection("DomainMappings").GetChildren() | ||||
|             .ToDictionary(x => x.Key, x => x.Value); | ||||
|  | ||||
|         var pathAliases = _configuration.GetSection("PathAliases").GetChildren() | ||||
|             .ToDictionary(x => x.Key, x => x.Value); | ||||
|  | ||||
|         var directRoutes = _configuration.GetSection("DirectRoutes").Get<List<DirectRouteConfig>>() ?? | ||||
|                            []; | ||||
|  | ||||
|         _logger.LogInformation("Indexing {ServiceCount} services from Etcd.", kvs.Count); | ||||
|  | ||||
|         var gatewayServiceName = _configuration["Service:Name"]; | ||||
|  | ||||
|         // Add direct routes | ||||
|         foreach (var directRoute in directRoutes) | ||||
|         { | ||||
|             if (serviceMap.TryGetValue(directRoute.Service, out var serviceUrl)) | ||||
|             { | ||||
|                 var existingCluster = clusters.FirstOrDefault(c => c.ClusterId == directRoute.Service); | ||||
|                 if (existingCluster is null) | ||||
|                 { | ||||
|                     var cluster = new ClusterConfig | ||||
|                     { | ||||
|                         ClusterId = directRoute.Service, | ||||
|                         Destinations = new Dictionary<string, DestinationConfig> | ||||
|                         { | ||||
|                             { "destination1", new DestinationConfig { Address = serviceUrl } } | ||||
|                         }, | ||||
|                     }; | ||||
|                     clusters.Add(cluster); | ||||
|                 } | ||||
|  | ||||
|                 var route = new RouteConfig | ||||
|                 { | ||||
|                     RouteId = $"direct-{directRoute.Service}-{directRoute.Path.Replace("/", "-")}", | ||||
|                     ClusterId = directRoute.Service, | ||||
|                     Match = new RouteMatch { Path = directRoute.Path }, | ||||
|                 }; | ||||
|                 routes.Add(route); | ||||
|                 _logger.LogInformation("    Added Direct Route: {Path} -> {Service}", directRoute.Path, | ||||
|                     directRoute.Service); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _logger.LogWarning("    Direct route service {Service} not found in Etcd.", directRoute.Service); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         foreach (var serviceName in serviceMap.Keys) | ||||
|         { | ||||
|             if (serviceName == gatewayServiceName) | ||||
|             { | ||||
|                 _logger.LogInformation("Skipping gateway service: {ServiceName}", serviceName); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             var serviceUrl = serviceMap[serviceName]; | ||||
|  | ||||
|             // Determine the path alias | ||||
|             string? pathAlias; | ||||
|             pathAlias = pathAliases.TryGetValue(serviceName, out var alias) | ||||
|                 ? alias | ||||
|                 : serviceName.Split('.').Last().ToLowerInvariant(); | ||||
|  | ||||
|             _logger.LogInformation("  Service: {ServiceName}, URL: {ServiceUrl}, Path Alias: {PathAlias}", serviceName, | ||||
|                 serviceUrl, pathAlias); | ||||
|  | ||||
|             // Check if the cluster already exists | ||||
|             var existingCluster = clusters.FirstOrDefault(c => c.ClusterId == serviceName); | ||||
|             if (existingCluster == null) | ||||
|             { | ||||
|                 var cluster = new ClusterConfig | ||||
|                 { | ||||
|                     ClusterId = serviceName, | ||||
|                     Destinations = new Dictionary<string, DestinationConfig> | ||||
|                     { | ||||
|                         { "destination1", new DestinationConfig { Address = serviceUrl } } | ||||
|                     } | ||||
|                 }; | ||||
|                 clusters.Add(cluster); | ||||
|                 _logger.LogInformation("  Added Cluster: {ServiceName}", serviceName); | ||||
|             } | ||||
|             else if (existingCluster.Destinations is not null) | ||||
|             { | ||||
|                 // Create a new cluster with merged destinations | ||||
|                 var newDestinations = new Dictionary<string, DestinationConfig>(existingCluster.Destinations) | ||||
|                 { | ||||
|                     { | ||||
|                         $"destination{existingCluster.Destinations.Count + 1}", | ||||
|                         new DestinationConfig { Address = serviceUrl } | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 var mergedCluster = new ClusterConfig | ||||
|                 { | ||||
|                     ClusterId = serviceName, | ||||
|                     Destinations = newDestinations | ||||
|                 }; | ||||
|  | ||||
|                 // Replace the existing cluster with the merged one | ||||
|                 var index = clusters.IndexOf(existingCluster); | ||||
|                 clusters[index] = mergedCluster; | ||||
|  | ||||
|                 _logger.LogInformation("  Updated Cluster {ServiceName} with {DestinationCount} destinations", | ||||
|                     serviceName, mergedCluster.Destinations.Count); | ||||
|             } | ||||
|  | ||||
|             // Host-based routing | ||||
|             if (domainMappings.TryGetValue(serviceName, out var domain) && domain is not null) | ||||
|             { | ||||
|                 var hostRoute = new RouteConfig | ||||
|                 { | ||||
|                     RouteId = $"{serviceName}-host", | ||||
|                     ClusterId = serviceName, | ||||
|                     Match = new RouteMatch | ||||
|                     { | ||||
|                         Hosts = [domain], | ||||
|                         Path = "/{**catch-all}" | ||||
|                     }, | ||||
|                 }; | ||||
|                 routes.Add(hostRoute); | ||||
|                 _logger.LogInformation("    Added Host-based Route: {Host}", domain); | ||||
|             } | ||||
|  | ||||
|             // Path-based routing | ||||
|             var pathRoute = new RouteConfig | ||||
|             { | ||||
|                 RouteId = $"{serviceName}-path", | ||||
|                 ClusterId = serviceName, | ||||
|                 Match = new RouteMatch { Path = $"/{pathAlias}/{{**catch-all}}" }, | ||||
|                 Transforms = new List<Dictionary<string, string>> | ||||
|                 { | ||||
|                     new() { { "PathRemovePrefix", $"/{pathAlias}" } }, | ||||
|                     new() { { "PathPrefix", "/api" } }, | ||||
|                 }, | ||||
|                 Timeout = TimeSpan.FromSeconds(5) | ||||
|             }; | ||||
|             routes.Add(pathRoute); | ||||
|             _logger.LogInformation("    Added Path-based Route: {Path}", pathRoute.Match.Path); | ||||
|         } | ||||
|  | ||||
|         return new CustomProxyConfig( | ||||
|             routes, | ||||
|             clusters, | ||||
|             new Microsoft.Extensions.Primitives.CancellationChangeToken(_cts.Token) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private class CustomProxyConfig( | ||||
|         IReadOnlyList<RouteConfig> routes, | ||||
|         IReadOnlyList<ClusterConfig> clusters, | ||||
|         Microsoft.Extensions.Primitives.IChangeToken changeToken | ||||
|     ) | ||||
|         : IProxyConfig | ||||
|     { | ||||
|         public IReadOnlyList<RouteConfig> Routes { get; } = routes; | ||||
|         public IReadOnlyList<ClusterConfig> Clusters { get; } = clusters; | ||||
|         public Microsoft.Extensions.Primitives.IChangeToken ChangeToken { get; } = changeToken; | ||||
|     } | ||||
|  | ||||
|     public record DirectRouteConfig | ||||
|     { | ||||
|         public required string Path { get; set; } | ||||
|         public required string Service { get; set; } | ||||
|     } | ||||
|  | ||||
|     public virtual void Dispose() | ||||
|     { | ||||
|         _cts.Cancel(); | ||||
|         _cts.Dispose(); | ||||
|         _watchCts.Cancel(); | ||||
|         _watchCts.Dispose(); | ||||
|     } | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| using System.Net.Security; | ||||
| using System.Security.Cryptography.X509Certificates; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Yarp.ReverseProxy.Configuration; | ||||
| using Yarp.ReverseProxy.Transforms; | ||||
|  | ||||
| namespace DysonNetwork.Gateway.Startup; | ||||
|  | ||||
| public static class ServiceCollectionExtensions | ||||
| { | ||||
|     public static IServiceCollection AddGateway(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddRequestTimeouts(); | ||||
|  | ||||
|         services | ||||
|             .AddReverseProxy() | ||||
|             .ConfigureHttpClient((context, handler) => | ||||
|             { | ||||
|                 // var caCert = X509CertificateLoader.LoadCertificateFromFile(configuration["CaCert"]!); | ||||
|                 handler.SslOptions = new SslClientAuthenticationOptions | ||||
|                 { | ||||
|                     RemoteCertificateValidationCallback = (sender, cert, chain, errors) => true | ||||
|                 }; | ||||
|             }) | ||||
|             .AddTransforms(context => | ||||
|             { | ||||
|                 context.CopyRequestHeaders = true; | ||||
|                 context.AddOriginalHost(); | ||||
|                 context.AddForwarded(action: ForwardedTransformActions.Set); | ||||
|                 context.AddXForwarded(action: ForwardedTransformActions.Set); | ||||
|             }); | ||||
|  | ||||
|         services.AddRegistryService(configuration, addForwarder: false); | ||||
|         services.AddSingleton<IProxyConfigProvider, RegistryProxyConfigProvider>(); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| namespace DysonNetwork.Gateway; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/version")] | ||||
| public class VersionController : ControllerBase | ||||
| { | ||||
|     [HttpGet] | ||||
|     public IActionResult Get() | ||||
|     { | ||||
|         return Ok(new AppVersion | ||||
|         { | ||||
|             Version = ThisAssembly.AssemblyVersion, | ||||
|             Commit = ThisAssembly.GitCommitId, | ||||
|             UpdateDate = ThisAssembly.GitCommitDate | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @@ -1,49 +0,0 @@ | ||||
| { | ||||
|   "LocalMode": true, | ||||
|   "CaCert": "../Certificates/ca.crt", | ||||
|   "Logging": { | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|       "Microsoft.AspNetCore": "Warning" | ||||
|     } | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "Etcd": "etcd.orb.local:2379" | ||||
|   }, | ||||
|   "Etcd": { | ||||
|     "Insecure": true | ||||
|   }, | ||||
|   "Service": { | ||||
|     "Name": "DysonNetwork.Gateway", | ||||
|     "Url": "https://localhost:7034" | ||||
|   }, | ||||
|   "DomainMappings": { | ||||
|     "DysonNetwork.Pass": "id.solsynth.dev", | ||||
|     "DysonNetwork.Drive": "drive.solsynth.dev", | ||||
|     "DysonNetwork.Pusher": "push.solsynth.dev", | ||||
|     "DysonNetwork.Sphere": "sphere.solsynth.dev" | ||||
|   }, | ||||
|   "PathAliases": { | ||||
|     "DysonNetwork.Pass": "id", | ||||
|     "DysonNetwork.Drive": "drive" | ||||
|   }, | ||||
|   "DirectRoutes": [ | ||||
|     { | ||||
|       "Path": "/ws", | ||||
|       "Service": "DysonNetwork.Pusher" | ||||
|     }, | ||||
|     { | ||||
|       "Path": "/api/tus", | ||||
|       "Service": "DysonNetwork.Drive" | ||||
|     }, | ||||
|     { | ||||
|       "Path": "/.well-known/openid-configuration", | ||||
|       "Service": "DysonNetwork.Pass" | ||||
|     }, | ||||
|     { | ||||
|       "Path": "/.well-known/jwks", | ||||
|       "Service": "DysonNetwork.Pass" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -1,7 +0,0 @@ | ||||
| { | ||||
|   "version": "1.0", | ||||
|   "publicReleaseRefSpec": ["^refs/heads/main$"], | ||||
|   "cloudBuild": { | ||||
|     "setVersionVariables": true | ||||
|   } | ||||
| } | ||||
| @@ -191,7 +191,9 @@ public class AccountController( | ||||
|         public StatusAttitude Attitude { get; set; } | ||||
|         public bool IsInvisible { get; set; } | ||||
|         public bool IsNotDisturb { get; set; } | ||||
|         public bool IsAutomated { get; set; } = false; | ||||
|         [MaxLength(1024)] public string? Label { get; set; } | ||||
|         [MaxLength(4096)] public string? AppIdentifier { get; set; } | ||||
|         public Instant? ClearedAt { get; set; } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -197,6 +197,8 @@ public class AccountCurrentController( | ||||
|     public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (request is { IsAutomated: true, AppIdentifier: not null }) | ||||
|             return BadRequest("Automated status cannot be updated."); | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var status = await db.AccountStatuses | ||||
| @@ -205,11 +207,15 @@ public class AccountCurrentController( | ||||
|             .OrderByDescending(e => e.CreatedAt) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (status is null) return NotFound(ApiError.NotFound("status", traceId: HttpContext.TraceIdentifier)); | ||||
|         if (status.IsAutomated && request.AppIdentifier is null) | ||||
|             return BadRequest("Automated status cannot be updated."); | ||||
|  | ||||
|         status.Attitude = request.Attitude; | ||||
|         status.IsInvisible = request.IsInvisible; | ||||
|         status.IsNotDisturb = request.IsNotDisturb; | ||||
|         status.IsAutomated = request.IsAutomated; | ||||
|         status.Label = request.Label; | ||||
|         status.AppIdentifier = request.AppIdentifier; | ||||
|         status.ClearedAt = request.ClearedAt; | ||||
|  | ||||
|         db.Update(status); | ||||
| @@ -225,13 +231,44 @@ public class AccountCurrentController( | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         if (request is { IsAutomated: true, AppIdentifier: not null }) | ||||
|         { | ||||
|             var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|             var existingStatus = await db.AccountStatuses | ||||
|                 .Where(s => s.AccountId == currentUser.Id) | ||||
|                 .Where(s => s.ClearedAt == null || s.ClearedAt > now) | ||||
|                 .OrderByDescending(s => s.CreatedAt) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (existingStatus is not null && existingStatus.IsAutomated) | ||||
|                 if (existingStatus.IsAutomated && request.AppIdentifier == existingStatus.AppIdentifier) | ||||
|                 { | ||||
|                     existingStatus.Attitude = request.Attitude; | ||||
|                     existingStatus.IsInvisible = request.IsInvisible; | ||||
|                     existingStatus.IsNotDisturb = request.IsNotDisturb; | ||||
|                     existingStatus.Label = request.Label; | ||||
|                     db.Update(existingStatus); | ||||
|                     await db.SaveChangesAsync(); | ||||
|                     return Ok(existingStatus); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     existingStatus.ClearedAt = now; | ||||
|                     db.Update(existingStatus); | ||||
|                     await db.SaveChangesAsync(); | ||||
|                 } | ||||
|             else if (existingStatus is not null) | ||||
|                 return Ok(existingStatus); // Do not override manually set status with automated ones | ||||
|         } | ||||
|  | ||||
|         var status = new Status | ||||
|         { | ||||
|             AccountId = currentUser.Id, | ||||
|             Attitude = request.Attitude, | ||||
|             IsInvisible = request.IsInvisible, | ||||
|             IsNotDisturb = request.IsNotDisturb, | ||||
|             IsAutomated = request.IsAutomated, | ||||
|             Label = request.Label, | ||||
|             AppIdentifier = request.AppIdentifier, | ||||
|             ClearedAt = request.ClearedAt | ||||
|         }; | ||||
|  | ||||
| @@ -239,15 +276,21 @@ public class AccountCurrentController( | ||||
|     } | ||||
|  | ||||
|     [HttpDelete("statuses")] | ||||
|     public async Task<ActionResult> DeleteStatus() | ||||
|     public async Task<ActionResult> DeleteStatus([FromQuery] string? app) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var status = await db.AccountStatuses | ||||
|         var queryable = db.AccountStatuses | ||||
|             .Where(s => s.AccountId == currentUser.Id) | ||||
|             .Where(s => s.ClearedAt == null || s.ClearedAt > now) | ||||
|             .OrderByDescending(s => s.CreatedAt) | ||||
|             .AsQueryable(); | ||||
|  | ||||
|         if (string.IsNullOrWhiteSpace(app)) | ||||
|             queryable = queryable.Where(s => s.IsAutomated && s.AppIdentifier == app); | ||||
|  | ||||
|         var status = await queryable | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (status is null) return NotFound(); | ||||
|  | ||||
|   | ||||
| @@ -14,7 +14,7 @@ public class AccountEventService( | ||||
|     Wallet.PaymentService payment, | ||||
|     ICacheService cache, | ||||
|     IStringLocalizer<Localization.AccountEventResource> localizer, | ||||
|     PusherService.PusherServiceClient pusher, | ||||
|     RingService.RingServiceClient pusher, | ||||
|     SubscriptionService subscriptions, | ||||
|     Pass.Leveling.ExperienceService experienceService | ||||
| ) | ||||
|   | ||||
| @@ -13,6 +13,8 @@ using EFCore.BulkExtensions; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NATS.Client.Core; | ||||
| using NATS.Client.JetStream; | ||||
| using NATS.Net; | ||||
| using NodaTime; | ||||
| using OtpNet; | ||||
| using AuthService = DysonNetwork.Pass.Auth.AuthService; | ||||
| @@ -26,7 +28,7 @@ public class AccountService( | ||||
|     FileReferenceService.FileReferenceServiceClient fileRefs, | ||||
|     AccountUsernameService uname, | ||||
|     EmailService mailer, | ||||
|     PusherService.PusherServiceClient pusher, | ||||
|     RingService.RingServiceClient pusher, | ||||
|     IStringLocalizer<NotificationResource> localizer, | ||||
|     IStringLocalizer<EmailResource> emailLocalizer, | ||||
|     ICacheService cache, | ||||
| @@ -189,7 +191,8 @@ public class AccountService( | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public async Task<Account> CreateBotAccount(Account account, Guid automatedId, string? pictureId, string? backgroundId) | ||||
|     public async Task<Account> CreateBotAccount(Account account, Guid automatedId, string? pictureId, | ||||
|         string? backgroundId) | ||||
|     { | ||||
|         var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync(); | ||||
|         if (dupeAutomateCount > 0) | ||||
| @@ -740,10 +743,14 @@ public class AccountService( | ||||
|         db.Accounts.Remove(account); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         await nats.PublishAsync(AccountDeletedEvent.Type, JsonSerializer.SerializeToUtf8Bytes(new AccountDeletedEvent | ||||
|         var js = nats.CreateJetStreamContext(); | ||||
|         await js.PublishAsync( | ||||
|             AccountDeletedEvent.Type, | ||||
|             GrpcTypeHelper.ConvertObjectToByteString(new AccountDeletedEvent | ||||
|             { | ||||
|                 AccountId = account.Id, | ||||
|                 DeletedAt = SystemClock.Instance.GetCurrentInstant() | ||||
|         })); | ||||
|             }).ToByteArray() | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +1,8 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.GeoIp; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
| using Point = NetTopologySuite.Geometries.Point; | ||||
| @@ -14,7 +16,7 @@ public class ActionLog : ModelBase | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); | ||||
|     [MaxLength(512)] public string? UserAgent { get; set; } | ||||
|     [MaxLength(128)] public string? IpAddress { get; set; } | ||||
|     public Point? Location { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public GeoPoint? Location { get; set; } | ||||
|  | ||||
|     public Guid AccountId { get; set; } | ||||
|     public Account Account { get; set; } = null!; | ||||
|   | ||||
| @@ -23,6 +23,12 @@ public class Status : ModelBase | ||||
|     public bool IsNotDisturb { get; set; } | ||||
|     [MaxLength(1024)] public string? Label { get; set; } | ||||
|     public Instant? ClearedAt { get; set; } | ||||
|     [MaxLength(4096)] public string? AppIdentifier { get; set; } | ||||
|      | ||||
|     /// <summary> | ||||
|     /// Indicates this status is created based on running process or rich presence | ||||
|     /// </summary> | ||||
|     public bool IsAutomated { get; set; } | ||||
|  | ||||
|     public Guid AccountId { get; set; } | ||||
|     public Account Account { get; set; } = null!; | ||||
|   | ||||
							
								
								
									
										53
									
								
								DysonNetwork.Pass/Account/NotableDay.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								DysonNetwork.Pass/Account/NotableDay.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| using Nager.Holiday; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| /// <summary> | ||||
| /// Reference from Nager.Holiday | ||||
| /// </summary> | ||||
| public enum NotableHolidayType | ||||
| { | ||||
|     /// <summary>Public holiday</summary> | ||||
|     Public, | ||||
|     /// <summary>Bank holiday, banks and offices are closed</summary> | ||||
|     Bank, | ||||
|     /// <summary>School holiday, schools are closed</summary> | ||||
|     School, | ||||
|     /// <summary>Authorities are closed</summary> | ||||
|     Authorities, | ||||
|     /// <summary>Majority of people take a day off</summary> | ||||
|     Optional, | ||||
|     /// <summary>Optional festivity, no paid day off</summary> | ||||
|     Observance, | ||||
| } | ||||
|  | ||||
|  | ||||
| public class NotableDay | ||||
| { | ||||
|     public Instant Date { get; set; } | ||||
|     public string? LocalName { get; set; } | ||||
|     public string? GlobalName { get; set; } | ||||
|     public string? CountryCode { get; set; } | ||||
|     public NotableHolidayType[] Holidays { get; set; } = []; | ||||
|  | ||||
|     public static NotableDay FromNagerHoliday(PublicHoliday holiday) | ||||
|     { | ||||
|         return new NotableDay() | ||||
|         { | ||||
|             Date = Instant.FromDateTimeUtc(holiday.Date.ToUniversalTime()), | ||||
|             LocalName = holiday.LocalName, | ||||
|             GlobalName = holiday.Name, | ||||
|             CountryCode = holiday.CountryCode, | ||||
|             Holidays = holiday.Types?.Select(x => x switch | ||||
|             { | ||||
|                 PublicHolidayType.Public => NotableHolidayType.Public, | ||||
|                 PublicHolidayType.Bank => NotableHolidayType.Bank, | ||||
|                 PublicHolidayType.School => NotableHolidayType.School, | ||||
|                 PublicHolidayType.Authorities => NotableHolidayType.Authorities, | ||||
|                 PublicHolidayType.Optional => NotableHolidayType.Optional, | ||||
|                 _ => NotableHolidayType.Observance | ||||
|             }).ToArray() ?? [], | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										79
									
								
								DysonNetwork.Pass/Account/NotableDaysController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								DysonNetwork.Pass/Account/NotableDaysController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/notable")] | ||||
| public class NotableDaysController(NotableDaysService days) : ControllerBase | ||||
| { | ||||
|     [HttpGet("{regionCode}/{year:int}")] | ||||
|     public async Task<ActionResult<List<NotableDay>>> GetRegionDays(string regionCode, int year) | ||||
|     { | ||||
|         var result = await days.GetNotableDays(year, regionCode); | ||||
|         return Ok(result); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{regionCode}")] | ||||
|     public async Task<ActionResult<List<NotableDay>>> GetRegionDaysCurrentYear(string regionCode) | ||||
|     { | ||||
|         var currentYear = DateTime.Now.Year; | ||||
|         var result = await days.GetNotableDays(currentYear, regionCode); | ||||
|         return Ok(result); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("me/{year:int}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<NotableDay>>> GetAccountNotableDays(int year) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var region = currentUser.Region; | ||||
|         if (string.IsNullOrWhiteSpace(region)) region = "us"; | ||||
|  | ||||
|         var result = await days.GetNotableDays(year, region); | ||||
|         return Ok(result); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("me")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<NotableDay>>> GetAccountNotableDaysCurrentYear() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var currentYear = DateTime.Now.Year; | ||||
|         var region = currentUser.Region; | ||||
|         if (string.IsNullOrWhiteSpace(region)) region = "us"; | ||||
|  | ||||
|         var result = await days.GetNotableDays(currentYear, region); | ||||
|         return Ok(result); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{regionCode}/next")] | ||||
|     public async Task<ActionResult<NotableDay?>> GetNextHoliday(string regionCode) | ||||
|     { | ||||
|         var result = await days.GetNextHoliday(regionCode); | ||||
|         if (result == null) | ||||
|         { | ||||
|             return NotFound("No upcoming holidays found"); | ||||
|         } | ||||
|         return Ok(result); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("me/next")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<NotableDay?>> GetAccountNextHoliday() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var region = currentUser.Region; | ||||
|         if (string.IsNullOrWhiteSpace(region)) region = "us"; | ||||
|  | ||||
|         var result = await days.GetNextHoliday(region); | ||||
|         if (result == null) | ||||
|         { | ||||
|             return NotFound("No upcoming holidays found"); | ||||
|         } | ||||
|         return Ok(result); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										55
									
								
								DysonNetwork.Pass/Account/NotableDaysService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								DysonNetwork.Pass/Account/NotableDaysService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Nager.Holiday; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class NotableDaysService(ICacheService cache) | ||||
| { | ||||
|     private const string NotableDaysCacheKeyPrefix = "notable:"; | ||||
|  | ||||
|     public async Task<List<NotableDay>> GetNotableDays(int? year, string regionCode) | ||||
|     { | ||||
|         year ??= DateTime.UtcNow.Year; | ||||
|  | ||||
|         // Generate cache key using year and region code | ||||
|         var cacheKey = $"{NotableDaysCacheKeyPrefix}:{year}:{regionCode}"; | ||||
|  | ||||
|         // Try to get from cache first | ||||
|         var (found, cachedDays) = await cache.GetAsyncWithStatus<List<NotableDay>>(cacheKey); | ||||
|         if (found && cachedDays != null) | ||||
|         { | ||||
|             return cachedDays; | ||||
|         } | ||||
|  | ||||
|         // If not in cache, fetch from API | ||||
|         using var holidayClient = new HolidayClient(); | ||||
|         var holidays = await holidayClient.GetHolidaysAsync(year.Value, regionCode); | ||||
|         var days = holidays?.Select(NotableDay.FromNagerHoliday).ToList() ?? []; | ||||
|  | ||||
|         // Cache the result for 1 day (holiday data doesn't change frequently) | ||||
|         await cache.SetAsync(cacheKey, days, TimeSpan.FromDays(1)); | ||||
|  | ||||
|         return days; | ||||
|     } | ||||
|  | ||||
|     public async Task<NotableDay?> GetNextHoliday(string regionCode) | ||||
|     { | ||||
|         var currentDate = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var currentYear = currentDate.InUtc().Year; | ||||
|  | ||||
|         // Get holidays for current year and next year to cover all possibilities | ||||
|         var currentYearHolidays = await GetNotableDays(currentYear, regionCode); | ||||
|         var nextYearHolidays = await GetNotableDays(currentYear + 1, regionCode); | ||||
|  | ||||
|         var allHolidays = currentYearHolidays.Concat(nextYearHolidays); | ||||
|  | ||||
|         // Find the first holiday that is today or in the future | ||||
|         var nextHoliday = allHolidays | ||||
|             .Where(day => day.Date >= currentDate) | ||||
|             .OrderBy(day => day.Date) | ||||
|             .FirstOrDefault(); | ||||
|  | ||||
|         return nextHoliday; | ||||
|     } | ||||
| } | ||||
| @@ -10,7 +10,7 @@ namespace DysonNetwork.Pass.Account; | ||||
| public class RelationshipService( | ||||
|     AppDatabase db, | ||||
|     ICacheService cache, | ||||
|     PusherService.PusherServiceClient pusher, | ||||
|     RingService.RingServiceClient pusher, | ||||
|     IStringLocalizer<NotificationResource> localizer | ||||
| ) | ||||
| { | ||||
|   | ||||
| @@ -69,7 +69,6 @@ public class AppDatabase( | ||||
|                     }) | ||||
|                 ) | ||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) | ||||
|                 .UseNetTopologySuite() | ||||
|                 .UseNodaTime() | ||||
|         ).UseSnakeCaseNamingConvention(); | ||||
|  | ||||
|   | ||||
| @@ -22,7 +22,7 @@ public class AuthController( | ||||
|     AuthService auth, | ||||
|     GeoIpService geo, | ||||
|     ActionLogService als, | ||||
|     PusherService.PusherServiceClient pusher, | ||||
|     RingService.RingServiceClient pusher, | ||||
|     IConfiguration configuration, | ||||
|     IStringLocalizer<NotificationResource> localizer | ||||
| ) : ControllerBase | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System.Security.Cryptography; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -137,6 +138,7 @@ public class AuthService( | ||||
|  | ||||
|         var jsonOpts = new JsonSerializerOptions | ||||
|         { | ||||
|             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||
|             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||
|             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower | ||||
|         }; | ||||
| @@ -211,8 +213,7 @@ public class AuthService( | ||||
|         var session = new AuthSession | ||||
|         { | ||||
|             LastGrantedAt = now, | ||||
|             // Never expire server-side | ||||
|             ExpiredAt = null, | ||||
|             ExpiredAt = now.Plus(Duration.FromDays(7)), | ||||
|             AccountId = challenge.AccountId, | ||||
|             ChallengeId = challenge.Id | ||||
|         }; | ||||
|   | ||||
| @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.GeoIp; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
| using Point = NetTopologySuite.Geometries.Point; | ||||
| @@ -69,7 +70,7 @@ public class AuthChallenge : ModelBase | ||||
|     [MaxLength(128)] public string? IpAddress { get; set; } | ||||
|     [MaxLength(512)] public string? UserAgent { get; set; } | ||||
|     [MaxLength(1024)] public string? Nonce { get; set; } | ||||
|     public Point? Location { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public GeoPoint? Location { get; set; } | ||||
|  | ||||
|     public Guid AccountId { get; set; } | ||||
|     [JsonIgnore] public Account.Account Account { get; set; } = null!; | ||||
|   | ||||
| @@ -84,6 +84,7 @@ public class OidcState | ||||
|     { | ||||
|         return JsonSerializer.Serialize(this, new JsonSerializerOptions | ||||
|         { | ||||
|             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||
|             PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | ||||
|             DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull | ||||
|         }); | ||||
|   | ||||
| @@ -14,7 +14,6 @@ | ||||
|             <PrivateAssets>all</PrivateAssets> | ||||
|         </PackageReference> | ||||
|         <PackageReference Include="Nager.Holiday" Version="1.0.1" /> | ||||
|         <PackageReference Include="NATS.Client.Core" Version="2.6.6" /> | ||||
|         <PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115"> | ||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|             <PrivateAssets>all</PrivateAssets> | ||||
| @@ -25,7 +24,6 @@ | ||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4"/> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4"/> | ||||
|         <PackageReference Include="OpenGraph-Net" Version="4.0.1" /> | ||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/> | ||||
| @@ -51,6 +49,7 @@ | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||
|         <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj"/> | ||||
|     </ItemGroup> | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,10 @@ | ||||
| using dotnet_etcd; | ||||
| using dotnet_etcd.interfaces; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Components; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Email; | ||||
|  | ||||
| public class EmailService( | ||||
|     PusherService.PusherServiceClient pusher, | ||||
|     RingService.RingServiceClient pusher, | ||||
|     RazorViewRenderer viewRenderer, | ||||
|     ILogger<EmailService> logger | ||||
| ) | ||||
|   | ||||
| @@ -42,8 +42,17 @@ public class LastActiveFlushHandler(IServiceProvider srp, ILogger<LastActiveFlus | ||||
|         var updatingSessions = sessionMap.Select(x => x.Key).ToList(); | ||||
|         var sessionUpdates = await db.AuthSessions | ||||
|             .Where(s => updatingSessions.Contains(s.Id)) | ||||
|             .ExecuteUpdateAsync(s => s.SetProperty(x => x.LastGrantedAt, now)); | ||||
|             .ExecuteUpdateAsync(s => | ||||
|                 s.SetProperty(x => x.LastGrantedAt, now) | ||||
|             ); | ||||
|         logger.LogInformation("Updated {Count} auth sessions according to LastActiveInfo", sessionUpdates); | ||||
|         var newExpiration = now.Plus(Duration.FromDays(7)); | ||||
|         var keepAliveSessionUpdates = await db.AuthSessions | ||||
|             .Where(s => updatingSessions.Contains(s.Id) && s.ExpiredAt != null) | ||||
|             .ExecuteUpdateAsync(s => | ||||
|                 s.SetProperty(x => x.ExpiredAt, newExpiration) | ||||
|             ); | ||||
|         logger.LogInformation("Updated {Count} auth sessions' duration according to LastActiveInfo", sessionUpdates); | ||||
|  | ||||
|         var updatingAccounts = accountMap.Select(x => x.Key).ToList(); | ||||
|         var profileUpdates = await db.AccountProfiles | ||||
| @@ -53,7 +62,8 @@ public class LastActiveFlushHandler(IServiceProvider srp, ILogger<LastActiveFlus | ||||
|     } | ||||
| } | ||||
|  | ||||
| public class LastActiveFlushJob(FlushBufferService fbs, LastActiveFlushHandler hdl, ILogger<LastActiveFlushJob> logger) : IJob | ||||
| public class LastActiveFlushJob(FlushBufferService fbs, LastActiveFlushHandler hdl, ILogger<LastActiveFlushJob> logger) | ||||
|     : IJob | ||||
| { | ||||
|     public async Task Execute(IJobExecutionContext context) | ||||
|     { | ||||
| @@ -62,7 +72,8 @@ public class LastActiveFlushJob(FlushBufferService fbs, LastActiveFlushHandler h | ||||
|             logger.LogInformation("Running LastActiveInfo flush job..."); | ||||
|             await fbs.FlushAsync(hdl); | ||||
|             logger.LogInformation("Completed LastActiveInfo flush job..."); | ||||
|         } catch (Exception ex) | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Error running LastActiveInfo job..."); | ||||
|         } | ||||
|   | ||||
							
								
								
									
										2027
									
								
								DysonNetwork.Pass/Migrations/20250907065433_RefactorGeoIpPoint.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2027
									
								
								DysonNetwork.Pass/Migrations/20250907065433_RefactorGeoIpPoint.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,63 @@ | ||||
| using DysonNetwork.Shared.GeoIp; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NetTopologySuite.Geometries; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Pass.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class RefactorGeoIpPoint : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.Sql("UPDATE auth_challenges SET location = NULL;"); | ||||
|             migrationBuilder.Sql("UPDATE action_logs SET location = NULL;"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "location", | ||||
|                 table: "auth_challenges"); | ||||
|  | ||||
|             migrationBuilder.AddColumn<GeoPoint>( | ||||
|                 name: "location", | ||||
|                 table: "auth_challenges", | ||||
|                 type: "jsonb", | ||||
|                 nullable: true); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "location", | ||||
|                 table: "action_logs"); | ||||
|  | ||||
|             migrationBuilder.AddColumn<GeoPoint>( | ||||
|                 name: "location", | ||||
|                 table: "action_logs", | ||||
|                 type: "jsonb", | ||||
|                 nullable: true); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "location", | ||||
|                 table: "auth_challenges"); | ||||
|  | ||||
|             migrationBuilder.AddColumn<Point>( | ||||
|                 name: "location", | ||||
|                 table: "auth_challenges", | ||||
|                 type: "geometry", | ||||
|                 nullable: true); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "location", | ||||
|                 table: "action_logs"); | ||||
|  | ||||
|             migrationBuilder.AddColumn<Point>( | ||||
|                 name: "location", | ||||
|                 table: "action_logs", | ||||
|                 type: "geometry", | ||||
|                 nullable: true); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										2026
									
								
								DysonNetwork.Pass/Migrations/20250907065933_RemoveNetTopo.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2026
									
								
								DysonNetwork.Pass/Migrations/20250907065933_RemoveNetTopo.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										24
									
								
								DysonNetwork.Pass/Migrations/20250907065933_RemoveNetTopo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								DysonNetwork.Pass/Migrations/20250907065933_RemoveNetTopo.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Pass.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class RemoveNetTopo : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AlterDatabase() | ||||
|                 .OldAnnotation("Npgsql:PostgresExtension:postgis", ",,"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AlterDatabase() | ||||
|                 .Annotation("Npgsql:PostgresExtension:postgis", ",,"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										2035
									
								
								DysonNetwork.Pass/Migrations/20250908151924_AddAutomatedStatus.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2035
									
								
								DysonNetwork.Pass/Migrations/20250908151924_AddAutomatedStatus.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,40 @@ | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Pass.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddAutomatedStatus : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AddColumn<string>( | ||||
|                 name: "app_identifier", | ||||
|                 table: "account_statuses", | ||||
|                 type: "character varying(4096)", | ||||
|                 maxLength: 4096, | ||||
|                 nullable: true); | ||||
|  | ||||
|             migrationBuilder.AddColumn<bool>( | ||||
|                 name: "is_automated", | ||||
|                 table: "account_statuses", | ||||
|                 type: "boolean", | ||||
|                 nullable: false, | ||||
|                 defaultValue: false); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "app_identifier", | ||||
|                 table: "account_statuses"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "is_automated", | ||||
|                 table: "account_statuses"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -6,10 +6,10 @@ using DysonNetwork.Pass; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Pass.Wallet; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.GeoIp; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using NetTopologySuite.Geometries; | ||||
| using NodaTime; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
|  | ||||
| @@ -27,7 +27,6 @@ namespace DysonNetwork.Pass.Migrations | ||||
|                 .HasAnnotation("ProductVersion", "9.0.7") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Pass.Account.AbuseReport", b => | ||||
| @@ -525,8 +524,8 @@ namespace DysonNetwork.Pass.Migrations | ||||
|                         .HasColumnType("character varying(128)") | ||||
|                         .HasColumnName("ip_address"); | ||||
|  | ||||
|                     b.Property<Point>("Location") | ||||
|                         .HasColumnType("geometry") | ||||
|                     b.Property<GeoPoint>("Location") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("location"); | ||||
|  | ||||
|                     b.Property<Dictionary<string, object>>("Meta") | ||||
| @@ -768,6 +767,11 @@ namespace DysonNetwork.Pass.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<string>("AppIdentifier") | ||||
|                         .HasMaxLength(4096) | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("app_identifier"); | ||||
|  | ||||
|                     b.Property<int>("Attitude") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("attitude"); | ||||
| @@ -784,6 +788,10 @@ namespace DysonNetwork.Pass.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
|  | ||||
|                     b.Property<bool>("IsAutomated") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_automated"); | ||||
|  | ||||
|                     b.Property<bool>("IsInvisible") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_invisible"); | ||||
| @@ -901,8 +909,8 @@ namespace DysonNetwork.Pass.Migrations | ||||
|                         .HasColumnType("character varying(128)") | ||||
|                         .HasColumnName("ip_address"); | ||||
|  | ||||
|                     b.Property<Point>("Location") | ||||
|                         .HasColumnType("geometry") | ||||
|                     b.Property<GeoPoint>("Location") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("location"); | ||||
|  | ||||
|                     b.Property<string>("Nonce") | ||||
|   | ||||
| @@ -4,25 +4,21 @@ using DysonNetwork.Pass.Startup; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using DysonNetwork.Shared.PageData; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| builder.AddServiceDefaults(); | ||||
|  | ||||
| // Configure Kestrel and server options | ||||
| builder.ConfigureAppKestrel(builder.Configuration); | ||||
|  | ||||
| // Add metrics and telemetry | ||||
| builder.Services.AddAppMetrics(); | ||||
|  | ||||
| // Add application services | ||||
| builder.Services.AddRegistryService(builder.Configuration); | ||||
| builder.Services.AddStreamConnection(builder.Configuration); | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
| builder.Services.AddAppRateLimiting(); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| builder.Services.AddAppSwagger(); | ||||
| builder.Services.AddPusherService(); | ||||
| builder.Services.AddRingService(); | ||||
| builder.Services.AddDriveService(); | ||||
| builder.Services.AddDevelopService(); | ||||
|  | ||||
| @@ -41,6 +37,8 @@ builder.Services.AddTransient<IPageDataProvider, AccountPageData>(); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| app.MapDefaultEndpoints(); | ||||
|  | ||||
| // Run database migrations | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
| @@ -51,8 +49,6 @@ using (var scope = app.Services.CreateScope()) | ||||
| // Configure application middleware pipeline | ||||
| app.ConfigureAppMiddleware(builder.Configuration, builder.Environment.ContentRootPath); | ||||
|  | ||||
| app.MapGatewayProxy(); | ||||
|  | ||||
| app.MapPages(Path.Combine(builder.Environment.WebRootPath, "dist", "index.html")); | ||||
|  | ||||
| // Configure gRPC | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Pass.Wallet; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using NATS.Client.Core; | ||||
| using NATS.Client.JetStream.Models; | ||||
| using NATS.Net; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Startup; | ||||
|  | ||||
| @@ -13,18 +16,30 @@ public class BroadcastEventHandler( | ||||
| { | ||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|     { | ||||
|         await foreach (var msg in nats.SubscribeAsync<byte[]>(PaymentOrderEvent.Type, cancellationToken: stoppingToken)) | ||||
|         var js = nats.CreateJetStreamContext(); | ||||
|  | ||||
|         await js.EnsureStreamCreated("payment_events", [PaymentOrderEventBase.Type]); | ||||
|          | ||||
|         var consumer = await js.CreateOrUpdateConsumerAsync("payment_events",  | ||||
|             new ConsumerConfig("pass_payment_handler"),  | ||||
|             cancellationToken: stoppingToken); | ||||
|          | ||||
|         await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken)) | ||||
|         { | ||||
|             PaymentOrderEvent? evt = null; | ||||
|             try | ||||
|             { | ||||
|                 evt = JsonSerializer.Deserialize<PaymentOrderEvent>(msg.Data); | ||||
|                 evt = JsonSerializer.Deserialize<PaymentOrderEvent>(msg.Data, GrpcTypeHelper.SerializerOptions); | ||||
|                  | ||||
|                 logger.LogInformation( | ||||
|                     "Received order event: {ProductIdentifier} {OrderId}", | ||||
|                     evt?.ProductIdentifier, | ||||
|                     evt?.OrderId | ||||
|                 ); | ||||
|  | ||||
|                 if (evt?.ProductIdentifier is null || | ||||
|                     !evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 logger.LogInformation("Handling stellar program order: {OrderId}", evt.OrderId); | ||||
|  | ||||
| @@ -38,17 +53,20 @@ public class BroadcastEventHandler( | ||||
|                 ); | ||||
|                 if (order is null) | ||||
|                 { | ||||
|                     logger.LogWarning("Order with ID {OrderId} not found.", evt.OrderId); | ||||
|                     logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId); | ||||
|                     await msg.NakAsync(cancellationToken: stoppingToken); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 await subscriptions.HandleSubscriptionOrder(order); | ||||
|  | ||||
|                 logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId); | ||||
|                 await msg.AckAsync(cancellationToken: stoppingToken); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 logger.LogError(ex, "Error processing payment order event for order {OrderId}", evt?.OrderId); | ||||
|                 logger.LogError(ex, "Error processing payment order event for order {OrderId}. Redelivering.", evt?.OrderId); | ||||
|                 await msg.NakAsync(cancellationToken: stoppingToken); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,40 +0,0 @@ | ||||
| using OpenTelemetry.Metrics; | ||||
| using OpenTelemetry.Trace; | ||||
| using Prometheus; | ||||
| using Prometheus.SystemMetrics; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Startup; | ||||
|  | ||||
| public static class MetricsConfiguration | ||||
| { | ||||
|     public static IServiceCollection AddAppMetrics(this IServiceCollection services) | ||||
|     { | ||||
|         // Prometheus | ||||
|         services.UseHttpClientMetrics(); | ||||
|         services.AddHealthChecks(); | ||||
|         services.AddSystemMetrics(); | ||||
|         services.AddPrometheusEntityFrameworkMetrics(); | ||||
|         services.AddPrometheusAspNetCoreMetrics(); | ||||
|         services.AddPrometheusHttpClientMetrics(); | ||||
|  | ||||
|         // OpenTelemetry | ||||
|         services.AddOpenTelemetry() | ||||
|             .WithTracing(tracing => | ||||
|             { | ||||
|                 tracing | ||||
|                     .AddAspNetCoreInstrumentation() | ||||
|                     .AddHttpClientInstrumentation() | ||||
|                     .AddOtlpExporter(); | ||||
|             }) | ||||
|             .WithMetrics(metrics => | ||||
|             { | ||||
|                 metrics | ||||
|                     .AddAspNetCoreInstrumentation() | ||||
|                     .AddHttpClientInstrumentation() | ||||
|                     .AddRuntimeInstrumentation() | ||||
|                     .AddOtlpExporter(); | ||||
|             }); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
| @@ -12,6 +12,7 @@ using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
| using StackExchange.Redis; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using System.Threading.RateLimiting; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Options; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Services; | ||||
| @@ -33,11 +34,6 @@ public static class ServiceCollectionExtensions | ||||
|         services.AddLocalization(options => options.ResourcesPath = "Resources"); | ||||
|  | ||||
|         services.AddDbContext<AppDatabase>(); | ||||
|         services.AddSingleton<IConnectionMultiplexer>(_ => | ||||
|         { | ||||
|             var connection = configuration.GetConnectionString("FastRetrieve")!; | ||||
|             return ConnectionMultiplexer.Connect(connection); | ||||
|         }); | ||||
|         services.AddSingleton<IClock>(SystemClock.Instance); | ||||
|         services.AddHttpContextAccessor(); | ||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); | ||||
| @@ -52,7 +48,7 @@ public static class ServiceCollectionExtensions | ||||
|             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB | ||||
|         }); | ||||
|  | ||||
|         services.AddPusherService(); | ||||
|         services.AddRingService(); | ||||
|  | ||||
|         // Register OIDC services | ||||
|         services.AddScoped<OidcService, GoogleOidcService>(); | ||||
| @@ -70,6 +66,7 @@ public static class ServiceCollectionExtensions | ||||
|  | ||||
|         services.AddControllers().AddJsonOptions(options => | ||||
|         { | ||||
|             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|  | ||||
| @@ -132,7 +129,8 @@ public static class ServiceCollectionExtensions | ||||
|             { | ||||
|                 Version = "v1", | ||||
|                 Title = "Dyson Pass", | ||||
|                 Description = "The authentication service of the Dyson Network. Mainly handling authentication and authorization.", | ||||
|                 Description = | ||||
|                     "The authentication service of the Dyson Network. Mainly handling authentication and authorization.", | ||||
|                 TermsOfService = new Uri("https://solsynth.dev/terms"), | ||||
|                 License = new OpenApiLicense | ||||
|                 { | ||||
| @@ -190,6 +188,7 @@ public static class ServiceCollectionExtensions | ||||
|         services.AddScoped<ActionLogService>(); | ||||
|         services.AddScoped<AccountService>(); | ||||
|         services.AddScoped<AccountEventService>(); | ||||
|         services.AddScoped<NotableDaysService>(); | ||||
|         services.AddScoped<ActionLogService>(); | ||||
|         services.AddScoped<RelationshipService>(); | ||||
|         services.AddScoped<MagicSpellService>(); | ||||
| @@ -207,6 +206,8 @@ public static class ServiceCollectionExtensions | ||||
|         services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider")); | ||||
|         services.AddScoped<OidcProviderService>(); | ||||
|          | ||||
|         services.AddHostedService<BroadcastEventHandler>(); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
| @@ -24,8 +24,7 @@ public class OrderController(PaymentService payment, AuthService auth, AppDataba | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Order>> PayOrder(Guid id, [FromBody] PayOrderRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|      | ||||
|         // Validate PIN code | ||||
|         if (!await auth.ValidatePinCode(currentUser.Id, request.PinCode)) | ||||
|   | ||||
| @@ -72,8 +72,8 @@ public class Order : ModelBase | ||||
|             : null, | ||||
|         Amount = decimal.Parse(proto.Amount), | ||||
|         ExpiredAt = proto.ExpiredAt.ToInstant(), | ||||
|         PayeeWalletId = proto.HasPayeeWalletId ? Guid.Parse(proto.PayeeWalletId) : null, | ||||
|         TransactionId = proto.HasTransactionId ? Guid.Parse(proto.TransactionId) : null, | ||||
|         PayeeWalletId = proto.PayeeWalletId is not null ? Guid.Parse(proto.PayeeWalletId) : null, | ||||
|         TransactionId = proto.TransactionId is not null ? Guid.Parse(proto.TransactionId) : null, | ||||
|         Transaction = proto.Transaction is not null ? Transaction.FromProtoValue(proto.Transaction) : null, | ||||
|     }; | ||||
| } | ||||
| @@ -106,7 +106,7 @@ public class Transaction : ModelBase | ||||
|     { | ||||
|         Id = Id.ToString(), | ||||
|         Currency = Currency, | ||||
|         Amount = Amount.ToString(), | ||||
|         Amount = Amount.ToString(CultureInfo.InvariantCulture), | ||||
|         Remarks = Remarks, | ||||
|         Type = (Shared.Proto.TransactionType)Type, | ||||
|         PayerWalletId = PayerWalletId?.ToString(), | ||||
| @@ -120,7 +120,7 @@ public class Transaction : ModelBase | ||||
|         Amount = decimal.Parse(proto.Amount), | ||||
|         Remarks = proto.Remarks, | ||||
|         Type = (TransactionType)proto.Type, | ||||
|         PayerWalletId = proto.HasPayerWalletId ? Guid.Parse(proto.PayerWalletId) : null, | ||||
|         PayeeWalletId = proto.HasPayeeWalletId ? Guid.Parse(proto.PayeeWalletId) : null, | ||||
|         PayerWalletId = proto.PayerWalletId is not null ? Guid.Parse(proto.PayerWalletId) : null, | ||||
|         PayeeWalletId = proto.PayeeWalletId is not null ? Guid.Parse(proto.PayeeWalletId) : null, | ||||
|     }; | ||||
| } | ||||
| @@ -7,6 +7,8 @@ using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Storage; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NATS.Client.Core; | ||||
| using NATS.Client.JetStream; | ||||
| using NATS.Net; | ||||
| using NodaTime; | ||||
| using AccountService = DysonNetwork.Pass.Account.AccountService; | ||||
|  | ||||
| @@ -15,7 +17,7 @@ namespace DysonNetwork.Pass.Wallet; | ||||
| public class PaymentService( | ||||
|     AppDatabase db, | ||||
|     WalletService wat, | ||||
|     PusherService.PusherServiceClient pusher, | ||||
|     RingService.RingServiceClient pusher, | ||||
|     IStringLocalizer<NotificationResource> localizer, | ||||
|     INatsConnection nats | ||||
| ) | ||||
| @@ -192,10 +194,12 @@ public class PaymentService( | ||||
|                     Notification = new PushNotification | ||||
|                     { | ||||
|                         Topic = "wallets.transactions", | ||||
|                         Title = transaction.Amount > 0 | ||||
|                             ? localizer["TransactionNewBodyMinus", readableTransactionRemark] | ||||
|                             : localizer["TransactionNewBodyPlus", readableTransactionRemark], | ||||
|                         Body = localizer["TransactionNewTitle", | ||||
|                         Title = localizer["TransactionNewTitle", readableTransactionRemark], | ||||
|                         Body = transaction.Amount > 0 | ||||
|                             ? localizer["TransactionNewBodyMinus", | ||||
|                                 transaction.Amount.ToString(CultureInfo.InvariantCulture), | ||||
|                                 transaction.Currency] | ||||
|                             : localizer["TransactionNewBodyPlus", | ||||
|                                 transaction.Amount.ToString(CultureInfo.InvariantCulture), | ||||
|                                 transaction.Currency], | ||||
|                         IsSavable = true | ||||
| @@ -224,10 +228,12 @@ public class PaymentService( | ||||
|                     Notification = new PushNotification | ||||
|                     { | ||||
|                         Topic = "wallets.transactions", | ||||
|                         Title = transaction.Amount > 0 | ||||
|                             ? localizer["TransactionNewBodyPlus", readableTransactionRemark] | ||||
|                             : localizer["TransactionNewBodyMinus", readableTransactionRemark], | ||||
|                         Body = localizer["TransactionNewTitle", | ||||
|                         Title = localizer["TransactionNewTitle", readableTransactionRemark], | ||||
|                         Body = transaction.Amount > 0 | ||||
|                             ? localizer["TransactionNewBodyPlus", | ||||
|                                 transaction.Amount.ToString(CultureInfo.InvariantCulture), | ||||
|                                 transaction.Currency] | ||||
|                             : localizer["TransactionNewBodyMinus", | ||||
|                                 transaction.Amount.ToString(CultureInfo.InvariantCulture), | ||||
|                                 transaction.Currency], | ||||
|                         IsSavable = true | ||||
| @@ -249,6 +255,27 @@ public class PaymentService( | ||||
|             throw new InvalidOperationException("Order not found"); | ||||
|         } | ||||
|  | ||||
|         var js = nats.CreateJetStreamContext(); | ||||
|  | ||||
|         if (order.Status == OrderStatus.Paid) | ||||
|         { | ||||
|             await js.PublishAsync( | ||||
|                 PaymentOrderEventBase.Type, | ||||
|                 GrpcTypeHelper.ConvertObjectToByteString(new PaymentOrderEvent | ||||
|                 { | ||||
|                     OrderId = order.Id, | ||||
|                     WalletId = payerWallet.Id, | ||||
|                     AccountId = payerWallet.AccountId, | ||||
|                     AppIdentifier = order.AppIdentifier, | ||||
|                     ProductIdentifier = order.ProductIdentifier, | ||||
|                     Meta = order.Meta ?? [], | ||||
|                     Status = (int)order.Status, | ||||
|                 }).ToByteArray() | ||||
|             ); | ||||
|  | ||||
|             return order; | ||||
|         } | ||||
|  | ||||
|         if (order.Status != OrderStatus.Unpaid) | ||||
|         { | ||||
|             throw new InvalidOperationException($"Order is in invalid status: {order.Status}"); | ||||
| @@ -267,7 +294,8 @@ public class PaymentService( | ||||
|             order.Currency, | ||||
|             order.Amount, | ||||
|             order.Remarks ?? $"Payment for Order #{order.Id}", | ||||
|             type: TransactionType.Order); | ||||
|             type: TransactionType.Order, | ||||
|             silent: true); | ||||
|  | ||||
|         order.TransactionId = transaction.Id; | ||||
|         order.Transaction = transaction; | ||||
| @@ -277,7 +305,9 @@ public class PaymentService( | ||||
|  | ||||
|         await NotifyOrderPaid(order, payerWallet, order.PayeeWallet); | ||||
|  | ||||
|         await nats.PublishAsync(PaymentOrderEvent.Type, JsonSerializer.SerializeToUtf8Bytes(new PaymentOrderEvent | ||||
|         await js.PublishAsync( | ||||
|             PaymentOrderEventBase.Type, | ||||
|             GrpcTypeHelper.ConvertObjectToByteString(new PaymentOrderEvent | ||||
|             { | ||||
|                 OrderId = order.Id, | ||||
|                 WalletId = payerWallet.Id, | ||||
| @@ -286,7 +316,8 @@ public class PaymentService( | ||||
|                 ProductIdentifier = order.ProductIdentifier, | ||||
|                 Meta = order.Meta ?? [], | ||||
|                 Status = (int)order.Status, | ||||
|         })); | ||||
|             }).ToByteArray() | ||||
|         ); | ||||
|  | ||||
|         return order; | ||||
|     } | ||||
|   | ||||
| @@ -68,7 +68,8 @@ public class SubscriptionRenewalJob( | ||||
|                     null, | ||||
|                     WalletCurrency.GoldenPoint, | ||||
|                     subscription.FinalPrice, | ||||
|                     appIdentifier: SubscriptionService.SubscriptionOrderIdentifier, | ||||
|                     appIdentifier: "internal", | ||||
|                     productIdentifier: subscription.Identifier, | ||||
|                     meta: new Dictionary<string, object>() | ||||
|                     { | ||||
|                         ["subscription_id"] = subscription.Id.ToString(), | ||||
|   | ||||
| @@ -18,7 +18,7 @@ public class SubscriptionService( | ||||
|     AppDatabase db, | ||||
|     PaymentService payment, | ||||
|     AccountService accounts, | ||||
|     PusherService.PusherServiceClient pusher, | ||||
|     RingService.RingServiceClient pusher, | ||||
|     IStringLocalizer<NotificationResource> localizer, | ||||
|     IConfiguration configuration, | ||||
|     ICacheService cache, | ||||
| @@ -229,8 +229,6 @@ public class SubscriptionService( | ||||
|         return subscription; | ||||
|     } | ||||
|  | ||||
|     public const string SubscriptionOrderIdentifier = "solian.subscription.order"; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Creates a subscription order for an unpaid or expired subscription. | ||||
|     /// If the subscription is active, it will extend its expiration date. | ||||
| @@ -259,7 +257,8 @@ public class SubscriptionService( | ||||
|             null, | ||||
|             subscriptionInfo.Currency, | ||||
|             subscription.FinalPrice, | ||||
|             appIdentifier: SubscriptionOrderIdentifier, | ||||
|             appIdentifier: "internal", | ||||
|             productIdentifier: identifier, | ||||
|             meta: new Dictionary<string, object>() | ||||
|             { | ||||
|                 ["subscription_id"] = subscription.Id.ToString(), | ||||
| @@ -270,8 +269,7 @@ public class SubscriptionService( | ||||
|  | ||||
|     public async Task<Subscription> HandleSubscriptionOrder(Order order) | ||||
|     { | ||||
|         if (order.AppIdentifier != SubscriptionOrderIdentifier || order.Status != OrderStatus.Paid || | ||||
|             order.Meta?["subscription_id"] is not JsonElement subscriptionIdJson) | ||||
|         if (order.Status != OrderStatus.Paid || order.Meta?["subscription_id"] is not JsonElement subscriptionIdJson) | ||||
|             throw new InvalidOperationException("Invalid order."); | ||||
|  | ||||
|         var subscriptionId = Guid.TryParse(subscriptionIdJson.ToString(), out var parsedSubscriptionId) | ||||
|   | ||||
| @@ -46,22 +46,51 @@ public class WalletController(AppDatabase db, WalletService ws, PaymentService p | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var query = db.PaymentTransactions.AsQueryable() | ||||
|             .Include(t => t.PayeeWallet) | ||||
|             .Include(t => t.PayerWallet) | ||||
|             .Where(t => (t.PayeeWallet != null && t.PayeeWallet.AccountId == currentUser.Id) || | ||||
|                         (t.PayerWallet != null && t.PayerWallet.AccountId == currentUser.Id)); | ||||
|         var accountWallet = await db.Wallets.Where(w => w.AccountId == currentUser.Id).FirstOrDefaultAsync(); | ||||
|         if (accountWallet is null) return NotFound(); | ||||
|  | ||||
|         var query = db.PaymentTransactions | ||||
|             .Where(t => t.PayeeWalletId == accountWallet.Id || t.PayerWalletId == accountWallet.Id) | ||||
|             .OrderByDescending(t => t.CreatedAt) | ||||
|             .AsQueryable(); | ||||
|  | ||||
|         var transactionCount = await query.CountAsync(); | ||||
|         Response.Headers["X-Total"] = transactionCount.ToString(); | ||||
|          | ||||
|         var transactions = await query | ||||
|             .Skip(offset) | ||||
|             .Take(take) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         return Ok(transactions); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("orders")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<Order>>> GetOrders( | ||||
|         [FromQuery] int offset = 0, [FromQuery] int take = 20 | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|          | ||||
|         var accountWallet = await db.Wallets.Where(w => w.AccountId == currentUser.Id).FirstOrDefaultAsync(); | ||||
|         if (accountWallet is null) return NotFound(); | ||||
|          | ||||
|         var query = db.PaymentOrders.AsQueryable() | ||||
|             .Include(o => o.Transaction) | ||||
|             .Where(o => o.Transaction != null && (o.Transaction.PayeeWalletId == accountWallet.Id || o.Transaction.PayerWalletId == accountWallet.Id)) | ||||
|             .AsQueryable(); | ||||
|          | ||||
|         var orderCount = await query.CountAsync(); | ||||
|         Response.Headers["X-Total"] = orderCount.ToString(); | ||||
|          | ||||
|         var orders = await query | ||||
|             .Skip(offset) | ||||
|             .Take(take) | ||||
|             .OrderByDescending(t => t.CreatedAt) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         Response.Headers["X-Total"] = transactionCount.ToString(); | ||||
|  | ||||
|         return Ok(transactions); | ||||
|         return Ok(orders); | ||||
|     } | ||||
|  | ||||
|     public class WalletBalanceRequest | ||||
|   | ||||
| @@ -10,10 +10,7 @@ | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_pass;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60", | ||||
|     "FastRetrieve": "localhost:6379", | ||||
|     "Etcd": "etcd.orb.local:2379", | ||||
|     "Stream": "nats.orb.local:4222" | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_pass;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" | ||||
|   }, | ||||
|   "Authentication": { | ||||
|     "Schemes": { | ||||
| @@ -83,9 +80,7 @@ | ||||
|   ], | ||||
|   "Service": { | ||||
|     "Name": "DysonNetwork.Pass", | ||||
|     "Url": "https://localhost:7058", | ||||
|     "ClientCert": "../Certificates/client.crt", | ||||
|     "ClientKey": "../Certificates/client.key" | ||||
|     "Url": "https://localhost:7058" | ||||
|   }, | ||||
|   "Etcd": { | ||||
|     "Insecure": true | ||||
|   | ||||
| @@ -1,27 +0,0 @@ | ||||
| FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base | ||||
| WORKDIR /app | ||||
| EXPOSE 8080 | ||||
| EXPOSE 8081 | ||||
|  | ||||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||||
|     libkrb5-dev | ||||
|  | ||||
| USER $APP_UID | ||||
|  | ||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| WORKDIR /src | ||||
| COPY ["DysonNetwork.Pusher/DysonNetwork.Pusher.csproj", "DysonNetwork.Pusher/"] | ||||
| RUN dotnet restore "DysonNetwork.Pusher/DysonNetwork.Pusher.csproj" | ||||
| COPY . . | ||||
| WORKDIR "/src/DysonNetwork.Pusher" | ||||
| RUN dotnet build "./DysonNetwork.Pusher.csproj" -c $BUILD_CONFIGURATION -o /app/build | ||||
|  | ||||
| FROM build AS publish | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| RUN dotnet publish "./DysonNetwork.Pusher.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false | ||||
|  | ||||
| FROM base AS final | ||||
| WORKDIR /app | ||||
| COPY --from=publish /app/publish . | ||||
| ENTRYPOINT ["dotnet", "DysonNetwork.Pusher.dll"] | ||||
| @@ -1,6 +1,6 @@ | ||||
| using System.Linq.Expressions; | ||||
| using System.Reflection; | ||||
| using DysonNetwork.Pusher.Notification; | ||||
| using DysonNetwork.Ring.Notification; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Query; | ||||
| using NodaTime; | ||||
| using Quartz; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher; | ||||
| namespace DysonNetwork.Ring; | ||||
| 
 | ||||
| public class AppDatabase( | ||||
|     DbContextOptions<AppDatabase> options, | ||||
| @@ -1,4 +1,4 @@ | ||||
| namespace DysonNetwork.Pusher.Connection; | ||||
| namespace DysonNetwork.Ring.Connection; | ||||
| 
 | ||||
| public class ClientTypeMiddleware(RequestDelegate next) | ||||
| { | ||||
| @@ -1,7 +1,7 @@ | ||||
| using System.Net.WebSockets; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Connection; | ||||
| namespace DysonNetwork.Ring.Connection; | ||||
| 
 | ||||
| public interface IWebSocketPacketHandler | ||||
| { | ||||
| @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Swashbuckle.AspNetCore.Annotations; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Connection; | ||||
| namespace DysonNetwork.Ring.Connection; | ||||
| 
 | ||||
| [ApiController] | ||||
| public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext> logger) : ControllerBase | ||||
| @@ -4,7 +4,7 @@ using DysonNetwork.Shared.Proto; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Connection; | ||||
| namespace DysonNetwork.Ring.Connection; | ||||
| 
 | ||||
| public class WebSocketPacket | ||||
| { | ||||
| @@ -29,6 +29,7 @@ public class WebSocketPacket | ||||
|         var json = System.Text.Encoding.UTF8.GetString(bytes); | ||||
|         var jsonOpts = new JsonSerializerOptions | ||||
|         { | ||||
|             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||
|             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||
|             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||
|         }; | ||||
| @@ -48,6 +49,7 @@ public class WebSocketPacket | ||||
| 
 | ||||
|         var jsonOpts = new JsonSerializerOptions | ||||
|         { | ||||
|             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||
|             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||
|             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||
|         }; | ||||
| @@ -65,6 +67,7 @@ public class WebSocketPacket | ||||
|     { | ||||
|         var jsonOpts = new JsonSerializerOptions | ||||
|         { | ||||
|             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||
|             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||
|             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||
|         }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||
| @@ -1,27 +1,23 @@ | ||||
| using System.Collections.Concurrent; | ||||
| using System.Net.WebSockets; | ||||
| using dotnet_etcd.interfaces; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Connection; | ||||
| namespace DysonNetwork.Ring.Connection; | ||||
| 
 | ||||
| public class WebSocketService | ||||
| { | ||||
|     private readonly IConfiguration _configuration; | ||||
|     private readonly ILogger<WebSocketService> _logger; | ||||
|     private readonly IEtcdClient _etcdClient; | ||||
|     private readonly IDictionary<string, IWebSocketPacketHandler> _handlerMap; | ||||
| 
 | ||||
|     public WebSocketService( | ||||
|         IEnumerable<IWebSocketPacketHandler> handlers, | ||||
|         IEtcdClient etcdClient, | ||||
|         ILogger<WebSocketService> logger, | ||||
|         IConfiguration configuration | ||||
|     ) | ||||
|     { | ||||
|         _etcdClient = etcdClient; | ||||
|         _logger = logger; | ||||
|         _configuration = configuration; | ||||
|         _handlerMap = handlers.ToDictionary(h => h.PacketType); | ||||
| @@ -59,8 +55,10 @@ public class WebSocketService | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             _logger.LogWarning(ex, "Error while closing WebSocket for {AccountId}:{DeviceId}", key.AccountId, key.DeviceId); | ||||
|             _logger.LogWarning(ex, "Error while closing WebSocket for {AccountId}:{DeviceId}", key.AccountId, | ||||
|                 key.DeviceId); | ||||
|         } | ||||
| 
 | ||||
|         data.Cts.Cancel(); | ||||
|         ActiveConnections.TryRemove(key, out _); | ||||
|     } | ||||
| @@ -140,26 +138,10 @@ public class WebSocketService | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 // Get the service URL from etcd for the specified endpoint | ||||
|                 var serviceKey = $"/services/{packet.Endpoint}"; | ||||
|                 var response = await _etcdClient.GetAsync(serviceKey); | ||||
|                 var serviceUrl = "https://" + packet.Endpoint; | ||||
| 
 | ||||
|                 if (response.Kvs.Count > 0) | ||||
|                 { | ||||
|                     var serviceUrl = response.Kvs[0].Value.ToStringUtf8(); | ||||
| 
 | ||||
|                     var clientCertPath = _configuration["Service:ClientCert"]!; | ||||
|                     var clientKeyPath = _configuration["Service:ClientKey"]!; | ||||
|                     var clientCertPassword = _configuration["Service:CertPassword"]; | ||||
| 
 | ||||
|                     var callInvoker = | ||||
|                         GrpcClientHelper.CreateCallInvoker( | ||||
|                             serviceUrl, | ||||
|                             clientCertPath, | ||||
|                             clientKeyPath, | ||||
|                             clientCertPassword | ||||
|                         ); | ||||
|                     var client = new PusherHandlerService.PusherHandlerServiceClient(callInvoker); | ||||
|                 var callInvoker = GrpcClientHelper.CreateCallInvoker(serviceUrl); | ||||
|                 var client = new RingHandlerService.RingHandlerServiceClient(callInvoker); | ||||
| 
 | ||||
|                 try | ||||
|                 { | ||||
| @@ -174,11 +156,6 @@ public class WebSocketService | ||||
|                 { | ||||
|                     _logger.LogError(ex, $"Error forwarding packet to endpoint: {packet.Endpoint}"); | ||||
|                 } | ||||
| 
 | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 _logger.LogWarning($"No service registered for endpoint: {packet.Endpoint}"); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
							
								
								
									
										27
									
								
								DysonNetwork.Ring/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								DysonNetwork.Ring/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base | ||||
| WORKDIR /app | ||||
| EXPOSE 8080 | ||||
| EXPOSE 8081 | ||||
|  | ||||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||||
|     libkrb5-dev | ||||
|  | ||||
| USER $APP_UID | ||||
|  | ||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| WORKDIR /src | ||||
| COPY ["DysonNetwork.Ring/DysonNetwork.Ring.csproj", "DysonNetwork.Ring/"] | ||||
| RUN dotnet restore "DysonNetwork.Ring/DysonNetwork.Ring.csproj" | ||||
| COPY . . | ||||
| WORKDIR "/src/DysonNetwork.Ring" | ||||
| RUN dotnet build "./DysonNetwork.Ring.csproj" -c $BUILD_CONFIGURATION -o /app/build | ||||
|  | ||||
| FROM build AS publish | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| RUN dotnet publish "./DysonNetwork.Ring.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false | ||||
|  | ||||
| FROM base AS final | ||||
| WORKDIR /app | ||||
| COPY --from=publish /app/publish . | ||||
| ENTRYPOINT ["dotnet", "DysonNetwork.Ring.dll"] | ||||
| @@ -5,6 +5,7 @@ | ||||
|         <Nullable>enable</Nullable> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> | ||||
|         <RootNamespace>DysonNetwork.Pusher</RootNamespace> | ||||
|     </PropertyGroup> | ||||
| 
 | ||||
|     <ItemGroup> | ||||
| @@ -41,6 +42,7 @@ | ||||
|     </ItemGroup> | ||||
| 
 | ||||
|     <ItemGroup> | ||||
|       <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||
|     </ItemGroup> | ||||
| 
 | ||||
| @@ -1,7 +1,7 @@ | ||||
| using MailKit.Net.Smtp; | ||||
| using MimeKit; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Email; | ||||
| namespace DysonNetwork.Ring.Email; | ||||
| 
 | ||||
| public class EmailServiceConfiguration | ||||
| { | ||||
| @@ -1,7 +1,7 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Pusher; | ||||
| using DysonNetwork.Ring; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| 
 | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Migrations | ||||
| namespace DysonNetwork.Ring.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250713122638_InitialMigration")] | ||||
| @@ -27,7 +27,7 @@ namespace DysonNetwork.Pusher.Migrations | ||||
| 
 | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
| 
 | ||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.Notification", b => | ||||
|             modelBuilder.Entity("DysonNetwork.Ring.Notification.Notification", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
| @@ -89,7 +89,7 @@ namespace DysonNetwork.Pusher.Migrations | ||||
|                     b.ToTable("notifications", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.PushSubscription", b => | ||||
|             modelBuilder.Entity("DysonNetwork.Ring.Notification.PushSubscription", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
| @@ -5,7 +5,7 @@ using NodaTime; | ||||
| 
 | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Migrations | ||||
| namespace DysonNetwork.Ring.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class InitialMigration : Migration | ||||
| @@ -1,7 +1,7 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Pusher; | ||||
| using DysonNetwork.Ring; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| 
 | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Migrations | ||||
| namespace DysonNetwork.Ring.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250724070546_UpdateNotificationMeta")] | ||||
| @@ -27,7 +27,7 @@ namespace DysonNetwork.Pusher.Migrations | ||||
| 
 | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
| 
 | ||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.Notification", b => | ||||
|             modelBuilder.Entity("DysonNetwork.Ring.Notification.Notification", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
| @@ -90,7 +90,7 @@ namespace DysonNetwork.Pusher.Migrations | ||||
|                     b.ToTable("notifications", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.PushSubscription", b => | ||||
|             modelBuilder.Entity("DysonNetwork.Ring.Notification.PushSubscription", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
| @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; | ||||
| 
 | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Migrations | ||||
| namespace DysonNetwork.Ring.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class UpdateNotificationMeta : Migration | ||||
| @@ -1,7 +1,7 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Pusher; | ||||
| using DysonNetwork.Ring; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| @@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| 
 | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Migrations | ||||
| namespace DysonNetwork.Ring.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     partial class AppDatabaseModelSnapshot : ModelSnapshot | ||||
| @@ -24,7 +24,7 @@ namespace DysonNetwork.Pusher.Migrations | ||||
| 
 | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
| 
 | ||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.Notification", b => | ||||
|             modelBuilder.Entity("DysonNetwork.Ring.Notification.Notification", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
| @@ -87,7 +87,7 @@ namespace DysonNetwork.Pusher.Migrations | ||||
|                     b.ToTable("notifications", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.PushSubscription", b => | ||||
|             modelBuilder.Entity("DysonNetwork.Ring.Notification.PushSubscription", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
| @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Notification; | ||||
| namespace DysonNetwork.Ring.Notification; | ||||
| 
 | ||||
| public class Notification : ModelBase | ||||
| { | ||||
| @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Notification; | ||||
| namespace DysonNetwork.Ring.Notification; | ||||
| 
 | ||||
| [ApiController] | ||||
| [Route("/api/notifications")] | ||||
| @@ -3,7 +3,7 @@ using EFCore.BulkExtensions; | ||||
| using NodaTime; | ||||
| using Quartz; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Notification; | ||||
| namespace DysonNetwork.Ring.Notification; | ||||
| 
 | ||||
| public class NotificationFlushHandler(AppDatabase db) : IFlushHandler<Notification> | ||||
| { | ||||
| @@ -1,13 +1,13 @@ | ||||
| using CorePush.Apple; | ||||
| using CorePush.Firebase; | ||||
| using DysonNetwork.Pusher.Connection; | ||||
| using DysonNetwork.Pusher.Services; | ||||
| using DysonNetwork.Ring.Connection; | ||||
| using DysonNetwork.Ring.Services; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using WebSocketPacket = DysonNetwork.Pusher.Connection.WebSocketPacket; | ||||
| using WebSocketPacket = DysonNetwork.Ring.Connection.WebSocketPacket; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Notification; | ||||
| namespace DysonNetwork.Ring.Notification; | ||||
| 
 | ||||
| public class PushService | ||||
| { | ||||
| @@ -3,7 +3,7 @@ using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Notification; | ||||
| namespace DysonNetwork.Ring.Notification; | ||||
| 
 | ||||
| public enum PushProvider | ||||
| { | ||||
| @@ -1,19 +1,18 @@ | ||||
| using DysonNetwork.Pusher; | ||||
| using DysonNetwork.Pusher.Startup; | ||||
| using DysonNetwork.Ring; | ||||
| using DysonNetwork.Ring.Startup; | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| 
 | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
| 
 | ||||
| builder.AddServiceDefaults(); | ||||
| 
 | ||||
| // Configure Kestrel and server options | ||||
| builder.ConfigureAppKestrel(builder.Configuration); | ||||
| 
 | ||||
| // Add application services | ||||
| builder.Services.AddRegistryService(builder.Configuration); | ||||
| builder.Services.AddStreamConnection(builder.Configuration); | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
| builder.Services.AddAppRateLimiting(); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| @@ -32,6 +31,8 @@ builder.Services.AddAppScheduledJobs(); | ||||
| 
 | ||||
| var app = builder.Build(); | ||||
| 
 | ||||
| app.MapDefaultEndpoints(); | ||||
| 
 | ||||
| // Run database migrations | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
| @@ -1,19 +1,18 @@ | ||||
| using DysonNetwork.Pusher.Connection; | ||||
| using DysonNetwork.Pusher.Email; | ||||
| using DysonNetwork.Pusher.Notification; | ||||
| using DysonNetwork.Ring.Connection; | ||||
| using DysonNetwork.Ring.Email; | ||||
| using DysonNetwork.Ring.Notification; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Google.Protobuf.WellKnownTypes; | ||||
| using Grpc.Core; | ||||
| using System.Text.Json; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Services; | ||||
| namespace DysonNetwork.Ring.Services; | ||||
| 
 | ||||
| public class PusherServiceGrpc( | ||||
| public class RingServiceGrpc( | ||||
|     QueueService queueService, | ||||
|     WebSocketService websocket, | ||||
|     PushService pushService | ||||
| ) : PusherService.PusherServiceBase | ||||
| ) : RingService.RingServiceBase | ||||
| { | ||||
|     public override async Task<Empty> SendEmail(SendEmailRequest request, ServerCallContext context) | ||||
|     { | ||||
| @@ -1,12 +1,15 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Pusher.Email; | ||||
| using DysonNetwork.Pusher.Notification; | ||||
| using DysonNetwork.Ring.Email; | ||||
| using DysonNetwork.Ring.Notification; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using Google.Protobuf; | ||||
| using NATS.Client.Core; | ||||
| using NATS.Client.JetStream; | ||||
| using NATS.Client.JetStream.Models; | ||||
| using NATS.Net; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Services; | ||||
| namespace DysonNetwork.Ring.Services; | ||||
| 
 | ||||
| public class QueueBackgroundService( | ||||
|     INatsConnection nats, | ||||
| @@ -16,8 +19,8 @@ public class QueueBackgroundService( | ||||
| ) | ||||
|     : BackgroundService | ||||
| { | ||||
|     public const string QueueName = "pusher.queue"; | ||||
|     public const string QueueGroup = "pusher.workers"; | ||||
|     public const string QueueName = "pusher_queue"; | ||||
|     private const string QueueGroup = "pusher_workers"; | ||||
|     private readonly int _consumerCount = configuration.GetValue<int?>("ConsumerCount") ?? Environment.ProcessorCount; | ||||
|     private readonly List<Task> _consumerTasks = []; | ||||
| 
 | ||||
| @@ -36,11 +39,16 @@ public class QueueBackgroundService( | ||||
|     private async Task RunConsumerAsync(CancellationToken stoppingToken) | ||||
|     { | ||||
|         logger.LogInformation("Queue consumer started"); | ||||
|         var js = nats.CreateJetStreamContext(); | ||||
| 
 | ||||
|         await foreach (var msg in nats.SubscribeAsync<byte[]>( | ||||
|                            QueueName, | ||||
|                            queueGroup: QueueGroup, | ||||
|                            cancellationToken: stoppingToken)) | ||||
|         await js.EnsureStreamCreated("pusher_events", [QueueName]); | ||||
|          | ||||
|         var consumer = await js.CreateOrUpdateConsumerAsync( | ||||
|             "pusher_events",  | ||||
|             new ConsumerConfig(QueueGroup), // durable consumer | ||||
|             cancellationToken: stoppingToken); | ||||
| 
 | ||||
|         await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken)) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
| @@ -48,10 +56,12 @@ public class QueueBackgroundService( | ||||
|                 if (message is not null) | ||||
|                 { | ||||
|                     await ProcessMessageAsync(msg, message, stoppingToken); | ||||
|                     await msg.AckAsync(cancellationToken: stoppingToken); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     logger.LogWarning($"Invalid message format for {msg.Subject}"); | ||||
|                     await msg.AckAsync(cancellationToken: stoppingToken); // Acknowledge invalid messages to avoid redelivery | ||||
|                 } | ||||
|             } | ||||
|             catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) | ||||
| @@ -62,21 +72,18 @@ public class QueueBackgroundService( | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 logger.LogError(ex, "Error in queue consumer"); | ||||
|                 // Add a small delay to prevent tight error loops | ||||
|                 await Task.Delay(1000, stoppingToken); | ||||
|                 await msg.NakAsync(cancellationToken: stoppingToken); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private async ValueTask ProcessMessageAsync(NatsMsg<byte[]> rawMsg, QueueMessage message, | ||||
|     private async ValueTask ProcessMessageAsync(NatsJSMsg<byte[]> rawMsg, QueueMessage message, | ||||
|         CancellationToken cancellationToken) | ||||
|     { | ||||
|         using var scope = serviceProvider.CreateScope(); | ||||
| 
 | ||||
|         logger.LogDebug("Processing message of type {MessageType}", message.Type); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|         switch (message.Type) | ||||
|         { | ||||
|             case QueueMessageType.Email: | ||||
| @@ -92,13 +99,6 @@ public class QueueBackgroundService( | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Error processing message of type {MessageType}", message.Type); | ||||
|             // Don't rethrow to prevent the message from being retried indefinitely | ||||
|             // In a production scenario, you might want to implement a dead-letter queue | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static async Task ProcessEmailMessageAsync(QueueMessage message, IServiceScope scope) | ||||
|     { | ||||
| @@ -126,16 +126,8 @@ public class QueueBackgroundService( | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|         logger.LogDebug("Processing push notification for account {AccountId}", notification.AccountId); | ||||
|         await pushService.DeliverPushNotification(notification, cancellationToken); | ||||
|         logger.LogDebug("Successfully processed push notification for account {AccountId}", notification.AccountId); | ||||
|     } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Error processing push notification for account {AccountId}", notification.AccountId); | ||||
|             // Don't rethrow to prevent the message from being retried indefinitely | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,8 +1,10 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using NATS.Client.Core; | ||||
| using NATS.Client.JetStream; | ||||
| using NATS.Net; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Services; | ||||
| namespace DysonNetwork.Ring.Services; | ||||
| 
 | ||||
| public class QueueService(INatsConnection nats) | ||||
| { | ||||
| @@ -20,7 +22,8 @@ public class QueueService(INatsConnection nats) | ||||
|             }) | ||||
|         }; | ||||
|         var rawMessage = GrpcTypeHelper.ConvertObjectToByteString(message).ToByteArray(); | ||||
|         await nats.PublishAsync(QueueBackgroundService.QueueName, rawMessage); | ||||
|         var js = nats.CreateJetStreamContext(); | ||||
|         await js.PublishAsync(QueueBackgroundService.QueueName, rawMessage); | ||||
|     } | ||||
| 
 | ||||
|     public async Task EnqueuePushNotification(Notification.Notification notification, Guid userId, bool isSavable = false) | ||||
| @@ -35,7 +38,8 @@ public class QueueService(INatsConnection nats) | ||||
|             Data = JsonSerializer.Serialize(notification) | ||||
|         }; | ||||
|         var rawMessage = GrpcTypeHelper.ConvertObjectToByteString(message).ToByteArray(); | ||||
|         await nats.PublishAsync(QueueBackgroundService.QueueName, rawMessage); | ||||
|         var js = nats.CreateJetStreamContext(); | ||||
|         await js.PublishAsync(QueueBackgroundService.QueueName, rawMessage); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @@ -1,9 +1,9 @@ | ||||
| using System.Net; | ||||
| using DysonNetwork.Pusher.Services; | ||||
| using DysonNetwork.Ring.Services; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using Microsoft.AspNetCore.HttpOverrides; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Startup; | ||||
| namespace DysonNetwork.Ring.Startup; | ||||
| 
 | ||||
| public static class ApplicationConfiguration | ||||
| { | ||||
| @@ -39,7 +39,7 @@ public static class ApplicationConfiguration | ||||
|      | ||||
|     public static WebApplication ConfigureGrpcServices(this WebApplication app) | ||||
|     { | ||||
|         app.MapGrpcService<PusherServiceGrpc>(); | ||||
|         app.MapGrpcService<RingServiceGrpc>(); | ||||
|          | ||||
|         return app; | ||||
|     } | ||||
| @@ -1,7 +1,7 @@ | ||||
| using DysonNetwork.Pusher.Notification; | ||||
| using DysonNetwork.Ring.Notification; | ||||
| using Quartz; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Startup; | ||||
| namespace DysonNetwork.Ring.Startup; | ||||
| 
 | ||||
| public static class ScheduledJobsConfiguration | ||||
| { | ||||
| @@ -1,11 +1,12 @@ | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using System.Threading.RateLimiting; | ||||
| using CorePush.Apple; | ||||
| using CorePush.Firebase; | ||||
| using DysonNetwork.Pusher.Connection; | ||||
| using DysonNetwork.Pusher.Email; | ||||
| using DysonNetwork.Pusher.Notification; | ||||
| using DysonNetwork.Pusher.Services; | ||||
| using DysonNetwork.Ring.Connection; | ||||
| using DysonNetwork.Ring.Email; | ||||
| using DysonNetwork.Ring.Notification; | ||||
| using DysonNetwork.Ring.Services; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.AspNetCore.RateLimiting; | ||||
| using Microsoft.OpenApi.Models; | ||||
| @@ -13,18 +14,13 @@ using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
| using StackExchange.Redis; | ||||
| 
 | ||||
| namespace DysonNetwork.Pusher.Startup; | ||||
| namespace DysonNetwork.Ring.Startup; | ||||
| 
 | ||||
| public static class ServiceCollectionExtensions | ||||
| { | ||||
|     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddDbContext<AppDatabase>(); | ||||
|         services.AddSingleton<IConnectionMultiplexer>(_ => | ||||
|         { | ||||
|             var connection = configuration.GetConnectionString("FastRetrieve")!; | ||||
|             return ConnectionMultiplexer.Connect(connection); | ||||
|         }); | ||||
|         services.AddSingleton<IClock>(SystemClock.Instance); | ||||
|         services.AddHttpContextAccessor(); | ||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); | ||||
| @@ -43,11 +39,12 @@ public static class ServiceCollectionExtensions | ||||
|         services.AddGrpc(); | ||||
| 
 | ||||
|         // Register gRPC services | ||||
|         services.AddScoped<PusherServiceGrpc>(); | ||||
|         services.AddScoped<RingServiceGrpc>(); | ||||
| 
 | ||||
|         // Register OIDC services | ||||
|         services.AddControllers().AddJsonOptions(options => | ||||
|         { | ||||
|             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
| 
 | ||||
| @@ -86,7 +83,7 @@ public static class ServiceCollectionExtensions | ||||
|             options.SwaggerDoc("v1", new OpenApiInfo | ||||
|             { | ||||
|                 Version = "v1", | ||||
|                 Title = "Dyson Pusher", | ||||
|                 Title = "Dyson Ring", | ||||
|                 Description = "The pusher service of the Dyson Network. Mainly handling emailing, notifications and websockets.", | ||||
|                 TermsOfService = new Uri("https://solsynth.dev/terms"), | ||||
|                 License = new OpenApiLicense | ||||
| @@ -9,10 +9,7 @@ | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_pusher;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60", | ||||
|     "FastRetrieve": "localhost:6379", | ||||
|     "Etcd": "etcd.orb.local:2379", | ||||
|     "Stream": "nats.orb.local:4222" | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_pusher;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" | ||||
|   }, | ||||
|   "Notifications": { | ||||
|     "Push": { | ||||
| @@ -44,10 +41,8 @@ | ||||
|     "::1" | ||||
|   ], | ||||
|   "Service": { | ||||
|     "Name": "DysonNetwork.Pusher", | ||||
|     "Url": "https://localhost:7259", | ||||
|     "ClientCert": "../Certificates/client.crt", | ||||
|     "ClientKey": "../Certificates/client.key" | ||||
|     "Name": "DysonNetwork.Ring", | ||||
|     "Url": "https://localhost:7259" | ||||
|   }, | ||||
|   "Etcd": { | ||||
|     "Insecure": true | ||||
| @@ -0,0 +1,26 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>net9.0</TargetFramework> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <IsAspireSharedProject>true</IsAspireSharedProject> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <FrameworkReference Include="Microsoft.AspNetCore.App"/> | ||||
|         <PackageReference Include="Aspire.NATS.Net" Version="9.4.2" /> | ||||
|         <PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.4.2" /> | ||||
|         <PackageReference Include="Aspire.StackExchange.Redis" Version="9.4.2" /> | ||||
|  | ||||
|         <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0"/> | ||||
|         <PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.4.2"/> | ||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/> | ||||
|         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.12.0-beta.1" /> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0"/> | ||||
|     </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										137
									
								
								DysonNetwork.ServiceDefaults/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								DysonNetwork.ServiceDefaults/Extensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| using Microsoft.AspNetCore.Builder; | ||||
| using Microsoft.AspNetCore.Diagnostics.HealthChecks; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.Diagnostics.HealthChecks; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using OpenTelemetry; | ||||
| using OpenTelemetry.Metrics; | ||||
| using OpenTelemetry.Trace; | ||||
|  | ||||
| namespace Microsoft.Extensions.Hosting; | ||||
|  | ||||
| // Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. | ||||
| // This project should be referenced by each service project in your solution. | ||||
| // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults | ||||
| public static class Extensions | ||||
| { | ||||
|     private const string HealthEndpointPath = "/health"; | ||||
|     private const string AlivenessEndpointPath = "/alive"; | ||||
|  | ||||
|     public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder | ||||
|     { | ||||
|         // Allow unencrypted grpc | ||||
|         AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); | ||||
|          | ||||
|         builder.ConfigureOpenTelemetry(); | ||||
|  | ||||
|         builder.AddDefaultHealthChecks(); | ||||
|  | ||||
|         builder.Services.AddServiceDiscovery(); | ||||
|  | ||||
|         builder.Services.ConfigureHttpClientDefaults(http => | ||||
|         { | ||||
|             // Turn on resilience by default | ||||
|             http.AddStandardResilienceHandler(); | ||||
|  | ||||
|             // Turn on service discovery by default | ||||
|             http.AddServiceDiscovery(); | ||||
|         }); | ||||
|  | ||||
|         // Uncomment the following to restrict the allowed schemes for service discovery. | ||||
|         // builder.Services.Configure<ServiceDiscoveryOptions>(options => | ||||
|         // { | ||||
|         //     options.AllowedSchemes = ["https"]; | ||||
|         // }); | ||||
|  | ||||
|         builder.AddNatsClient("queue"); | ||||
|         builder.AddRedisClient("cache", configureOptions: opts => | ||||
|         { | ||||
|             opts.AbortOnConnectFail = false; | ||||
|         }); | ||||
|  | ||||
|         return builder; | ||||
|     } | ||||
|  | ||||
|     public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) | ||||
|         where TBuilder : IHostApplicationBuilder | ||||
|     { | ||||
|         builder.Logging.AddOpenTelemetry(logging => | ||||
|         { | ||||
|             logging.IncludeFormattedMessage = true; | ||||
|             logging.IncludeScopes = true; | ||||
|         }); | ||||
|  | ||||
|         builder.Services.AddOpenTelemetry() | ||||
|             .WithMetrics(metrics => | ||||
|             { | ||||
|                 metrics.AddAspNetCoreInstrumentation() | ||||
|                     .AddHttpClientInstrumentation() | ||||
|                     .AddRuntimeInstrumentation(); | ||||
|             }) | ||||
|             .WithTracing(tracing => | ||||
|             { | ||||
|                 tracing.AddSource(builder.Environment.ApplicationName) | ||||
|                     .AddAspNetCoreInstrumentation(tracing => | ||||
|                         // Exclude health check requests from tracing | ||||
|                         tracing.Filter = context => | ||||
|                             !context.Request.Path.StartsWithSegments(HealthEndpointPath) | ||||
|                             && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) | ||||
|                     ) | ||||
|                     .AddGrpcClientInstrumentation() | ||||
|                     .AddHttpClientInstrumentation(); | ||||
|             }); | ||||
|  | ||||
|         builder.AddOpenTelemetryExporters(); | ||||
|  | ||||
|         return builder; | ||||
|     } | ||||
|  | ||||
|     private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) | ||||
|         where TBuilder : IHostApplicationBuilder | ||||
|     { | ||||
|         var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); | ||||
|  | ||||
|         if (useOtlpExporter) | ||||
|         { | ||||
|             builder.Services.AddOpenTelemetry().UseOtlpExporter(); | ||||
|         } | ||||
|  | ||||
|         // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) | ||||
|         //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) | ||||
|         //{ | ||||
|         //    builder.Services.AddOpenTelemetry() | ||||
|         //       .UseAzureMonitor(); | ||||
|         //} | ||||
|  | ||||
|         return builder; | ||||
|     } | ||||
|  | ||||
|     public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) | ||||
|         where TBuilder : IHostApplicationBuilder | ||||
|     { | ||||
|         builder.Services.AddHealthChecks() | ||||
|             // Add a default liveness check to ensure app is responsive | ||||
|             .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); | ||||
|  | ||||
|         return builder; | ||||
|     } | ||||
|  | ||||
|     public static WebApplication MapDefaultEndpoints(this WebApplication app) | ||||
|     { | ||||
|         // Adding health checks endpoints to applications in non-development environments has security implications. | ||||
|         // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. | ||||
|         if (app.Environment.IsDevelopment()) | ||||
|         { | ||||
|             // All health checks must pass for app to be considered ready to accept traffic after starting | ||||
|             app.MapHealthChecks(HealthEndpointPath); | ||||
|  | ||||
|             // Only health checks tagged with the "live" tag must pass for app to be considered alive | ||||
|             app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions | ||||
|             { | ||||
|                 Predicate = r => r.Tags.Contains("live") | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         return app; | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,5 @@ | ||||
| using dotnet_etcd.interfaces; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
|  | ||||
| @@ -11,33 +11,7 @@ public static class DysonAuthStartup | ||||
|         this IServiceCollection services | ||||
|     ) | ||||
|     { | ||||
|         services.AddSingleton<AuthService.AuthServiceClient>(sp => | ||||
|         { | ||||
|             var etcdClient = sp.GetRequiredService<IEtcdClient>(); | ||||
|             var config = sp.GetRequiredService<IConfiguration>(); | ||||
|             var clientCertPath = config["Service:ClientCert"]!; | ||||
|             var clientKeyPath = config["Service:ClientKey"]!; | ||||
|             var clientCertPassword = config["Service:CertPassword"]; | ||||
|  | ||||
|             return GrpcClientHelper | ||||
|                 .CreateAuthServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword) | ||||
|                 .GetAwaiter() | ||||
|                 .GetResult(); | ||||
|         }); | ||||
|          | ||||
|         services.AddSingleton<PermissionService.PermissionServiceClient>(sp => | ||||
|         { | ||||
|             var etcdClient = sp.GetRequiredService<IEtcdClient>(); | ||||
|             var config = sp.GetRequiredService<IConfiguration>(); | ||||
|             var clientCertPath = config["Service:ClientCert"]!; | ||||
|             var clientKeyPath = config["Service:ClientKey"]!; | ||||
|             var clientCertPassword = config["Service:CertPassword"]; | ||||
|  | ||||
|             return GrpcClientHelper | ||||
|                 .CreatePermissionServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword) | ||||
|                 .GetAwaiter() | ||||
|                 .GetResult(); | ||||
|         }); | ||||
|         services.AddAuthService(); | ||||
|  | ||||
|         services.AddAuthentication(options => | ||||
|             { | ||||
|   | ||||
| @@ -210,6 +210,7 @@ public class CacheServiceRedis : ICacheService | ||||
|                 Modifiers = { JsonExtensions.UnignoreAllProperties() }, | ||||
|             }, | ||||
|             ReferenceHandler = ReferenceHandler.Preserve, | ||||
|             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||
|         }; | ||||
|         _jsonOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||
|         _jsonOptions.PropertyNameCaseInsensitive = true; | ||||
|   | ||||
| @@ -7,11 +7,11 @@ | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="dotnet-etcd" Version="8.0.1" /> | ||||
|         <PackageReference Include="Google.Api.CommonProtos" Version="2.17.0" /> | ||||
|         <PackageReference Include="Google.Protobuf" Version="3.31.1" /> | ||||
|         <PackageReference Include="Google.Protobuf.Tools" Version="3.31.1" /> | ||||
|         <PackageReference Include="Grpc" Version="2.46.6" /> | ||||
|         <PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.71.0" /> | ||||
|         <PackageReference Include="Grpc.Net.Client" Version="2.71.0" /> | ||||
|         <PackageReference Include="Grpc.Tools" Version="2.72.0"> | ||||
|             <PrivateAssets>all</PrivateAssets> | ||||
| @@ -21,14 +21,12 @@ | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" /> | ||||
|         <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" /> | ||||
|         <PackageReference Include="NATS.Client.Core" Version="2.6.6" /> | ||||
|         <PackageReference Include="NetTopologySuite" Version="2.6.0" /> | ||||
|         <PackageReference Include="NATS.Net" Version="2.6.8" /> | ||||
|         <PackageReference Include="NodaTime" Version="3.2.2" /> | ||||
|         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> | ||||
|         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> | ||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> | ||||
|         <PackageReference Include="OpenGraph-Net" Version="4.0.1" /> | ||||
|         <PackageReference Include="StackExchange.Redis" Version="2.8.41" /> | ||||
|         <PackageReference Include="System.Net.Http" Version="4.3.4" /> | ||||
|         <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" /> | ||||
|     </ItemGroup> | ||||
| @@ -41,4 +39,8 @@ | ||||
|       <Folder Include="Error\" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user