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 | ||||||
							
								
								
									
										249
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										249
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,189 +1,60 @@ | |||||||
|  name: Build and Push Microservices | name: Aspire Publish Workflow | ||||||
|   |  | ||||||
|  on: | on: | ||||||
|    push: |   push: | ||||||
|      branches: |     branches: | ||||||
|        - master |       - master | ||||||
|    workflow_dispatch: |   workflow_dispatch: | ||||||
|   |  | ||||||
|  jobs: | jobs: | ||||||
|    build-sphere: |   publish: | ||||||
|      runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|      permissions: |     permissions: | ||||||
|        contents: read |       contents: read | ||||||
|        packages: write |       packages: write | ||||||
|      steps: |     steps: | ||||||
|        - name: Checkout repository |       - name: Checkout repository | ||||||
|          uses: actions/checkout@v3 |         uses: actions/checkout@v3 | ||||||
|          with: |  | ||||||
|            fetch-depth: 0 |       - name: Setup .NET | ||||||
|        - name: Setup NBGV |         uses: actions/setup-dotnet@v3 | ||||||
|          uses: dotnet/nbgv@master |         with: | ||||||
|          id: nbgv |           dotnet-version: "9.0.x" | ||||||
|        - name: Set up Docker Buildx |  | ||||||
|          uses: docker/setup-buildx-action@v3 |       - name: Log in to GitHub Container Registry | ||||||
|        - name: Log in to GitHub Container Registry |         uses: docker/login-action@v3 | ||||||
|          uses: docker/login-action@v3 |         with: | ||||||
|          with: |           registry: ghcr.io | ||||||
|            registry: ghcr.io |           username: ${{ github.actor }} | ||||||
|            username: ${{ github.actor }} |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|            password: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
|        - name: Build and push DysonNetwork.Sphere Docker image |       - name: Install Aspire CLI | ||||||
|          uses: docker/build-push-action@v6 |         run: dotnet tool install -g Aspire.Cli --prerelease | ||||||
|          with: |  | ||||||
|            file: DysonNetwork.Sphere/Dockerfile |       - name: Build and Publish Aspire Application | ||||||
|            context: . |         run: aspire publish --project ./DysonNetwork.Control/DysonNetwork.Control.csproj --output publish | ||||||
|            push: true |  | ||||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-sphere:latest |       - name: Tag and Push Images | ||||||
|            platforms: linux/amd64 |         run: | | ||||||
|   |           IMAGES=( "sphere" "pass" "ring" "drive" "develop" ) | ||||||
|    build-pass: |  | ||||||
|      runs-on: ubuntu-latest |           for image in "${IMAGES[@]}"; do | ||||||
|      permissions: |             IMAGE_NAME="ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-$image:alpha" | ||||||
|        contents: read |             SOURCE_IMAGE_NAME="$image:latest" # Aspire's default local image name | ||||||
|        packages: write |  | ||||||
|      steps: |             echo "Tagging and pushing $SOURCE_IMAGE_NAME to $IMAGE_NAME..." | ||||||
|        - name: Checkout repository |             docker tag $SOURCE_IMAGE_NAME $IMAGE_NAME | ||||||
|          uses: actions/checkout@v3 |             docker push $IMAGE_NAME | ||||||
|          with: |           done | ||||||
|            fetch-depth: 0 |  | ||||||
|        - name: Setup NBGV |       - name: Upload Aspire Publish Directory | ||||||
|          uses: dotnet/nbgv@master |         uses: actions/upload-artifact@v3 | ||||||
|          id: nbgv |         with: | ||||||
|        - name: Set up Docker Buildx |           name: aspire-publish-output | ||||||
|          uses: docker/setup-buildx-action@v3 |           path: ./publish/ | ||||||
|        - name: Log in to GitHub Container Registry |  | ||||||
|          uses: docker/login-action@v3 |       - name: Upload Docker Compose file | ||||||
|          with: |         uses: actions/upload-artifact@v3 | ||||||
|            registry: ghcr.io |         with: | ||||||
|            username: ${{ github.actor }} |           name: docker-compose-output | ||||||
|            password: ${{ secrets.GITHUB_TOKEN }} |           path: ./publish/docker-compose.yml | ||||||
|        - 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 |  | ||||||
|   |  | ||||||
|    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 |  | ||||||
|   |  | ||||||
|    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 |  | ||||||
|   |  | ||||||
|    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 |  | ||||||
|   |  | ||||||
|    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 |  | ||||||
|   |  | ||||||
|   | |||||||
							
								
								
									
										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> | ||||||
|  |  | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|  |     <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||||
|     <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> |     <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|   |   | ||||||
|   | |||||||
| @@ -1,17 +1,16 @@ | |||||||
| using DysonNetwork.Develop; | using DysonNetwork.Develop; | ||||||
| using DysonNetwork.Shared.Auth; | using DysonNetwork.Shared.Auth; | ||||||
| using DysonNetwork.Shared.Http; | using DysonNetwork.Shared.Http; | ||||||
| using DysonNetwork.Shared.Registry; |  | ||||||
| using DysonNetwork.Develop.Startup; | using DysonNetwork.Develop.Startup; | ||||||
| using DysonNetwork.Shared.Stream; | using DysonNetwork.Shared.Registry; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
|  |  | ||||||
| var builder = WebApplication.CreateBuilder(args); | var builder = WebApplication.CreateBuilder(args); | ||||||
|  |  | ||||||
|  | builder.AddServiceDefaults(); | ||||||
|  |  | ||||||
| builder.ConfigureAppKestrel(builder.Configuration); | builder.ConfigureAppKestrel(builder.Configuration); | ||||||
|  |  | ||||||
| builder.Services.AddRegistryService(builder.Configuration); |  | ||||||
| builder.Services.AddStreamConnection(builder.Configuration); |  | ||||||
| builder.Services.AddAppServices(builder.Configuration); | builder.Services.AddAppServices(builder.Configuration); | ||||||
| builder.Services.AddAppAuthentication(); | builder.Services.AddAppAuthentication(); | ||||||
| builder.Services.AddAppSwagger(); | builder.Services.AddAppSwagger(); | ||||||
| @@ -22,6 +21,8 @@ builder.Services.AddDriveService(); | |||||||
|  |  | ||||||
| var app = builder.Build(); | var app = builder.Build(); | ||||||
|  |  | ||||||
|  | app.MapDefaultEndpoints(); | ||||||
|  |  | ||||||
| using (var scope = app.Services.CreateScope()) | using (var scope = app.Services.CreateScope()) | ||||||
| { | { | ||||||
|     var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); |     var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ using Microsoft.OpenApi.Models; | |||||||
| using NodaTime; | using NodaTime; | ||||||
| using NodaTime.Serialization.SystemTextJson; | using NodaTime.Serialization.SystemTextJson; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
|  | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Develop.Identity; | using DysonNetwork.Develop.Identity; | ||||||
| using DysonNetwork.Develop.Project; | using DysonNetwork.Develop.Project; | ||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
| @@ -19,19 +20,16 @@ public static class ServiceCollectionExtensions | |||||||
|         services.AddDbContext<AppDatabase>(); |         services.AddDbContext<AppDatabase>(); | ||||||
|         services.AddSingleton<IClock>(SystemClock.Instance); |         services.AddSingleton<IClock>(SystemClock.Instance); | ||||||
|         services.AddHttpContextAccessor(); |         services.AddHttpContextAccessor(); | ||||||
|         services.AddSingleton<IConnectionMultiplexer>(_ => |  | ||||||
|         { |  | ||||||
|             var connection = configuration.GetConnectionString("FastRetrieve")!; |  | ||||||
|             return ConnectionMultiplexer.Connect(connection); |  | ||||||
|         }); |  | ||||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); |         services.AddSingleton<ICacheService, CacheServiceRedis>(); | ||||||
|  |  | ||||||
|         services.AddHttpClient(); |         services.AddHttpClient(); | ||||||
|  |  | ||||||
|         services.AddControllers().AddJsonOptions(options => |         services.AddControllers().AddJsonOptions(options => | ||||||
|         { |         { | ||||||
|  |             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; |             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; |             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||||
|  |              | ||||||
|             options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); |             options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,10 +10,7 @@ | |||||||
|   }, |   }, | ||||||
|   "AllowedHosts": "*", |   "AllowedHosts": "*", | ||||||
|   "ConnectionStrings": { |   "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", |     "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" |  | ||||||
|   }, |   }, | ||||||
|   "KnownProxies": [ |   "KnownProxies": [ | ||||||
|     "127.0.0.1", |     "127.0.0.1", | ||||||
| @@ -24,8 +21,6 @@ | |||||||
|   }, |   }, | ||||||
|   "Service": { |   "Service": { | ||||||
|     "Name": "DysonNetwork.Develop", |     "Name": "DysonNetwork.Develop", | ||||||
|     "Url": "https://localhost:7192", |     "Url": "https://localhost:7192" | ||||||
|     "ClientCert": "../Certificates/client.crt", |  | ||||||
|     "ClientKey": "../Certificates/client.key" |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -31,7 +31,6 @@ public class AppDatabase( | |||||||
|             opt => opt |             opt => opt | ||||||
|                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) |                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) | ||||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) |                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) | ||||||
|                 .UseNetTopologySuite() |  | ||||||
|                 .UseNodaTime() |                 .UseNodaTime() | ||||||
|         ).UseSnakeCaseNamingConvention(); |         ).UseSnakeCaseNamingConvention(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -35,7 +35,6 @@ | |||||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> |         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> |         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> |         <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="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" /> |         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" /> |         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" /> | ||||||
| @@ -67,6 +66,7 @@ | |||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|  |       <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||||
|       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> |       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||||
|     </ItemGroup> |     </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("ProductVersion", "9.0.7") | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => |             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => | ||||||
|   | |||||||
| @@ -5,18 +5,18 @@ using DysonNetwork.Shared.Auth; | |||||||
| using DysonNetwork.Shared.Http; | using DysonNetwork.Shared.Http; | ||||||
| using DysonNetwork.Shared.PageData; | using DysonNetwork.Shared.PageData; | ||||||
| using DysonNetwork.Shared.Registry; | using DysonNetwork.Shared.Registry; | ||||||
| using DysonNetwork.Shared.Stream; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using tusdotnet.Stores; | using tusdotnet.Stores; | ||||||
|  |  | ||||||
| var builder = WebApplication.CreateBuilder(args); | var builder = WebApplication.CreateBuilder(args); | ||||||
|  |  | ||||||
|  | builder.AddServiceDefaults(); | ||||||
|  |  | ||||||
| // Configure Kestrel and server options | // Configure Kestrel and server options | ||||||
| builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue); | builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue); | ||||||
|  |  | ||||||
| // Add application services | // Add application services | ||||||
| builder.Services.AddRegistryService(builder.Configuration); |  | ||||||
| builder.Services.AddStreamConnection(builder.Configuration); |  | ||||||
| builder.Services.AddAppServices(builder.Configuration); | builder.Services.AddAppServices(builder.Configuration); | ||||||
| builder.Services.AddAppRateLimiting(); | builder.Services.AddAppRateLimiting(); | ||||||
| builder.Services.AddAppAuthentication(); | builder.Services.AddAppAuthentication(); | ||||||
| @@ -39,6 +39,8 @@ builder.Services.AddTransient<IPageDataProvider, VersionPageData>(); | |||||||
|  |  | ||||||
| var app = builder.Build(); | var app = builder.Build(); | ||||||
|  |  | ||||||
|  | app.MapDefaultEndpoints(); | ||||||
|  |  | ||||||
| // Run database migrations | // Run database migrations | ||||||
| using (var scope = app.Services.CreateScope()) | using (var scope = app.Services.CreateScope()) | ||||||
| { | { | ||||||
| @@ -51,8 +53,6 @@ var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>(); | |||||||
| // Configure application middleware pipeline | // Configure application middleware pipeline | ||||||
| app.ConfigureAppMiddleware(tusDiskStore, builder.Environment.ContentRootPath); | app.ConfigureAppMiddleware(tusDiskStore, builder.Environment.ContentRootPath); | ||||||
|  |  | ||||||
| app.MapGatewayProxy(); |  | ||||||
|  |  | ||||||
| app.MapPages(Path.Combine(app.Environment.WebRootPath, "dist", "index.html")); | app.MapPages(Path.Combine(app.Environment.WebRootPath, "dist", "index.html")); | ||||||
|  |  | ||||||
| // Configure gRPC | // Configure gRPC | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ using DysonNetwork.Drive.Storage; | |||||||
| using DysonNetwork.Shared.Stream; | using DysonNetwork.Shared.Stream; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NATS.Client.Core; | using NATS.Client.Core; | ||||||
|  | using NATS.Client.JetStream.Models; | ||||||
|  | using NATS.Net; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Drive.Startup; | namespace DysonNetwork.Drive.Startup; | ||||||
|  |  | ||||||
| @@ -14,12 +16,23 @@ public class BroadcastEventHandler( | |||||||
| { | { | ||||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) |     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 |             try | ||||||
|             { |             { | ||||||
|                 var evt = JsonSerializer.Deserialize<AccountDeletedEvent>(msg.Data); |                 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); |                 logger.LogInformation("Account deleted: {AccountId}", evt.AccountId); | ||||||
|  |  | ||||||
| @@ -46,10 +59,13 @@ public class BroadcastEventHandler( | |||||||
|                     await transaction.RollbackAsync(cancellationToken: stoppingToken); |                     await transaction.RollbackAsync(cancellationToken: stoppingToken); | ||||||
|                     throw; |                     throw; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 await msg.AckAsync(cancellationToken: stoppingToken); | ||||||
|             } |             } | ||||||
|             catch (Exception ex) |             catch (Exception ex) | ||||||
|             { |             { | ||||||
|                 logger.LogError(ex, "Error processing AccountDeleted"); |                 logger.LogError(ex, "Error processing AccountDeleted"); | ||||||
|  |                 await msg.NakAsync(cancellationToken: stoppingToken); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
|  | using System.Text.Json.Serialization; | ||||||
| using System.Threading.RateLimiting; | using System.Threading.RateLimiting; | ||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
| using Microsoft.AspNetCore.RateLimiting; | using Microsoft.AspNetCore.RateLimiting; | ||||||
| @@ -16,11 +17,6 @@ public static class ServiceCollectionExtensions | |||||||
|     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) |     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) | ||||||
|     { |     { | ||||||
|         services.AddDbContext<AppDatabase>(); // Assuming you'll have an AppDatabase |         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.AddSingleton<IClock>(SystemClock.Instance); | ||||||
|         services.AddHttpContextAccessor(); |         services.AddHttpContextAccessor(); | ||||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); // Uncomment if you have CacheServiceRedis |         services.AddSingleton<ICacheService, CacheServiceRedis>(); // Uncomment if you have CacheServiceRedis | ||||||
| @@ -40,6 +36,7 @@ public static class ServiceCollectionExtensions | |||||||
|  |  | ||||||
|         services.AddControllers().AddJsonOptions(options => |         services.AddControllers().AddJsonOptions(options => | ||||||
|         { |         { | ||||||
|  |             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; |             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; |             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,10 +10,7 @@ | |||||||
|   }, |   }, | ||||||
|   "AllowedHosts": "*", |   "AllowedHosts": "*", | ||||||
|   "ConnectionStrings": { |   "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", |     "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" |  | ||||||
|   }, |   }, | ||||||
|   "Authentication": { |   "Authentication": { | ||||||
|     "Schemes": { |     "Schemes": { | ||||||
| @@ -131,8 +128,6 @@ | |||||||
|   ], |   ], | ||||||
|   "Service": { |   "Service": { | ||||||
|     "Name": "DysonNetwork.Drive", |     "Name": "DysonNetwork.Drive", | ||||||
|     "Url": "https://localhost:7092", |     "Url": "https://localhost:7092" | ||||||
|     "ClientCert": "../Certificates/client.crt", |  | ||||||
|     "ClientKey": "../Certificates/client.key" |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 StatusAttitude Attitude { get; set; } | ||||||
|         public bool IsInvisible { get; set; } |         public bool IsInvisible { get; set; } | ||||||
|         public bool IsNotDisturb { get; set; } |         public bool IsNotDisturb { get; set; } | ||||||
|  |         public bool IsAutomated { get; set; } = false; | ||||||
|         [MaxLength(1024)] public string? Label { get; set; } |         [MaxLength(1024)] public string? Label { get; set; } | ||||||
|  |         [MaxLength(4096)] public string? AppIdentifier { get; set; } | ||||||
|         public Instant? ClearedAt { get; set; } |         public Instant? ClearedAt { get; set; } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -197,6 +197,8 @@ public class AccountCurrentController( | |||||||
|     public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request) |     public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         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 now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         var status = await db.AccountStatuses |         var status = await db.AccountStatuses | ||||||
| @@ -205,11 +207,15 @@ public class AccountCurrentController( | |||||||
|             .OrderByDescending(e => e.CreatedAt) |             .OrderByDescending(e => e.CreatedAt) | ||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
|         if (status is null) return NotFound(ApiError.NotFound("status", traceId: HttpContext.TraceIdentifier)); |         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.Attitude = request.Attitude; | ||||||
|         status.IsInvisible = request.IsInvisible; |         status.IsInvisible = request.IsInvisible; | ||||||
|         status.IsNotDisturb = request.IsNotDisturb; |         status.IsNotDisturb = request.IsNotDisturb; | ||||||
|  |         status.IsAutomated = request.IsAutomated; | ||||||
|         status.Label = request.Label; |         status.Label = request.Label; | ||||||
|  |         status.AppIdentifier = request.AppIdentifier; | ||||||
|         status.ClearedAt = request.ClearedAt; |         status.ClearedAt = request.ClearedAt; | ||||||
|  |  | ||||||
|         db.Update(status); |         db.Update(status); | ||||||
| @@ -225,13 +231,44 @@ public class AccountCurrentController( | |||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); |         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 |         var status = new Status | ||||||
|         { |         { | ||||||
|             AccountId = currentUser.Id, |             AccountId = currentUser.Id, | ||||||
|             Attitude = request.Attitude, |             Attitude = request.Attitude, | ||||||
|             IsInvisible = request.IsInvisible, |             IsInvisible = request.IsInvisible, | ||||||
|             IsNotDisturb = request.IsNotDisturb, |             IsNotDisturb = request.IsNotDisturb, | ||||||
|  |             IsAutomated = request.IsAutomated, | ||||||
|             Label = request.Label, |             Label = request.Label, | ||||||
|  |             AppIdentifier = request.AppIdentifier, | ||||||
|             ClearedAt = request.ClearedAt |             ClearedAt = request.ClearedAt | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
| @@ -239,15 +276,21 @@ public class AccountCurrentController( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     [HttpDelete("statuses")] |     [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(); |         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||||
|  |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|         var status = await db.AccountStatuses |         var queryable = db.AccountStatuses | ||||||
|             .Where(s => s.AccountId == currentUser.Id) |             .Where(s => s.AccountId == currentUser.Id) | ||||||
|             .Where(s => s.ClearedAt == null || s.ClearedAt > now) |             .Where(s => s.ClearedAt == null || s.ClearedAt > now) | ||||||
|             .OrderByDescending(s => s.CreatedAt) |             .OrderByDescending(s => s.CreatedAt) | ||||||
|  |             .AsQueryable(); | ||||||
|  |  | ||||||
|  |         if (string.IsNullOrWhiteSpace(app)) | ||||||
|  |             queryable = queryable.Where(s => s.IsAutomated && s.AppIdentifier == app); | ||||||
|  |  | ||||||
|  |         var status = await queryable | ||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
|         if (status is null) return NotFound(); |         if (status is null) return NotFound(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ public class AccountEventService( | |||||||
|     Wallet.PaymentService payment, |     Wallet.PaymentService payment, | ||||||
|     ICacheService cache, |     ICacheService cache, | ||||||
|     IStringLocalizer<Localization.AccountEventResource> localizer, |     IStringLocalizer<Localization.AccountEventResource> localizer, | ||||||
|     PusherService.PusherServiceClient pusher, |     RingService.RingServiceClient pusher, | ||||||
|     SubscriptionService subscriptions, |     SubscriptionService subscriptions, | ||||||
|     Pass.Leveling.ExperienceService experienceService |     Pass.Leveling.ExperienceService experienceService | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -13,6 +13,8 @@ using EFCore.BulkExtensions; | |||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.Extensions.Localization; | using Microsoft.Extensions.Localization; | ||||||
| using NATS.Client.Core; | using NATS.Client.Core; | ||||||
|  | using NATS.Client.JetStream; | ||||||
|  | using NATS.Net; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using OtpNet; | using OtpNet; | ||||||
| using AuthService = DysonNetwork.Pass.Auth.AuthService; | using AuthService = DysonNetwork.Pass.Auth.AuthService; | ||||||
| @@ -26,7 +28,7 @@ public class AccountService( | |||||||
|     FileReferenceService.FileReferenceServiceClient fileRefs, |     FileReferenceService.FileReferenceServiceClient fileRefs, | ||||||
|     AccountUsernameService uname, |     AccountUsernameService uname, | ||||||
|     EmailService mailer, |     EmailService mailer, | ||||||
|     PusherService.PusherServiceClient pusher, |     RingService.RingServiceClient pusher, | ||||||
|     IStringLocalizer<NotificationResource> localizer, |     IStringLocalizer<NotificationResource> localizer, | ||||||
|     IStringLocalizer<EmailResource> emailLocalizer, |     IStringLocalizer<EmailResource> emailLocalizer, | ||||||
|     ICacheService cache, |     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(); |         var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync(); | ||||||
|         if (dupeAutomateCount > 0) |         if (dupeAutomateCount > 0) | ||||||
| @@ -230,7 +233,7 @@ public class AccountService( | |||||||
|             ); |             ); | ||||||
|             account.Profile.Background = CloudFileReferenceObject.FromProtoValue(file); |             account.Profile.Background = CloudFileReferenceObject.FromProtoValue(file); | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         db.Accounts.Add(account); |         db.Accounts.Add(account); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
| @@ -442,7 +445,7 @@ public class AccountService( | |||||||
|                 if (contact is null) |                 if (contact is null) | ||||||
|                 { |                 { | ||||||
|                     logger.LogWarning( |                     logger.LogWarning( | ||||||
|                         "Unable to send factor code to #{FactorId} with, due to no contact method was found...",  |                         "Unable to send factor code to #{FactorId} with, due to no contact method was found...", | ||||||
|                         factor.Id |                         factor.Id | ||||||
|                     ); |                     ); | ||||||
|                     return; |                     return; | ||||||
| @@ -740,10 +743,14 @@ public class AccountService( | |||||||
|         db.Accounts.Remove(account); |         db.Accounts.Remove(account); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|         await nats.PublishAsync(AccountDeletedEvent.Type, JsonSerializer.SerializeToUtf8Bytes(new AccountDeletedEvent |         var js = nats.CreateJetStreamContext(); | ||||||
|         { |         await js.PublishAsync( | ||||||
|             AccountId = account.Id, |             AccountDeletedEvent.Type, | ||||||
|             DeletedAt = SystemClock.Instance.GetCurrentInstant() |             GrpcTypeHelper.ConvertObjectToByteString(new AccountDeletedEvent | ||||||
|         })); |             { | ||||||
|  |                 AccountId = account.Id, | ||||||
|  |                 DeletedAt = SystemClock.Instance.GetCurrentInstant() | ||||||
|  |             }).ToByteArray() | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,6 +1,8 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
|  | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Shared.Data; | using DysonNetwork.Shared.Data; | ||||||
|  | using DysonNetwork.Shared.GeoIp; | ||||||
| using DysonNetwork.Shared.Proto; | using DysonNetwork.Shared.Proto; | ||||||
| using NodaTime.Serialization.Protobuf; | using NodaTime.Serialization.Protobuf; | ||||||
| using Point = NetTopologySuite.Geometries.Point; | using Point = NetTopologySuite.Geometries.Point; | ||||||
| @@ -14,7 +16,7 @@ public class ActionLog : ModelBase | |||||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); |     [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); | ||||||
|     [MaxLength(512)] public string? UserAgent { get; set; } |     [MaxLength(512)] public string? UserAgent { get; set; } | ||||||
|     [MaxLength(128)] public string? IpAddress { 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 Guid AccountId { get; set; } | ||||||
|     public Account Account { get; set; } = null!; |     public Account Account { get; set; } = null!; | ||||||
|   | |||||||
| @@ -23,6 +23,12 @@ public class Status : ModelBase | |||||||
|     public bool IsNotDisturb { get; set; } |     public bool IsNotDisturb { get; set; } | ||||||
|     [MaxLength(1024)] public string? Label { get; set; } |     [MaxLength(1024)] public string? Label { get; set; } | ||||||
|     public Instant? ClearedAt { 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 Guid AccountId { get; set; } | ||||||
|     public Account Account { get; set; } = null!; |     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( | public class RelationshipService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     ICacheService cache, |     ICacheService cache, | ||||||
|     PusherService.PusherServiceClient pusher, |     RingService.RingServiceClient pusher, | ||||||
|     IStringLocalizer<NotificationResource> localizer |     IStringLocalizer<NotificationResource> localizer | ||||||
| ) | ) | ||||||
| { | { | ||||||
|   | |||||||
| @@ -69,7 +69,6 @@ public class AppDatabase( | |||||||
|                     }) |                     }) | ||||||
|                 ) |                 ) | ||||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) |                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) | ||||||
|                 .UseNetTopologySuite() |  | ||||||
|                 .UseNodaTime() |                 .UseNodaTime() | ||||||
|         ).UseSnakeCaseNamingConvention(); |         ).UseSnakeCaseNamingConvention(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ public class AuthController( | |||||||
|     AuthService auth, |     AuthService auth, | ||||||
|     GeoIpService geo, |     GeoIpService geo, | ||||||
|     ActionLogService als, |     ActionLogService als, | ||||||
|     PusherService.PusherServiceClient pusher, |     RingService.RingServiceClient pusher, | ||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
|     IStringLocalizer<NotificationResource> localizer |     IStringLocalizer<NotificationResource> localizer | ||||||
| ) : ControllerBase | ) : ControllerBase | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| using System.Security.Cryptography; | using System.Security.Cryptography; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
|  | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Pass.Account; | using DysonNetwork.Pass.Account; | ||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| @@ -137,6 +138,7 @@ public class AuthService( | |||||||
|  |  | ||||||
|         var jsonOpts = new JsonSerializerOptions |         var jsonOpts = new JsonSerializerOptions | ||||||
|         { |         { | ||||||
|  |             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||||
|             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, |             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||||
|             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower |             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower | ||||||
|         }; |         }; | ||||||
| @@ -211,8 +213,7 @@ public class AuthService( | |||||||
|         var session = new AuthSession |         var session = new AuthSession | ||||||
|         { |         { | ||||||
|             LastGrantedAt = now, |             LastGrantedAt = now, | ||||||
|             // Never expire server-side |             ExpiredAt = now.Plus(Duration.FromDays(7)), | ||||||
|             ExpiredAt = null, |  | ||||||
|             AccountId = challenge.AccountId, |             AccountId = challenge.AccountId, | ||||||
|             ChallengeId = challenge.Id |             ChallengeId = challenge.Id | ||||||
|         }; |         }; | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; | |||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
| using System.Text.Json.Serialization; | using System.Text.Json.Serialization; | ||||||
| using DysonNetwork.Shared.Data; | using DysonNetwork.Shared.Data; | ||||||
|  | using DysonNetwork.Shared.GeoIp; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using NodaTime.Serialization.Protobuf; | using NodaTime.Serialization.Protobuf; | ||||||
| using Point = NetTopologySuite.Geometries.Point; | using Point = NetTopologySuite.Geometries.Point; | ||||||
| @@ -16,11 +17,11 @@ public class AuthSession : ModelBase | |||||||
|  |  | ||||||
|     public Guid AccountId { get; set; } |     public Guid AccountId { get; set; } | ||||||
|     [JsonIgnore] public Account.Account Account { get; set; } = null!; |     [JsonIgnore] public Account.Account Account { get; set; } = null!; | ||||||
|      |  | ||||||
|     // When the challenge is null, indicates the session is for an API Key |     // When the challenge is null, indicates the session is for an API Key | ||||||
|     public Guid? ChallengeId { get; set; } |     public Guid? ChallengeId { get; set; } | ||||||
|     public AuthChallenge? Challenge { get; set; } = null!; |     public AuthChallenge? Challenge { get; set; } = null!; | ||||||
|      |  | ||||||
|     // Indicates the session is for an OIDC connection |     // Indicates the session is for an OIDC connection | ||||||
|     public Guid? AppId { get; set; } |     public Guid? AppId { get; set; } | ||||||
|  |  | ||||||
| @@ -69,7 +70,7 @@ public class AuthChallenge : ModelBase | |||||||
|     [MaxLength(128)] public string? IpAddress { get; set; } |     [MaxLength(128)] public string? IpAddress { get; set; } | ||||||
|     [MaxLength(512)] public string? UserAgent { get; set; } |     [MaxLength(512)] public string? UserAgent { get; set; } | ||||||
|     [MaxLength(1024)] public string? Nonce { 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; } |     public Guid AccountId { get; set; } | ||||||
|     [JsonIgnore] public Account.Account Account { get; set; } = null!; |     [JsonIgnore] public Account.Account Account { get; set; } = null!; | ||||||
| @@ -129,4 +130,4 @@ public class AuthClientWithChallenge : AuthClient | |||||||
|             AccountId = client.AccountId, |             AccountId = client.AccountId, | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -84,6 +84,7 @@ public class OidcState | |||||||
|     { |     { | ||||||
|         return JsonSerializer.Serialize(this, new JsonSerializerOptions |         return JsonSerializer.Serialize(this, new JsonSerializerOptions | ||||||
|         { |         { | ||||||
|  |             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||||
|             PropertyNamingPolicy = JsonNamingPolicy.CamelCase, |             PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | ||||||
|             DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull |             DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull | ||||||
|         }); |         }); | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ | |||||||
|             <PrivateAssets>all</PrivateAssets> |             <PrivateAssets>all</PrivateAssets> | ||||||
|         </PackageReference> |         </PackageReference> | ||||||
|         <PackageReference Include="Nager.Holiday" Version="1.0.1" /> |         <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"> |         <PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115"> | ||||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|             <PrivateAssets>all</PrivateAssets> |             <PrivateAssets>all</PrivateAssets> | ||||||
| @@ -25,7 +24,6 @@ | |||||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/> |         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/> | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/> |         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/> | ||||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/> |         <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="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4"/> | ||||||
|         <PackageReference Include="OpenGraph-Net" Version="4.0.1" /> |         <PackageReference Include="OpenGraph-Net" Version="4.0.1" /> | ||||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/> |         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/> | ||||||
| @@ -51,6 +49,7 @@ | |||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|  |         <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||||
|         <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj"/> |         <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj"/> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,12 +1,10 @@ | |||||||
| using dotnet_etcd; |  | ||||||
| using dotnet_etcd.interfaces; |  | ||||||
| using DysonNetwork.Shared.Proto; | using DysonNetwork.Shared.Proto; | ||||||
| using Microsoft.AspNetCore.Components; | using Microsoft.AspNetCore.Components; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Pass.Email; | namespace DysonNetwork.Pass.Email; | ||||||
|  |  | ||||||
| public class EmailService( | public class EmailService( | ||||||
|     PusherService.PusherServiceClient pusher, |     RingService.RingServiceClient pusher, | ||||||
|     RazorViewRenderer viewRenderer, |     RazorViewRenderer viewRenderer, | ||||||
|     ILogger<EmailService> logger |     ILogger<EmailService> logger | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ public class LastActiveFlushHandler(IServiceProvider srp, ILogger<LastActiveFlus | |||||||
|     public async Task FlushAsync(IReadOnlyList<LastActiveInfo> items) |     public async Task FlushAsync(IReadOnlyList<LastActiveInfo> items) | ||||||
|     { |     { | ||||||
|         logger.LogInformation("Flushing {Count} LastActiveInfo items...", items.Count); |         logger.LogInformation("Flushing {Count} LastActiveInfo items...", items.Count); | ||||||
|          |  | ||||||
|         using var scope = srp.CreateScope(); |         using var scope = srp.CreateScope(); | ||||||
|         var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); |         var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||||
|  |  | ||||||
| @@ -38,13 +38,22 @@ public class LastActiveFlushHandler(IServiceProvider srp, ILogger<LastActiveFlus | |||||||
|             .ToDictionary(g => g.Key, g => g.Last().SeenAt); |             .ToDictionary(g => g.Key, g => g.Last().SeenAt); | ||||||
|  |  | ||||||
|         var now = SystemClock.Instance.GetCurrentInstant(); |         var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|          |  | ||||||
|         var updatingSessions = sessionMap.Select(x => x.Key).ToList(); |         var updatingSessions = sessionMap.Select(x => x.Key).ToList(); | ||||||
|         var sessionUpdates = await db.AuthSessions |         var sessionUpdates = await db.AuthSessions | ||||||
|             .Where(s => updatingSessions.Contains(s.Id)) |             .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); |         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 updatingAccounts = accountMap.Select(x => x.Key).ToList(); | ||||||
|         var profileUpdates = await db.AccountProfiles |         var profileUpdates = await db.AccountProfiles | ||||||
|             .Where(a => updatingAccounts.Contains(a.AccountId)) |             .Where(a => updatingAccounts.Contains(a.AccountId)) | ||||||
| @@ -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) |     public async Task Execute(IJobExecutionContext context) | ||||||
|     { |     { | ||||||
| @@ -62,7 +72,8 @@ public class LastActiveFlushJob(FlushBufferService fbs, LastActiveFlushHandler h | |||||||
|             logger.LogInformation("Running LastActiveInfo flush job..."); |             logger.LogInformation("Running LastActiveInfo flush job..."); | ||||||
|             await fbs.FlushAsync(hdl); |             await fbs.FlushAsync(hdl); | ||||||
|             logger.LogInformation("Completed LastActiveInfo flush job..."); |             logger.LogInformation("Completed LastActiveInfo flush job..."); | ||||||
|         } catch (Exception ex) |         } | ||||||
|  |         catch (Exception ex) | ||||||
|         { |         { | ||||||
|             logger.LogError(ex, "Error running LastActiveInfo job..."); |             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.Account; | ||||||
| using DysonNetwork.Pass.Wallet; | using DysonNetwork.Pass.Wallet; | ||||||
| using DysonNetwork.Shared.Data; | using DysonNetwork.Shared.Data; | ||||||
|  | using DysonNetwork.Shared.GeoIp; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
| using NetTopologySuite.Geometries; |  | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||||
|  |  | ||||||
| @@ -27,7 +27,6 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|                 .HasAnnotation("ProductVersion", "9.0.7") |                 .HasAnnotation("ProductVersion", "9.0.7") | ||||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); |                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||||
|  |  | ||||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); |  | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
|  |  | ||||||
|             modelBuilder.Entity("DysonNetwork.Pass.Account.AbuseReport", b => |             modelBuilder.Entity("DysonNetwork.Pass.Account.AbuseReport", b => | ||||||
| @@ -525,8 +524,8 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|                         .HasColumnType("character varying(128)") |                         .HasColumnType("character varying(128)") | ||||||
|                         .HasColumnName("ip_address"); |                         .HasColumnName("ip_address"); | ||||||
|  |  | ||||||
|                     b.Property<Point>("Location") |                     b.Property<GeoPoint>("Location") | ||||||
|                         .HasColumnType("geometry") |                         .HasColumnType("jsonb") | ||||||
|                         .HasColumnName("location"); |                         .HasColumnName("location"); | ||||||
|  |  | ||||||
|                     b.Property<Dictionary<string, object>>("Meta") |                     b.Property<Dictionary<string, object>>("Meta") | ||||||
| @@ -768,6 +767,11 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|                         .HasColumnType("uuid") |                         .HasColumnType("uuid") | ||||||
|                         .HasColumnName("account_id"); |                         .HasColumnName("account_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("AppIdentifier") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("app_identifier"); | ||||||
|  |  | ||||||
|                     b.Property<int>("Attitude") |                     b.Property<int>("Attitude") | ||||||
|                         .HasColumnType("integer") |                         .HasColumnType("integer") | ||||||
|                         .HasColumnName("attitude"); |                         .HasColumnName("attitude"); | ||||||
| @@ -784,6 +788,10 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|                         .HasColumnType("timestamp with time zone") |                         .HasColumnType("timestamp with time zone") | ||||||
|                         .HasColumnName("deleted_at"); |                         .HasColumnName("deleted_at"); | ||||||
|  |  | ||||||
|  |                     b.Property<bool>("IsAutomated") | ||||||
|  |                         .HasColumnType("boolean") | ||||||
|  |                         .HasColumnName("is_automated"); | ||||||
|  |  | ||||||
|                     b.Property<bool>("IsInvisible") |                     b.Property<bool>("IsInvisible") | ||||||
|                         .HasColumnType("boolean") |                         .HasColumnType("boolean") | ||||||
|                         .HasColumnName("is_invisible"); |                         .HasColumnName("is_invisible"); | ||||||
| @@ -901,8 +909,8 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|                         .HasColumnType("character varying(128)") |                         .HasColumnType("character varying(128)") | ||||||
|                         .HasColumnName("ip_address"); |                         .HasColumnName("ip_address"); | ||||||
|  |  | ||||||
|                     b.Property<Point>("Location") |                     b.Property<GeoPoint>("Location") | ||||||
|                         .HasColumnType("geometry") |                         .HasColumnType("jsonb") | ||||||
|                         .HasColumnName("location"); |                         .HasColumnName("location"); | ||||||
|  |  | ||||||
|                     b.Property<string>("Nonce") |                     b.Property<string>("Nonce") | ||||||
|   | |||||||
| @@ -4,25 +4,21 @@ using DysonNetwork.Pass.Startup; | |||||||
| using DysonNetwork.Shared.Http; | using DysonNetwork.Shared.Http; | ||||||
| using DysonNetwork.Shared.PageData; | using DysonNetwork.Shared.PageData; | ||||||
| using DysonNetwork.Shared.Registry; | using DysonNetwork.Shared.Registry; | ||||||
| using DysonNetwork.Shared.Stream; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
|  |  | ||||||
| var builder = WebApplication.CreateBuilder(args); | var builder = WebApplication.CreateBuilder(args); | ||||||
|  |  | ||||||
|  | builder.AddServiceDefaults(); | ||||||
|  |  | ||||||
| // Configure Kestrel and server options | // Configure Kestrel and server options | ||||||
| builder.ConfigureAppKestrel(builder.Configuration); | builder.ConfigureAppKestrel(builder.Configuration); | ||||||
|  |  | ||||||
| // Add metrics and telemetry |  | ||||||
| builder.Services.AddAppMetrics(); |  | ||||||
|  |  | ||||||
| // Add application services | // Add application services | ||||||
| builder.Services.AddRegistryService(builder.Configuration); |  | ||||||
| builder.Services.AddStreamConnection(builder.Configuration); |  | ||||||
| builder.Services.AddAppServices(builder.Configuration); | builder.Services.AddAppServices(builder.Configuration); | ||||||
| builder.Services.AddAppRateLimiting(); | builder.Services.AddAppRateLimiting(); | ||||||
| builder.Services.AddAppAuthentication(); | builder.Services.AddAppAuthentication(); | ||||||
| builder.Services.AddAppSwagger(); | builder.Services.AddAppSwagger(); | ||||||
| builder.Services.AddPusherService(); | builder.Services.AddRingService(); | ||||||
| builder.Services.AddDriveService(); | builder.Services.AddDriveService(); | ||||||
| builder.Services.AddDevelopService(); | builder.Services.AddDevelopService(); | ||||||
|  |  | ||||||
| @@ -41,6 +37,8 @@ builder.Services.AddTransient<IPageDataProvider, AccountPageData>(); | |||||||
|  |  | ||||||
| var app = builder.Build(); | var app = builder.Build(); | ||||||
|  |  | ||||||
|  | app.MapDefaultEndpoints(); | ||||||
|  |  | ||||||
| // Run database migrations | // Run database migrations | ||||||
| using (var scope = app.Services.CreateScope()) | using (var scope = app.Services.CreateScope()) | ||||||
| { | { | ||||||
| @@ -51,8 +49,6 @@ using (var scope = app.Services.CreateScope()) | |||||||
| // Configure application middleware pipeline | // Configure application middleware pipeline | ||||||
| app.ConfigureAppMiddleware(builder.Configuration, builder.Environment.ContentRootPath); | app.ConfigureAppMiddleware(builder.Configuration, builder.Environment.ContentRootPath); | ||||||
|  |  | ||||||
| app.MapGatewayProxy(); |  | ||||||
|  |  | ||||||
| app.MapPages(Path.Combine(builder.Environment.WebRootPath, "dist", "index.html")); | app.MapPages(Path.Combine(builder.Environment.WebRootPath, "dist", "index.html")); | ||||||
|  |  | ||||||
| // Configure gRPC | // Configure gRPC | ||||||
|   | |||||||
| @@ -1,7 +1,10 @@ | |||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Pass.Wallet; | using DysonNetwork.Pass.Wallet; | ||||||
|  | using DysonNetwork.Shared.Proto; | ||||||
| using DysonNetwork.Shared.Stream; | using DysonNetwork.Shared.Stream; | ||||||
| using NATS.Client.Core; | using NATS.Client.Core; | ||||||
|  | using NATS.Client.JetStream.Models; | ||||||
|  | using NATS.Net; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Pass.Startup; | namespace DysonNetwork.Pass.Startup; | ||||||
|  |  | ||||||
| @@ -13,18 +16,30 @@ public class BroadcastEventHandler( | |||||||
| { | { | ||||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) |     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; |             PaymentOrderEvent? evt = null; | ||||||
|             try |             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 || |                 if (evt?.ProductIdentifier is null || | ||||||
|                     !evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram)) |                     !evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram)) | ||||||
|                 { |  | ||||||
|                     continue; |                     continue; | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 logger.LogInformation("Handling stellar program order: {OrderId}", evt.OrderId); |                 logger.LogInformation("Handling stellar program order: {OrderId}", evt.OrderId); | ||||||
|  |  | ||||||
| @@ -38,18 +53,21 @@ public class BroadcastEventHandler( | |||||||
|                 ); |                 ); | ||||||
|                 if (order is null) |                 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; |                     continue; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 await subscriptions.HandleSubscriptionOrder(order); |                 await subscriptions.HandleSubscriptionOrder(order); | ||||||
|  |  | ||||||
|                 logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId); |                 logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId); | ||||||
|  |                 await msg.AckAsync(cancellationToken: stoppingToken); | ||||||
|             } |             } | ||||||
|             catch (Exception ex) |             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 NodaTime.Serialization.SystemTextJson; | ||||||
| using StackExchange.Redis; | using StackExchange.Redis; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
|  | using System.Text.Json.Serialization; | ||||||
| using System.Threading.RateLimiting; | using System.Threading.RateLimiting; | ||||||
| using DysonNetwork.Pass.Auth.OidcProvider.Options; | using DysonNetwork.Pass.Auth.OidcProvider.Options; | ||||||
| using DysonNetwork.Pass.Auth.OidcProvider.Services; | using DysonNetwork.Pass.Auth.OidcProvider.Services; | ||||||
| @@ -33,11 +34,6 @@ public static class ServiceCollectionExtensions | |||||||
|         services.AddLocalization(options => options.ResourcesPath = "Resources"); |         services.AddLocalization(options => options.ResourcesPath = "Resources"); | ||||||
|  |  | ||||||
|         services.AddDbContext<AppDatabase>(); |         services.AddDbContext<AppDatabase>(); | ||||||
|         services.AddSingleton<IConnectionMultiplexer>(_ => |  | ||||||
|         { |  | ||||||
|             var connection = configuration.GetConnectionString("FastRetrieve")!; |  | ||||||
|             return ConnectionMultiplexer.Connect(connection); |  | ||||||
|         }); |  | ||||||
|         services.AddSingleton<IClock>(SystemClock.Instance); |         services.AddSingleton<IClock>(SystemClock.Instance); | ||||||
|         services.AddHttpContextAccessor(); |         services.AddHttpContextAccessor(); | ||||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); |         services.AddSingleton<ICacheService, CacheServiceRedis>(); | ||||||
| @@ -51,9 +47,9 @@ public static class ServiceCollectionExtensions | |||||||
|             options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB |             options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB | ||||||
|             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB |             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB | ||||||
|         }); |         }); | ||||||
|          |  | ||||||
|         services.AddPusherService(); |         services.AddRingService(); | ||||||
|          |  | ||||||
|         // Register OIDC services |         // Register OIDC services | ||||||
|         services.AddScoped<OidcService, GoogleOidcService>(); |         services.AddScoped<OidcService, GoogleOidcService>(); | ||||||
|         services.AddScoped<OidcService, AppleOidcService>(); |         services.AddScoped<OidcService, AppleOidcService>(); | ||||||
| @@ -70,6 +66,7 @@ public static class ServiceCollectionExtensions | |||||||
|  |  | ||||||
|         services.AddControllers().AddJsonOptions(options => |         services.AddControllers().AddJsonOptions(options => | ||||||
|         { |         { | ||||||
|  |             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; |             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; |             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||||
|  |  | ||||||
| @@ -132,7 +129,8 @@ public static class ServiceCollectionExtensions | |||||||
|             { |             { | ||||||
|                 Version = "v1", |                 Version = "v1", | ||||||
|                 Title = "Dyson Pass", |                 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"), |                 TermsOfService = new Uri("https://solsynth.dev/terms"), | ||||||
|                 License = new OpenApiLicense |                 License = new OpenApiLicense | ||||||
|                 { |                 { | ||||||
| @@ -190,6 +188,7 @@ public static class ServiceCollectionExtensions | |||||||
|         services.AddScoped<ActionLogService>(); |         services.AddScoped<ActionLogService>(); | ||||||
|         services.AddScoped<AccountService>(); |         services.AddScoped<AccountService>(); | ||||||
|         services.AddScoped<AccountEventService>(); |         services.AddScoped<AccountEventService>(); | ||||||
|  |         services.AddScoped<NotableDaysService>(); | ||||||
|         services.AddScoped<ActionLogService>(); |         services.AddScoped<ActionLogService>(); | ||||||
|         services.AddScoped<RelationshipService>(); |         services.AddScoped<RelationshipService>(); | ||||||
|         services.AddScoped<MagicSpellService>(); |         services.AddScoped<MagicSpellService>(); | ||||||
| @@ -206,6 +205,8 @@ public static class ServiceCollectionExtensions | |||||||
|          |          | ||||||
|         services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider")); |         services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider")); | ||||||
|         services.AddScoped<OidcProviderService>(); |         services.AddScoped<OidcProviderService>(); | ||||||
|  |          | ||||||
|  |         services.AddHostedService<BroadcastEventHandler>(); | ||||||
|  |  | ||||||
|         return services; |         return services; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -24,8 +24,7 @@ public class OrderController(PaymentService payment, AuthService auth, AppDataba | |||||||
|     [Authorize] |     [Authorize] | ||||||
|     public async Task<ActionResult<Order>> PayOrder(Guid id, [FromBody] PayOrderRequest request) |     public async Task<ActionResult<Order>> PayOrder(Guid id, [FromBody] PayOrderRequest request) | ||||||
|     { |     { | ||||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser || |         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); |  | ||||||
|      |      | ||||||
|         // Validate PIN code |         // Validate PIN code | ||||||
|         if (!await auth.ValidatePinCode(currentUser.Id, request.PinCode)) |         if (!await auth.ValidatePinCode(currentUser.Id, request.PinCode)) | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ public enum OrderStatus | |||||||
| public class Order : ModelBase | public class Order : ModelBase | ||||||
| { | { | ||||||
|     public const string InternalAppIdentifier = "internal"; |     public const string InternalAppIdentifier = "internal"; | ||||||
|      |  | ||||||
|     public Guid Id { get; set; } = Guid.NewGuid(); |     public Guid Id { get; set; } = Guid.NewGuid(); | ||||||
|     public OrderStatus Status { get; set; } = OrderStatus.Unpaid; |     public OrderStatus Status { get; set; } = OrderStatus.Unpaid; | ||||||
|     [MaxLength(128)] public string Currency { get; set; } = null!; |     [MaxLength(128)] public string Currency { get; set; } = null!; | ||||||
| @@ -72,8 +72,8 @@ public class Order : ModelBase | |||||||
|             : null, |             : null, | ||||||
|         Amount = decimal.Parse(proto.Amount), |         Amount = decimal.Parse(proto.Amount), | ||||||
|         ExpiredAt = proto.ExpiredAt.ToInstant(), |         ExpiredAt = proto.ExpiredAt.ToInstant(), | ||||||
|         PayeeWalletId = proto.HasPayeeWalletId ? Guid.Parse(proto.PayeeWalletId) : null, |         PayeeWalletId = proto.PayeeWalletId is not null ? Guid.Parse(proto.PayeeWalletId) : null, | ||||||
|         TransactionId = proto.HasTransactionId ? Guid.Parse(proto.TransactionId) : null, |         TransactionId = proto.TransactionId is not null ? Guid.Parse(proto.TransactionId) : null, | ||||||
|         Transaction = proto.Transaction is not null ? Transaction.FromProtoValue(proto.Transaction) : null, |         Transaction = proto.Transaction is not null ? Transaction.FromProtoValue(proto.Transaction) : null, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| @@ -106,7 +106,7 @@ public class Transaction : ModelBase | |||||||
|     { |     { | ||||||
|         Id = Id.ToString(), |         Id = Id.ToString(), | ||||||
|         Currency = Currency, |         Currency = Currency, | ||||||
|         Amount = Amount.ToString(), |         Amount = Amount.ToString(CultureInfo.InvariantCulture), | ||||||
|         Remarks = Remarks, |         Remarks = Remarks, | ||||||
|         Type = (Shared.Proto.TransactionType)Type, |         Type = (Shared.Proto.TransactionType)Type, | ||||||
|         PayerWalletId = PayerWalletId?.ToString(), |         PayerWalletId = PayerWalletId?.ToString(), | ||||||
| @@ -120,7 +120,7 @@ public class Transaction : ModelBase | |||||||
|         Amount = decimal.Parse(proto.Amount), |         Amount = decimal.Parse(proto.Amount), | ||||||
|         Remarks = proto.Remarks, |         Remarks = proto.Remarks, | ||||||
|         Type = (TransactionType)proto.Type, |         Type = (TransactionType)proto.Type, | ||||||
|         PayerWalletId = proto.HasPayerWalletId ? Guid.Parse(proto.PayerWalletId) : null, |         PayerWalletId = proto.PayerWalletId is not null ? Guid.Parse(proto.PayerWalletId) : null, | ||||||
|         PayeeWalletId = proto.HasPayeeWalletId ? Guid.Parse(proto.PayeeWalletId) : 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.EntityFrameworkCore.Storage; | ||||||
| using Microsoft.Extensions.Localization; | using Microsoft.Extensions.Localization; | ||||||
| using NATS.Client.Core; | using NATS.Client.Core; | ||||||
|  | using NATS.Client.JetStream; | ||||||
|  | using NATS.Net; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using AccountService = DysonNetwork.Pass.Account.AccountService; | using AccountService = DysonNetwork.Pass.Account.AccountService; | ||||||
|  |  | ||||||
| @@ -15,7 +17,7 @@ namespace DysonNetwork.Pass.Wallet; | |||||||
| public class PaymentService( | public class PaymentService( | ||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     WalletService wat, |     WalletService wat, | ||||||
|     PusherService.PusherServiceClient pusher, |     RingService.RingServiceClient pusher, | ||||||
|     IStringLocalizer<NotificationResource> localizer, |     IStringLocalizer<NotificationResource> localizer, | ||||||
|     INatsConnection nats |     INatsConnection nats | ||||||
| ) | ) | ||||||
| @@ -130,11 +132,11 @@ public class PaymentService( | |||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         Wallet? payerWallet = null, payeeWallet = null; |         Wallet? payerWallet = null, payeeWallet = null; | ||||||
|          |  | ||||||
|         if (payerWalletId.HasValue) |         if (payerWalletId.HasValue) | ||||||
|         { |         { | ||||||
|             payerWallet = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payerWalletId.Value); |             payerWallet = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payerWalletId.Value); | ||||||
|              |  | ||||||
|             var (payerPocket, isNewlyCreated) = |             var (payerPocket, isNewlyCreated) = | ||||||
|                 await wat.GetOrCreateWalletPocketAsync(payerWalletId.Value, currency); |                 await wat.GetOrCreateWalletPocketAsync(payerWalletId.Value, currency); | ||||||
|  |  | ||||||
| @@ -150,7 +152,7 @@ public class PaymentService( | |||||||
|         if (payeeWalletId.HasValue) |         if (payeeWalletId.HasValue) | ||||||
|         { |         { | ||||||
|             payeeWallet = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payeeWalletId.Value); |             payeeWallet = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payeeWalletId.Value); | ||||||
|              |  | ||||||
|             var (payeePocket, isNewlyCreated) = |             var (payeePocket, isNewlyCreated) = | ||||||
|                 await wat.GetOrCreateWalletPocketAsync(payeeWalletId.Value, currency, amount); |                 await wat.GetOrCreateWalletPocketAsync(payeeWalletId.Value, currency, amount); | ||||||
|  |  | ||||||
| @@ -166,10 +168,10 @@ public class PaymentService( | |||||||
|  |  | ||||||
|         if (!silent) |         if (!silent) | ||||||
|             await NotifyNewTransaction(transaction, payerWallet, payeeWallet); |             await NotifyNewTransaction(transaction, payerWallet, payeeWallet); | ||||||
|          |  | ||||||
|         return transaction; |         return transaction; | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     private async Task NotifyNewTransaction(Transaction transaction, Wallet? payerWallet, Wallet? payeeWallet) |     private async Task NotifyNewTransaction(Transaction transaction, Wallet? payerWallet, Wallet? payeeWallet) | ||||||
|     { |     { | ||||||
|         if (payerWallet is not null) |         if (payerWallet is not null) | ||||||
| @@ -192,18 +194,20 @@ public class PaymentService( | |||||||
|                     Notification = new PushNotification |                     Notification = new PushNotification | ||||||
|                     { |                     { | ||||||
|                         Topic = "wallets.transactions", |                         Topic = "wallets.transactions", | ||||||
|                         Title = transaction.Amount > 0 |                         Title = localizer["TransactionNewTitle", readableTransactionRemark], | ||||||
|                             ? localizer["TransactionNewBodyMinus", readableTransactionRemark] |                         Body = transaction.Amount > 0 | ||||||
|                             : localizer["TransactionNewBodyPlus", readableTransactionRemark], |                             ? localizer["TransactionNewBodyMinus", | ||||||
|                         Body = localizer["TransactionNewTitle", |                                 transaction.Amount.ToString(CultureInfo.InvariantCulture), | ||||||
|                             transaction.Amount.ToString(CultureInfo.InvariantCulture), |                                 transaction.Currency] | ||||||
|                             transaction.Currency], |                             : localizer["TransactionNewBodyPlus", | ||||||
|  |                                 transaction.Amount.ToString(CultureInfo.InvariantCulture), | ||||||
|  |                                 transaction.Currency], | ||||||
|                         IsSavable = true |                         IsSavable = true | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         if (payeeWallet is not null) |         if (payeeWallet is not null) | ||||||
|         { |         { | ||||||
|             var account = await db.Accounts |             var account = await db.Accounts | ||||||
| @@ -224,12 +228,14 @@ public class PaymentService( | |||||||
|                     Notification = new PushNotification |                     Notification = new PushNotification | ||||||
|                     { |                     { | ||||||
|                         Topic = "wallets.transactions", |                         Topic = "wallets.transactions", | ||||||
|                         Title = transaction.Amount > 0 |                         Title = localizer["TransactionNewTitle", readableTransactionRemark], | ||||||
|                             ? localizer["TransactionNewBodyPlus", readableTransactionRemark] |                         Body = transaction.Amount > 0 | ||||||
|                             : localizer["TransactionNewBodyMinus", readableTransactionRemark], |                             ? localizer["TransactionNewBodyPlus", | ||||||
|                         Body = localizer["TransactionNewTitle", |                                 transaction.Amount.ToString(CultureInfo.InvariantCulture), | ||||||
|                             transaction.Amount.ToString(CultureInfo.InvariantCulture), |                                 transaction.Currency] | ||||||
|                             transaction.Currency], |                             : localizer["TransactionNewBodyMinus", | ||||||
|  |                                 transaction.Amount.ToString(CultureInfo.InvariantCulture), | ||||||
|  |                                 transaction.Currency], | ||||||
|                         IsSavable = true |                         IsSavable = true | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| @@ -249,6 +255,27 @@ public class PaymentService( | |||||||
|             throw new InvalidOperationException("Order not found"); |             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) |         if (order.Status != OrderStatus.Unpaid) | ||||||
|         { |         { | ||||||
|             throw new InvalidOperationException($"Order is in invalid status: {order.Status}"); |             throw new InvalidOperationException($"Order is in invalid status: {order.Status}"); | ||||||
| @@ -267,7 +294,8 @@ public class PaymentService( | |||||||
|             order.Currency, |             order.Currency, | ||||||
|             order.Amount, |             order.Amount, | ||||||
|             order.Remarks ?? $"Payment for Order #{order.Id}", |             order.Remarks ?? $"Payment for Order #{order.Id}", | ||||||
|             type: TransactionType.Order); |             type: TransactionType.Order, | ||||||
|  |             silent: true); | ||||||
|  |  | ||||||
|         order.TransactionId = transaction.Id; |         order.TransactionId = transaction.Id; | ||||||
|         order.Transaction = transaction; |         order.Transaction = transaction; | ||||||
| @@ -277,16 +305,19 @@ public class PaymentService( | |||||||
|  |  | ||||||
|         await NotifyOrderPaid(order, payerWallet, order.PayeeWallet); |         await NotifyOrderPaid(order, payerWallet, order.PayeeWallet); | ||||||
|  |  | ||||||
|         await nats.PublishAsync(PaymentOrderEvent.Type, JsonSerializer.SerializeToUtf8Bytes(new PaymentOrderEvent |         await js.PublishAsync( | ||||||
|         { |             PaymentOrderEventBase.Type, | ||||||
|             OrderId = order.Id, |             GrpcTypeHelper.ConvertObjectToByteString(new PaymentOrderEvent | ||||||
|             WalletId = payerWallet.Id, |             { | ||||||
|             AccountId = payerWallet.AccountId, |                 OrderId = order.Id, | ||||||
|             AppIdentifier = order.AppIdentifier, |                 WalletId = payerWallet.Id, | ||||||
|             ProductIdentifier = order.ProductIdentifier, |                 AccountId = payerWallet.AccountId, | ||||||
|             Meta = order.Meta ?? [], |                 AppIdentifier = order.AppIdentifier, | ||||||
|             Status = (int)order.Status, |                 ProductIdentifier = order.ProductIdentifier, | ||||||
|         })); |                 Meta = order.Meta ?? [], | ||||||
|  |                 Status = (int)order.Status, | ||||||
|  |             }).ToByteArray() | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         return order; |         return order; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -68,7 +68,8 @@ public class SubscriptionRenewalJob( | |||||||
|                     null, |                     null, | ||||||
|                     WalletCurrency.GoldenPoint, |                     WalletCurrency.GoldenPoint, | ||||||
|                     subscription.FinalPrice, |                     subscription.FinalPrice, | ||||||
|                     appIdentifier: SubscriptionService.SubscriptionOrderIdentifier, |                     appIdentifier: "internal", | ||||||
|  |                     productIdentifier: subscription.Identifier, | ||||||
|                     meta: new Dictionary<string, object>() |                     meta: new Dictionary<string, object>() | ||||||
|                     { |                     { | ||||||
|                         ["subscription_id"] = subscription.Id.ToString(), |                         ["subscription_id"] = subscription.Id.ToString(), | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ public class SubscriptionService( | |||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     PaymentService payment, |     PaymentService payment, | ||||||
|     AccountService accounts, |     AccountService accounts, | ||||||
|     PusherService.PusherServiceClient pusher, |     RingService.RingServiceClient pusher, | ||||||
|     IStringLocalizer<NotificationResource> localizer, |     IStringLocalizer<NotificationResource> localizer, | ||||||
|     IConfiguration configuration, |     IConfiguration configuration, | ||||||
|     ICacheService cache, |     ICacheService cache, | ||||||
| @@ -229,8 +229,6 @@ public class SubscriptionService( | |||||||
|         return subscription; |         return subscription; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public const string SubscriptionOrderIdentifier = "solian.subscription.order"; |  | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Creates a subscription order for an unpaid or expired subscription. |     /// Creates a subscription order for an unpaid or expired subscription. | ||||||
|     /// If the subscription is active, it will extend its expiration date. |     /// If the subscription is active, it will extend its expiration date. | ||||||
| @@ -259,7 +257,8 @@ public class SubscriptionService( | |||||||
|             null, |             null, | ||||||
|             subscriptionInfo.Currency, |             subscriptionInfo.Currency, | ||||||
|             subscription.FinalPrice, |             subscription.FinalPrice, | ||||||
|             appIdentifier: SubscriptionOrderIdentifier, |             appIdentifier: "internal", | ||||||
|  |             productIdentifier: identifier, | ||||||
|             meta: new Dictionary<string, object>() |             meta: new Dictionary<string, object>() | ||||||
|             { |             { | ||||||
|                 ["subscription_id"] = subscription.Id.ToString(), |                 ["subscription_id"] = subscription.Id.ToString(), | ||||||
| @@ -270,8 +269,7 @@ public class SubscriptionService( | |||||||
|  |  | ||||||
|     public async Task<Subscription> HandleSubscriptionOrder(Order order) |     public async Task<Subscription> HandleSubscriptionOrder(Order order) | ||||||
|     { |     { | ||||||
|         if (order.AppIdentifier != SubscriptionOrderIdentifier || order.Status != OrderStatus.Paid || |         if (order.Status != OrderStatus.Paid || order.Meta?["subscription_id"] is not JsonElement subscriptionIdJson) | ||||||
|             order.Meta?["subscription_id"] is not JsonElement subscriptionIdJson) |  | ||||||
|             throw new InvalidOperationException("Invalid order."); |             throw new InvalidOperationException("Invalid order."); | ||||||
|  |  | ||||||
|         var subscriptionId = Guid.TryParse(subscriptionIdJson.ToString(), out var parsedSubscriptionId) |         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(); |         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||||
|  |  | ||||||
|         var query = db.PaymentTransactions.AsQueryable() |         var accountWallet = await db.Wallets.Where(w => w.AccountId == currentUser.Id).FirstOrDefaultAsync(); | ||||||
|             .Include(t => t.PayeeWallet) |         if (accountWallet is null) return NotFound(); | ||||||
|             .Include(t => t.PayerWallet) |  | ||||||
|             .Where(t => (t.PayeeWallet != null && t.PayeeWallet.AccountId == currentUser.Id) || |         var query = db.PaymentTransactions | ||||||
|                         (t.PayerWallet != null && t.PayerWallet.AccountId == currentUser.Id)); |             .Where(t => t.PayeeWalletId == accountWallet.Id || t.PayerWalletId == accountWallet.Id) | ||||||
|  |             .OrderByDescending(t => t.CreatedAt) | ||||||
|  |             .AsQueryable(); | ||||||
|  |  | ||||||
|         var transactionCount = await query.CountAsync(); |         var transactionCount = await query.CountAsync(); | ||||||
|  |         Response.Headers["X-Total"] = transactionCount.ToString(); | ||||||
|  |          | ||||||
|         var transactions = await query |         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) |             .Skip(offset) | ||||||
|             .Take(take) |             .Take(take) | ||||||
|             .OrderByDescending(t => t.CreatedAt) |             .OrderByDescending(t => t.CreatedAt) | ||||||
|             .ToListAsync(); |             .ToListAsync(); | ||||||
|  |  | ||||||
|         Response.Headers["X-Total"] = transactionCount.ToString(); |         return Ok(orders); | ||||||
|  |  | ||||||
|         return Ok(transactions); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public class WalletBalanceRequest |     public class WalletBalanceRequest | ||||||
|   | |||||||
| @@ -10,10 +10,7 @@ | |||||||
|   }, |   }, | ||||||
|   "AllowedHosts": "*", |   "AllowedHosts": "*", | ||||||
|   "ConnectionStrings": { |   "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", |     "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" |  | ||||||
|   }, |   }, | ||||||
|   "Authentication": { |   "Authentication": { | ||||||
|     "Schemes": { |     "Schemes": { | ||||||
| @@ -83,9 +80,7 @@ | |||||||
|   ], |   ], | ||||||
|   "Service": { |   "Service": { | ||||||
|     "Name": "DysonNetwork.Pass", |     "Name": "DysonNetwork.Pass", | ||||||
|     "Url": "https://localhost:7058", |     "Url": "https://localhost:7058" | ||||||
|     "ClientCert": "../Certificates/client.crt", |  | ||||||
|     "ClientKey": "../Certificates/client.key" |  | ||||||
|   }, |   }, | ||||||
|   "Etcd": { |   "Etcd": { | ||||||
|     "Insecure": true |     "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.Linq.Expressions; | ||||||
| using System.Reflection; | using System.Reflection; | ||||||
| using DysonNetwork.Pusher.Notification; | using DysonNetwork.Ring.Notification; | ||||||
| using DysonNetwork.Shared.Data; | using DysonNetwork.Shared.Data; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.EntityFrameworkCore.Design; | using Microsoft.EntityFrameworkCore.Design; | ||||||
| @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Query; | |||||||
| using NodaTime; | using NodaTime; | ||||||
| using Quartz; | using Quartz; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher; | namespace DysonNetwork.Ring; | ||||||
| 
 | 
 | ||||||
| public class AppDatabase( | public class AppDatabase( | ||||||
|     DbContextOptions<AppDatabase> options, |     DbContextOptions<AppDatabase> options, | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| namespace DysonNetwork.Pusher.Connection; | namespace DysonNetwork.Ring.Connection; | ||||||
| 
 | 
 | ||||||
| public class ClientTypeMiddleware(RequestDelegate next) | public class ClientTypeMiddleware(RequestDelegate next) | ||||||
| { | { | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| using System.Net.WebSockets; | using System.Net.WebSockets; | ||||||
| using DysonNetwork.Shared.Proto; | using DysonNetwork.Shared.Proto; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Connection; | namespace DysonNetwork.Ring.Connection; | ||||||
| 
 | 
 | ||||||
| public interface IWebSocketPacketHandler | public interface IWebSocketPacketHandler | ||||||
| { | { | ||||||
| @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authorization; | |||||||
| using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||||
| using Swashbuckle.AspNetCore.Annotations; | using Swashbuckle.AspNetCore.Annotations; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Connection; | namespace DysonNetwork.Ring.Connection; | ||||||
| 
 | 
 | ||||||
| [ApiController] | [ApiController] | ||||||
| public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext> logger) : ControllerBase | public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext> logger) : ControllerBase | ||||||
| @@ -4,7 +4,7 @@ using DysonNetwork.Shared.Proto; | |||||||
| using NodaTime; | using NodaTime; | ||||||
| using NodaTime.Serialization.SystemTextJson; | using NodaTime.Serialization.SystemTextJson; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Connection; | namespace DysonNetwork.Ring.Connection; | ||||||
| 
 | 
 | ||||||
| public class WebSocketPacket | public class WebSocketPacket | ||||||
| { | { | ||||||
| @@ -29,6 +29,7 @@ public class WebSocketPacket | |||||||
|         var json = System.Text.Encoding.UTF8.GetString(bytes); |         var json = System.Text.Encoding.UTF8.GetString(bytes); | ||||||
|         var jsonOpts = new JsonSerializerOptions |         var jsonOpts = new JsonSerializerOptions | ||||||
|         { |         { | ||||||
|  |             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||||
|             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, |             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||||
|             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, |             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||||
|         }; |         }; | ||||||
| @@ -48,6 +49,7 @@ public class WebSocketPacket | |||||||
| 
 | 
 | ||||||
|         var jsonOpts = new JsonSerializerOptions |         var jsonOpts = new JsonSerializerOptions | ||||||
|         { |         { | ||||||
|  |             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||||
|             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, |             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||||
|             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, |             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||||
|         }; |         }; | ||||||
| @@ -65,6 +67,7 @@ public class WebSocketPacket | |||||||
|     { |     { | ||||||
|         var jsonOpts = new JsonSerializerOptions |         var jsonOpts = new JsonSerializerOptions | ||||||
|         { |         { | ||||||
|  |             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||||
|             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, |             PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||||
|             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, |             DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, | ||||||
|         }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); |         }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||||
| @@ -1,27 +1,23 @@ | |||||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||||
| using System.Net.WebSockets; | using System.Net.WebSockets; | ||||||
| using dotnet_etcd.interfaces; |  | ||||||
| using DysonNetwork.Shared.Data; | using DysonNetwork.Shared.Data; | ||||||
| using DysonNetwork.Shared.Proto; | using DysonNetwork.Shared.Proto; | ||||||
| using Grpc.Core; | using Grpc.Core; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Connection; | namespace DysonNetwork.Ring.Connection; | ||||||
| 
 | 
 | ||||||
| public class WebSocketService | public class WebSocketService | ||||||
| { | { | ||||||
|     private readonly IConfiguration _configuration; |     private readonly IConfiguration _configuration; | ||||||
|     private readonly ILogger<WebSocketService> _logger; |     private readonly ILogger<WebSocketService> _logger; | ||||||
|     private readonly IEtcdClient _etcdClient; |  | ||||||
|     private readonly IDictionary<string, IWebSocketPacketHandler> _handlerMap; |     private readonly IDictionary<string, IWebSocketPacketHandler> _handlerMap; | ||||||
| 
 | 
 | ||||||
|     public WebSocketService( |     public WebSocketService( | ||||||
|         IEnumerable<IWebSocketPacketHandler> handlers, |         IEnumerable<IWebSocketPacketHandler> handlers, | ||||||
|         IEtcdClient etcdClient, |  | ||||||
|         ILogger<WebSocketService> logger, |         ILogger<WebSocketService> logger, | ||||||
|         IConfiguration configuration |         IConfiguration configuration | ||||||
|     ) |     ) | ||||||
|     { |     { | ||||||
|         _etcdClient = etcdClient; |  | ||||||
|         _logger = logger; |         _logger = logger; | ||||||
|         _configuration = configuration; |         _configuration = configuration; | ||||||
|         _handlerMap = handlers.ToDictionary(h => h.PacketType); |         _handlerMap = handlers.ToDictionary(h => h.PacketType); | ||||||
| @@ -59,8 +55,10 @@ public class WebSocketService | |||||||
|         } |         } | ||||||
|         catch (Exception ex) |         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(); |         data.Cts.Cancel(); | ||||||
|         ActiveConnections.TryRemove(key, out _); |         ActiveConnections.TryRemove(key, out _); | ||||||
|     } |     } | ||||||
| @@ -140,45 +138,24 @@ public class WebSocketService | |||||||
|         { |         { | ||||||
|             try |             try | ||||||
|             { |             { | ||||||
|                 // Get the service URL from etcd for the specified endpoint |                 var serviceUrl = "https://" + packet.Endpoint; | ||||||
|                 var serviceKey = $"/services/{packet.Endpoint}"; |  | ||||||
|                 var response = await _etcdClient.GetAsync(serviceKey); |  | ||||||
| 
 | 
 | ||||||
|                 if (response.Kvs.Count > 0) |                 var callInvoker = GrpcClientHelper.CreateCallInvoker(serviceUrl); | ||||||
|  |                 var client = new RingHandlerService.RingHandlerServiceClient(callInvoker); | ||||||
|  | 
 | ||||||
|  |                 try | ||||||
|                 { |                 { | ||||||
|                     var serviceUrl = response.Kvs[0].Value.ToStringUtf8(); |                     await client.ReceiveWebSocketPacketAsync(new ReceiveWebSocketPacketRequest | ||||||
| 
 |  | ||||||
|                     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); |  | ||||||
| 
 |  | ||||||
|                     try |  | ||||||
|                     { |                     { | ||||||
|                         await client.ReceiveWebSocketPacketAsync(new ReceiveWebSocketPacketRequest |                         Account = currentUser, | ||||||
|                         { |                         DeviceId = deviceId, | ||||||
|                             Account = currentUser, |                         Packet = packet.ToProtoValue() | ||||||
|                             DeviceId = deviceId, |                     }); | ||||||
|                             Packet = packet.ToProtoValue() |                 } | ||||||
|                         }); |                 catch (RpcException ex) | ||||||
|                     } |                 { | ||||||
|                     catch (RpcException ex) |                     _logger.LogError(ex, $"Error forwarding packet to endpoint: {packet.Endpoint}"); | ||||||
|                     { |  | ||||||
|                         _logger.LogError(ex, $"Error forwarding packet to endpoint: {packet.Endpoint}"); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     return; |  | ||||||
|                 } |                 } | ||||||
| 
 |  | ||||||
|                 _logger.LogWarning($"No service registered for endpoint: {packet.Endpoint}"); |  | ||||||
|             } |             } | ||||||
|             catch (Exception ex) |             catch (Exception ex) | ||||||
|             { |             { | ||||||
| @@ -197,4 +174,4 @@ public class WebSocketService | |||||||
|             CancellationToken.None |             CancellationToken.None | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										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> |         <Nullable>enable</Nullable> | ||||||
|         <ImplicitUsings>enable</ImplicitUsings> |         <ImplicitUsings>enable</ImplicitUsings> | ||||||
|         <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> |         <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> | ||||||
|  |         <RootNamespace>DysonNetwork.Pusher</RootNamespace> | ||||||
|     </PropertyGroup> |     </PropertyGroup> | ||||||
| 
 | 
 | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
| @@ -41,6 +42,7 @@ | |||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
| 
 | 
 | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|  |       <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||||
|       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> |       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
| 
 | 
 | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| using MailKit.Net.Smtp; | using MailKit.Net.Smtp; | ||||||
| using MimeKit; | using MimeKit; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Email; | namespace DysonNetwork.Ring.Email; | ||||||
| 
 | 
 | ||||||
| public class EmailServiceConfiguration | public class EmailServiceConfiguration | ||||||
| { | { | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| // <auto-generated /> | // <auto-generated /> | ||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using DysonNetwork.Pusher; | using DysonNetwork.Ring; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
| @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | |||||||
| 
 | 
 | ||||||
| #nullable disable | #nullable disable | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Migrations | namespace DysonNetwork.Ring.Migrations | ||||||
| { | { | ||||||
|     [DbContext(typeof(AppDatabase))] |     [DbContext(typeof(AppDatabase))] | ||||||
|     [Migration("20250713122638_InitialMigration")] |     [Migration("20250713122638_InitialMigration")] | ||||||
| @@ -27,7 +27,7 @@ namespace DysonNetwork.Pusher.Migrations | |||||||
| 
 | 
 | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
| 
 | 
 | ||||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.Notification", b => |             modelBuilder.Entity("DysonNetwork.Ring.Notification.Notification", b => | ||||||
|                 { |                 { | ||||||
|                     b.Property<Guid>("Id") |                     b.Property<Guid>("Id") | ||||||
|                         .ValueGeneratedOnAdd() |                         .ValueGeneratedOnAdd() | ||||||
| @@ -89,7 +89,7 @@ namespace DysonNetwork.Pusher.Migrations | |||||||
|                     b.ToTable("notifications", (string)null); |                     b.ToTable("notifications", (string)null); | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.PushSubscription", b => |             modelBuilder.Entity("DysonNetwork.Ring.Notification.PushSubscription", b => | ||||||
|                 { |                 { | ||||||
|                     b.Property<Guid>("Id") |                     b.Property<Guid>("Id") | ||||||
|                         .ValueGeneratedOnAdd() |                         .ValueGeneratedOnAdd() | ||||||
| @@ -5,7 +5,7 @@ using NodaTime; | |||||||
| 
 | 
 | ||||||
| #nullable disable | #nullable disable | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Migrations | namespace DysonNetwork.Ring.Migrations | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     public partial class InitialMigration : Migration |     public partial class InitialMigration : Migration | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| // <auto-generated /> | // <auto-generated /> | ||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using DysonNetwork.Pusher; | using DysonNetwork.Ring; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
| using Microsoft.EntityFrameworkCore.Migrations; | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
| @@ -11,7 +11,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | |||||||
| 
 | 
 | ||||||
| #nullable disable | #nullable disable | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Migrations | namespace DysonNetwork.Ring.Migrations | ||||||
| { | { | ||||||
|     [DbContext(typeof(AppDatabase))] |     [DbContext(typeof(AppDatabase))] | ||||||
|     [Migration("20250724070546_UpdateNotificationMeta")] |     [Migration("20250724070546_UpdateNotificationMeta")] | ||||||
| @@ -27,7 +27,7 @@ namespace DysonNetwork.Pusher.Migrations | |||||||
| 
 | 
 | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
| 
 | 
 | ||||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.Notification", b => |             modelBuilder.Entity("DysonNetwork.Ring.Notification.Notification", b => | ||||||
|                 { |                 { | ||||||
|                     b.Property<Guid>("Id") |                     b.Property<Guid>("Id") | ||||||
|                         .ValueGeneratedOnAdd() |                         .ValueGeneratedOnAdd() | ||||||
| @@ -90,7 +90,7 @@ namespace DysonNetwork.Pusher.Migrations | |||||||
|                     b.ToTable("notifications", (string)null); |                     b.ToTable("notifications", (string)null); | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.PushSubscription", b => |             modelBuilder.Entity("DysonNetwork.Ring.Notification.PushSubscription", b => | ||||||
|                 { |                 { | ||||||
|                     b.Property<Guid>("Id") |                     b.Property<Guid>("Id") | ||||||
|                         .ValueGeneratedOnAdd() |                         .ValueGeneratedOnAdd() | ||||||
| @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; | |||||||
| 
 | 
 | ||||||
| #nullable disable | #nullable disable | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Migrations | namespace DysonNetwork.Ring.Migrations | ||||||
| { | { | ||||||
|     /// <inheritdoc /> |     /// <inheritdoc /> | ||||||
|     public partial class UpdateNotificationMeta : Migration |     public partial class UpdateNotificationMeta : Migration | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| // <auto-generated /> | // <auto-generated /> | ||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using DysonNetwork.Pusher; | using DysonNetwork.Ring; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | using Microsoft.EntityFrameworkCore.Infrastructure; | ||||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||||
| @@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | |||||||
| 
 | 
 | ||||||
| #nullable disable | #nullable disable | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Migrations | namespace DysonNetwork.Ring.Migrations | ||||||
| { | { | ||||||
|     [DbContext(typeof(AppDatabase))] |     [DbContext(typeof(AppDatabase))] | ||||||
|     partial class AppDatabaseModelSnapshot : ModelSnapshot |     partial class AppDatabaseModelSnapshot : ModelSnapshot | ||||||
| @@ -24,7 +24,7 @@ namespace DysonNetwork.Pusher.Migrations | |||||||
| 
 | 
 | ||||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); |             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||||
| 
 | 
 | ||||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.Notification", b => |             modelBuilder.Entity("DysonNetwork.Ring.Notification.Notification", b => | ||||||
|                 { |                 { | ||||||
|                     b.Property<Guid>("Id") |                     b.Property<Guid>("Id") | ||||||
|                         .ValueGeneratedOnAdd() |                         .ValueGeneratedOnAdd() | ||||||
| @@ -87,7 +87,7 @@ namespace DysonNetwork.Pusher.Migrations | |||||||
|                     b.ToTable("notifications", (string)null); |                     b.ToTable("notifications", (string)null); | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|             modelBuilder.Entity("DysonNetwork.Pusher.Notification.PushSubscription", b => |             modelBuilder.Entity("DysonNetwork.Ring.Notification.PushSubscription", b => | ||||||
|                 { |                 { | ||||||
|                     b.Property<Guid>("Id") |                     b.Property<Guid>("Id") | ||||||
|                         .ValueGeneratedOnAdd() |                         .ValueGeneratedOnAdd() | ||||||
| @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; | |||||||
| using DysonNetwork.Shared.Data; | using DysonNetwork.Shared.Data; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Notification; | namespace DysonNetwork.Ring.Notification; | ||||||
| 
 | 
 | ||||||
| public class Notification : ModelBase | public class Notification : ModelBase | ||||||
| { | { | ||||||
| @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc; | |||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Notification; | namespace DysonNetwork.Ring.Notification; | ||||||
| 
 | 
 | ||||||
| [ApiController] | [ApiController] | ||||||
| [Route("/api/notifications")] | [Route("/api/notifications")] | ||||||
| @@ -3,7 +3,7 @@ using EFCore.BulkExtensions; | |||||||
| using NodaTime; | using NodaTime; | ||||||
| using Quartz; | using Quartz; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Notification; | namespace DysonNetwork.Ring.Notification; | ||||||
| 
 | 
 | ||||||
| public class NotificationFlushHandler(AppDatabase db) : IFlushHandler<Notification> | public class NotificationFlushHandler(AppDatabase db) : IFlushHandler<Notification> | ||||||
| { | { | ||||||
| @@ -1,13 +1,13 @@ | |||||||
| using CorePush.Apple; | using CorePush.Apple; | ||||||
| using CorePush.Firebase; | using CorePush.Firebase; | ||||||
| using DysonNetwork.Pusher.Connection; | using DysonNetwork.Ring.Connection; | ||||||
| using DysonNetwork.Pusher.Services; | using DysonNetwork.Ring.Services; | ||||||
| using DysonNetwork.Shared.Proto; | using DysonNetwork.Shared.Proto; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | 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 | public class PushService | ||||||
| { | { | ||||||
| @@ -3,7 +3,7 @@ using DysonNetwork.Shared.Data; | |||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Notification; | namespace DysonNetwork.Ring.Notification; | ||||||
| 
 | 
 | ||||||
| public enum PushProvider | public enum PushProvider | ||||||
| { | { | ||||||
| @@ -1,19 +1,18 @@ | |||||||
| using DysonNetwork.Pusher; | using DysonNetwork.Ring; | ||||||
| using DysonNetwork.Pusher.Startup; | using DysonNetwork.Ring.Startup; | ||||||
| using DysonNetwork.Shared.Auth; | using DysonNetwork.Shared.Auth; | ||||||
| using DysonNetwork.Shared.Http; | using DysonNetwork.Shared.Http; | ||||||
| using DysonNetwork.Shared.Registry; | using DysonNetwork.Shared.Registry; | ||||||
| using DysonNetwork.Shared.Stream; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| 
 | 
 | ||||||
| var builder = WebApplication.CreateBuilder(args); | var builder = WebApplication.CreateBuilder(args); | ||||||
| 
 | 
 | ||||||
|  | builder.AddServiceDefaults(); | ||||||
|  | 
 | ||||||
| // Configure Kestrel and server options | // Configure Kestrel and server options | ||||||
| builder.ConfigureAppKestrel(builder.Configuration); | builder.ConfigureAppKestrel(builder.Configuration); | ||||||
| 
 | 
 | ||||||
| // Add application services | // Add application services | ||||||
| builder.Services.AddRegistryService(builder.Configuration); |  | ||||||
| builder.Services.AddStreamConnection(builder.Configuration); |  | ||||||
| builder.Services.AddAppServices(builder.Configuration); | builder.Services.AddAppServices(builder.Configuration); | ||||||
| builder.Services.AddAppRateLimiting(); | builder.Services.AddAppRateLimiting(); | ||||||
| builder.Services.AddAppAuthentication(); | builder.Services.AddAppAuthentication(); | ||||||
| @@ -32,6 +31,8 @@ builder.Services.AddAppScheduledJobs(); | |||||||
| 
 | 
 | ||||||
| var app = builder.Build(); | var app = builder.Build(); | ||||||
| 
 | 
 | ||||||
|  | app.MapDefaultEndpoints(); | ||||||
|  | 
 | ||||||
| // Run database migrations | // Run database migrations | ||||||
| using (var scope = app.Services.CreateScope()) | using (var scope = app.Services.CreateScope()) | ||||||
| { | { | ||||||
| @@ -1,19 +1,18 @@ | |||||||
| using DysonNetwork.Pusher.Connection; | using DysonNetwork.Ring.Connection; | ||||||
| using DysonNetwork.Pusher.Email; | using DysonNetwork.Ring.Email; | ||||||
| using DysonNetwork.Pusher.Notification; | using DysonNetwork.Ring.Notification; | ||||||
| using DysonNetwork.Shared.Proto; | using DysonNetwork.Shared.Proto; | ||||||
| using DysonNetwork.Shared.Registry; |  | ||||||
| using Google.Protobuf.WellKnownTypes; | using Google.Protobuf.WellKnownTypes; | ||||||
| using Grpc.Core; | using Grpc.Core; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Services; | namespace DysonNetwork.Ring.Services; | ||||||
| 
 | 
 | ||||||
| public class PusherServiceGrpc( | public class RingServiceGrpc( | ||||||
|     QueueService queueService, |     QueueService queueService, | ||||||
|     WebSocketService websocket, |     WebSocketService websocket, | ||||||
|     PushService pushService |     PushService pushService | ||||||
| ) : PusherService.PusherServiceBase | ) : RingService.RingServiceBase | ||||||
| { | { | ||||||
|     public override async Task<Empty> SendEmail(SendEmailRequest request, ServerCallContext context) |     public override async Task<Empty> SendEmail(SendEmailRequest request, ServerCallContext context) | ||||||
|     { |     { | ||||||
| @@ -1,12 +1,15 @@ | |||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using DysonNetwork.Pusher.Email; | using DysonNetwork.Ring.Email; | ||||||
| using DysonNetwork.Pusher.Notification; | using DysonNetwork.Ring.Notification; | ||||||
| using DysonNetwork.Shared.Proto; | using DysonNetwork.Shared.Proto; | ||||||
| using DysonNetwork.Shared.Registry; | using DysonNetwork.Shared.Stream; | ||||||
| using Google.Protobuf; | using Google.Protobuf; | ||||||
| using NATS.Client.Core; | 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( | public class QueueBackgroundService( | ||||||
|     INatsConnection nats, |     INatsConnection nats, | ||||||
| @@ -16,8 +19,8 @@ public class QueueBackgroundService( | |||||||
| ) | ) | ||||||
|     : BackgroundService |     : BackgroundService | ||||||
| { | { | ||||||
|     public const string QueueName = "pusher.queue"; |     public const string QueueName = "pusher_queue"; | ||||||
|     public const string QueueGroup = "pusher.workers"; |     private const string QueueGroup = "pusher_workers"; | ||||||
|     private readonly int _consumerCount = configuration.GetValue<int?>("ConsumerCount") ?? Environment.ProcessorCount; |     private readonly int _consumerCount = configuration.GetValue<int?>("ConsumerCount") ?? Environment.ProcessorCount; | ||||||
|     private readonly List<Task> _consumerTasks = []; |     private readonly List<Task> _consumerTasks = []; | ||||||
| 
 | 
 | ||||||
| @@ -36,11 +39,16 @@ public class QueueBackgroundService( | |||||||
|     private async Task RunConsumerAsync(CancellationToken stoppingToken) |     private async Task RunConsumerAsync(CancellationToken stoppingToken) | ||||||
|     { |     { | ||||||
|         logger.LogInformation("Queue consumer started"); |         logger.LogInformation("Queue consumer started"); | ||||||
|  |         var js = nats.CreateJetStreamContext(); | ||||||
| 
 | 
 | ||||||
|         await foreach (var msg in nats.SubscribeAsync<byte[]>( |         await js.EnsureStreamCreated("pusher_events", [QueueName]); | ||||||
|                            QueueName, |          | ||||||
|                            queueGroup: QueueGroup, |         var consumer = await js.CreateOrUpdateConsumerAsync( | ||||||
|                            cancellationToken: stoppingToken)) |             "pusher_events",  | ||||||
|  |             new ConsumerConfig(QueueGroup), // durable consumer | ||||||
|  |             cancellationToken: stoppingToken); | ||||||
|  | 
 | ||||||
|  |         await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken)) | ||||||
|         { |         { | ||||||
|             try |             try | ||||||
|             { |             { | ||||||
| @@ -48,10 +56,12 @@ public class QueueBackgroundService( | |||||||
|                 if (message is not null) |                 if (message is not null) | ||||||
|                 { |                 { | ||||||
|                     await ProcessMessageAsync(msg, message, stoppingToken); |                     await ProcessMessageAsync(msg, message, stoppingToken); | ||||||
|  |                     await msg.AckAsync(cancellationToken: stoppingToken); | ||||||
|                 } |                 } | ||||||
|                 else |                 else | ||||||
|                 { |                 { | ||||||
|                     logger.LogWarning($"Invalid message format for {msg.Subject}"); |                     logger.LogWarning($"Invalid message format for {msg.Subject}"); | ||||||
|  |                     await msg.AckAsync(cancellationToken: stoppingToken); // Acknowledge invalid messages to avoid redelivery | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) |             catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) | ||||||
| @@ -62,41 +72,31 @@ public class QueueBackgroundService( | |||||||
|             catch (Exception ex) |             catch (Exception ex) | ||||||
|             { |             { | ||||||
|                 logger.LogError(ex, "Error in queue consumer"); |                 logger.LogError(ex, "Error in queue consumer"); | ||||||
|                 // Add a small delay to prevent tight error loops |                 await msg.NakAsync(cancellationToken: stoppingToken); | ||||||
|                 await Task.Delay(1000, stoppingToken); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async ValueTask ProcessMessageAsync(NatsMsg<byte[]> rawMsg, QueueMessage message, |     private async ValueTask ProcessMessageAsync(NatsJSMsg<byte[]> rawMsg, QueueMessage message, | ||||||
|         CancellationToken cancellationToken) |         CancellationToken cancellationToken) | ||||||
|     { |     { | ||||||
|         using var scope = serviceProvider.CreateScope(); |         using var scope = serviceProvider.CreateScope(); | ||||||
| 
 | 
 | ||||||
|         logger.LogDebug("Processing message of type {MessageType}", message.Type); |         logger.LogDebug("Processing message of type {MessageType}", message.Type); | ||||||
| 
 | 
 | ||||||
|         try |         switch (message.Type) | ||||||
|         { |         { | ||||||
|             switch (message.Type) |             case QueueMessageType.Email: | ||||||
|             { |                 await ProcessEmailMessageAsync(message, scope); | ||||||
|                 case QueueMessageType.Email: |                 break; | ||||||
|                     await ProcessEmailMessageAsync(message, scope); |  | ||||||
|                     break; |  | ||||||
| 
 | 
 | ||||||
|                 case QueueMessageType.PushNotification: |             case QueueMessageType.PushNotification: | ||||||
|                     await ProcessPushNotificationMessageAsync(message, scope, cancellationToken); |                 await ProcessPushNotificationMessageAsync(message, scope, cancellationToken); | ||||||
|                     break; |                 break; | ||||||
| 
 | 
 | ||||||
|                 default: |             default: | ||||||
|                     logger.LogWarning("Unknown message type: {MessageType}", message.Type); |                 logger.LogWarning("Unknown message type: {MessageType}", message.Type); | ||||||
|                     break; |                 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 |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -126,16 +126,8 @@ public class QueueBackgroundService( | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         try |         logger.LogDebug("Processing push notification for account {AccountId}", notification.AccountId); | ||||||
|         { |         await pushService.DeliverPushNotification(notification, cancellationToken); | ||||||
|             logger.LogDebug("Processing push notification for account {AccountId}", notification.AccountId); |         logger.LogDebug("Successfully processed 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 System.Text.Json; | ||||||
| using DysonNetwork.Shared.Proto; | using DysonNetwork.Shared.Proto; | ||||||
| using NATS.Client.Core; | using NATS.Client.Core; | ||||||
|  | using NATS.Client.JetStream; | ||||||
|  | using NATS.Net; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Services; | namespace DysonNetwork.Ring.Services; | ||||||
| 
 | 
 | ||||||
| public class QueueService(INatsConnection nats) | public class QueueService(INatsConnection nats) | ||||||
| { | { | ||||||
| @@ -20,7 +22,8 @@ public class QueueService(INatsConnection nats) | |||||||
|             }) |             }) | ||||||
|         }; |         }; | ||||||
|         var rawMessage = GrpcTypeHelper.ConvertObjectToByteString(message).ToByteArray(); |         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) |     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) |             Data = JsonSerializer.Serialize(notification) | ||||||
|         }; |         }; | ||||||
|         var rawMessage = GrpcTypeHelper.ConvertObjectToByteString(message).ToByteArray(); |         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 System.Net; | ||||||
| using DysonNetwork.Pusher.Services; | using DysonNetwork.Ring.Services; | ||||||
| using DysonNetwork.Shared.Http; | using DysonNetwork.Shared.Http; | ||||||
| using Microsoft.AspNetCore.HttpOverrides; | using Microsoft.AspNetCore.HttpOverrides; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Startup; | namespace DysonNetwork.Ring.Startup; | ||||||
| 
 | 
 | ||||||
| public static class ApplicationConfiguration | public static class ApplicationConfiguration | ||||||
| { | { | ||||||
| @@ -39,7 +39,7 @@ public static class ApplicationConfiguration | |||||||
|      |      | ||||||
|     public static WebApplication ConfigureGrpcServices(this WebApplication app) |     public static WebApplication ConfigureGrpcServices(this WebApplication app) | ||||||
|     { |     { | ||||||
|         app.MapGrpcService<PusherServiceGrpc>(); |         app.MapGrpcService<RingServiceGrpc>(); | ||||||
|          |          | ||||||
|         return app; |         return app; | ||||||
|     } |     } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| using DysonNetwork.Pusher.Notification; | using DysonNetwork.Ring.Notification; | ||||||
| using Quartz; | using Quartz; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Startup; | namespace DysonNetwork.Ring.Startup; | ||||||
| 
 | 
 | ||||||
| public static class ScheduledJobsConfiguration | public static class ScheduledJobsConfiguration | ||||||
| { | { | ||||||
| @@ -1,11 +1,12 @@ | |||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
|  | using System.Text.Json.Serialization; | ||||||
| using System.Threading.RateLimiting; | using System.Threading.RateLimiting; | ||||||
| using CorePush.Apple; | using CorePush.Apple; | ||||||
| using CorePush.Firebase; | using CorePush.Firebase; | ||||||
| using DysonNetwork.Pusher.Connection; | using DysonNetwork.Ring.Connection; | ||||||
| using DysonNetwork.Pusher.Email; | using DysonNetwork.Ring.Email; | ||||||
| using DysonNetwork.Pusher.Notification; | using DysonNetwork.Ring.Notification; | ||||||
| using DysonNetwork.Pusher.Services; | using DysonNetwork.Ring.Services; | ||||||
| using DysonNetwork.Shared.Cache; | using DysonNetwork.Shared.Cache; | ||||||
| using Microsoft.AspNetCore.RateLimiting; | using Microsoft.AspNetCore.RateLimiting; | ||||||
| using Microsoft.OpenApi.Models; | using Microsoft.OpenApi.Models; | ||||||
| @@ -13,18 +14,13 @@ using NodaTime; | |||||||
| using NodaTime.Serialization.SystemTextJson; | using NodaTime.Serialization.SystemTextJson; | ||||||
| using StackExchange.Redis; | using StackExchange.Redis; | ||||||
| 
 | 
 | ||||||
| namespace DysonNetwork.Pusher.Startup; | namespace DysonNetwork.Ring.Startup; | ||||||
| 
 | 
 | ||||||
| public static class ServiceCollectionExtensions | public static class ServiceCollectionExtensions | ||||||
| { | { | ||||||
|     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) |     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) | ||||||
|     { |     { | ||||||
|         services.AddDbContext<AppDatabase>(); |         services.AddDbContext<AppDatabase>(); | ||||||
|         services.AddSingleton<IConnectionMultiplexer>(_ => |  | ||||||
|         { |  | ||||||
|             var connection = configuration.GetConnectionString("FastRetrieve")!; |  | ||||||
|             return ConnectionMultiplexer.Connect(connection); |  | ||||||
|         }); |  | ||||||
|         services.AddSingleton<IClock>(SystemClock.Instance); |         services.AddSingleton<IClock>(SystemClock.Instance); | ||||||
|         services.AddHttpContextAccessor(); |         services.AddHttpContextAccessor(); | ||||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); |         services.AddSingleton<ICacheService, CacheServiceRedis>(); | ||||||
| @@ -43,11 +39,12 @@ public static class ServiceCollectionExtensions | |||||||
|         services.AddGrpc(); |         services.AddGrpc(); | ||||||
| 
 | 
 | ||||||
|         // Register gRPC services |         // Register gRPC services | ||||||
|         services.AddScoped<PusherServiceGrpc>(); |         services.AddScoped<RingServiceGrpc>(); | ||||||
| 
 | 
 | ||||||
|         // Register OIDC services |         // Register OIDC services | ||||||
|         services.AddControllers().AddJsonOptions(options => |         services.AddControllers().AddJsonOptions(options => | ||||||
|         { |         { | ||||||
|  |             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; |             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; |             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||||
| 
 | 
 | ||||||
| @@ -86,7 +83,7 @@ public static class ServiceCollectionExtensions | |||||||
|             options.SwaggerDoc("v1", new OpenApiInfo |             options.SwaggerDoc("v1", new OpenApiInfo | ||||||
|             { |             { | ||||||
|                 Version = "v1", |                 Version = "v1", | ||||||
|                 Title = "Dyson Pusher", |                 Title = "Dyson Ring", | ||||||
|                 Description = "The pusher service of the Dyson Network. Mainly handling emailing, notifications and websockets.", |                 Description = "The pusher service of the Dyson Network. Mainly handling emailing, notifications and websockets.", | ||||||
|                 TermsOfService = new Uri("https://solsynth.dev/terms"), |                 TermsOfService = new Uri("https://solsynth.dev/terms"), | ||||||
|                 License = new OpenApiLicense |                 License = new OpenApiLicense | ||||||
| @@ -9,10 +9,7 @@ | |||||||
|   }, |   }, | ||||||
|   "AllowedHosts": "*", |   "AllowedHosts": "*", | ||||||
|   "ConnectionStrings": { |   "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", |     "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" |  | ||||||
|   }, |   }, | ||||||
|   "Notifications": { |   "Notifications": { | ||||||
|     "Push": { |     "Push": { | ||||||
| @@ -44,10 +41,8 @@ | |||||||
|     "::1" |     "::1" | ||||||
|   ], |   ], | ||||||
|   "Service": { |   "Service": { | ||||||
|     "Name": "DysonNetwork.Pusher", |     "Name": "DysonNetwork.Ring", | ||||||
|     "Url": "https://localhost:7259", |     "Url": "https://localhost:7259" | ||||||
|     "ClientCert": "../Certificates/client.crt", |  | ||||||
|     "ClientKey": "../Certificates/client.key" |  | ||||||
|   }, |   }, | ||||||
|   "Etcd": { |   "Etcd": { | ||||||
|     "Insecure": true |     "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.Proto; | ||||||
|  | using DysonNetwork.Shared.Registry; | ||||||
| using Microsoft.Extensions.Configuration; | using Microsoft.Extensions.Configuration; | ||||||
| using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||||
|  |  | ||||||
| @@ -11,33 +11,7 @@ public static class DysonAuthStartup | |||||||
|         this IServiceCollection services |         this IServiceCollection services | ||||||
|     ) |     ) | ||||||
|     { |     { | ||||||
|         services.AddSingleton<AuthService.AuthServiceClient>(sp => |         services.AddAuthService(); | ||||||
|         { |  | ||||||
|             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.AddAuthentication(options => |         services.AddAuthentication(options => | ||||||
|             { |             { | ||||||
|   | |||||||
| @@ -210,6 +210,7 @@ public class CacheServiceRedis : ICacheService | |||||||
|                 Modifiers = { JsonExtensions.UnignoreAllProperties() }, |                 Modifiers = { JsonExtensions.UnignoreAllProperties() }, | ||||||
|             }, |             }, | ||||||
|             ReferenceHandler = ReferenceHandler.Preserve, |             ReferenceHandler = ReferenceHandler.Preserve, | ||||||
|  |             NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||||
|         }; |         }; | ||||||
|         _jsonOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); |         _jsonOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||||
|         _jsonOptions.PropertyNameCaseInsensitive = true; |         _jsonOptions.PropertyNameCaseInsensitive = true; | ||||||
|   | |||||||
| @@ -7,11 +7,11 @@ | |||||||
|     </PropertyGroup> |     </PropertyGroup> | ||||||
|  |  | ||||||
|     <ItemGroup> |     <ItemGroup> | ||||||
|         <PackageReference Include="dotnet-etcd" Version="8.0.1" /> |  | ||||||
|         <PackageReference Include="Google.Api.CommonProtos" Version="2.17.0" /> |         <PackageReference Include="Google.Api.CommonProtos" Version="2.17.0" /> | ||||||
|         <PackageReference Include="Google.Protobuf" Version="3.31.1" /> |         <PackageReference Include="Google.Protobuf" Version="3.31.1" /> | ||||||
|         <PackageReference Include="Google.Protobuf.Tools" Version="3.31.1" /> |         <PackageReference Include="Google.Protobuf.Tools" Version="3.31.1" /> | ||||||
|         <PackageReference Include="Grpc" Version="2.46.6" /> |         <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.Net.Client" Version="2.71.0" /> | ||||||
|         <PackageReference Include="Grpc.Tools" Version="2.72.0"> |         <PackageReference Include="Grpc.Tools" Version="2.72.0"> | ||||||
|             <PrivateAssets>all</PrivateAssets> |             <PrivateAssets>all</PrivateAssets> | ||||||
| @@ -21,14 +21,12 @@ | |||||||
|         <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" /> |         <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" /> | ||||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" /> |         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" /> | ||||||
|         <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" /> |         <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" /> | ||||||
|         <PackageReference Include="NATS.Client.Core" Version="2.6.6" /> |         <PackageReference Include="NATS.Net" Version="2.6.8" /> | ||||||
|         <PackageReference Include="NetTopologySuite" Version="2.6.0" /> |  | ||||||
|         <PackageReference Include="NodaTime" Version="3.2.2" /> |         <PackageReference Include="NodaTime" Version="3.2.2" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> |         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> |         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> | ||||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> |         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> | ||||||
|         <PackageReference Include="OpenGraph-Net" Version="4.0.1" /> |         <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="System.Net.Http" Version="4.3.4" /> | ||||||
|         <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" /> |         <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" /> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
| @@ -41,4 +39,8 @@ | |||||||
|       <Folder Include="Error\" /> |       <Folder Include="Error\" /> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|  |     <ItemGroup> | ||||||
|  |       <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||||
|  |     </ItemGroup> | ||||||
|  |  | ||||||
| </Project> | </Project> | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user