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