Compare commits
	
		
			418 Commits
		
	
	
		
			f1867e7916
			...
			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 | |||
| 4a27794ccc | |||
| d2f5ba36ab | |||
| 0117fdf084 | |||
| 02680d224a | |||
| 68bfdebcbd | |||
| 54907eede1 | |||
| a21d19c3ef | |||
| df732616d5 | |||
| 79a31ae060 | |||
| 6eacfcd8f2 | |||
| 5e328509bd | |||
| 9c078db564 | |||
| ddd109c77c | |||
| 3ee04d0b24 | |||
| 7f110313e9 | |||
| bc2e87c56f | |||
| d7271a2d11 | |||
| c57d65db67 | |||
| edf3aab173 | |||
| 352746a141 | |||
| 216c72ea36 | |||
| d0723b366b | |||
| fb6721cb1b | |||
| 9fcb169c94 | |||
| 572874431d | |||
| f595ac8001 | |||
| 18674e0e1d | |||
| da4c4d3a84 | |||
| aec01b117d | |||
| d299c32e35 | |||
| 344007af66 | |||
| d4de5aeac2 | |||
| 8ce5ba50f4 | |||
| 5a44952b27 | |||
| c30946daf6 | |||
| 0221d7b294 | |||
| c44b0b64c3 | |||
| 442ee3bcfd | |||
| 081815c512 | |||
| eab2a388ae | |||
| 5f7ab49abb | |||
| 4ff89173b2 | |||
| f2052410c7 | |||
| 83a49be725 | |||
| 9b205a73fd | |||
| d5157eb7e3 | |||
| 75c92c51db | |||
| 915054fce0 | |||
| 63653680ba | |||
| 84c4df6620 | |||
| 8c748fd57a | |||
| 4684550ebf | |||
| 51db08f374 | |||
| 9f38a288b9 | |||
| 75a975049c | |||
| f8c35c0350 | |||
| d9a5fed77f | |||
| 7cb14940d9 | |||
| 953bf5d4de | |||
| d9620fd6a4 | |||
| 541e2dd14c | |||
| c7925d98c8 | |||
| f759b19bcb | |||
| 5d7429a416 | |||
| fb7e52d6f3 | |||
| 50e888b075 | |||
| 76c8bbf307 | |||
| 8f3825e92c | |||
| d1c3610ec8 | |||
| 4b958a3c31 | |||
| 1f9021d459 | |||
| 7ad9deaf70 | |||
| c1c17b5f4e | |||
| d92220b4bc | |||
| 4d1972bc99 | |||
| 83c052ec4e | |||
| 57a75fe9e6 | |||
| 379bc37aff | |||
| 0217fbb13b | |||
| 4e9943e6a2 | |||
| b3cc623168 | |||
| 3ee5e5367d | |||
| 85fef30c7f | |||
| e8d8dcbb2d | |||
| 3b679d6134 | |||
| ec44b51ab6 | |||
| 2e52a13c30 | |||
| 1e8e2e9ea7 | |||
| 9e8363c004 | |||
| 56c40ee001 | |||
| e3dfccfee3 | |||
| d555fcaf17 | |||
| 2fdefae718 | |||
| e78858b7b4 | |||
| 636b674229 | |||
| fc6cee17d7 | |||
| 7f7b47fb1c | |||
| bf181b88ec | |||
| c056938b6e | |||
| 66eadf96b0 | |||
| 665595b8b4 | |||
| 29550401fd | |||
| 1bb0012c40 | |||
| 2cea391ebf | |||
| 32e91da0b2 | |||
| 69b56b9658 | |||
| 83e3d77f79 | |||
| 38a8eecd50 | |||
| bd77137714 | |||
| 201126e5d0 | |||
| d4a2e5ef5b | |||
| 2761abf405 | |||
| add16ffdad | |||
| b49cd1c382 | |||
| aa9ae5c11e | |||
| 8e8965eb3d | |||
| a0fe8fd0f0 | |||
| 855031a4fe | |||
| adc2b20aeb | |||
| c860f10cf9 | |||
| d441eff2d2 | |||
| d31f36d3dc | |||
| 4fc7bd47f9 | |||
| a66037d947 | |||
| bb4e04df0b | |||
| d3752caf1d | |||
| 614c77d7ce | |||
| 5d13f08d47 | |||
| 07ba148d9b | |||
| 917e2d5393 | |||
| e384763faf | |||
| 7fb199b187 | |||
| 924e31aad5 | |||
| 48f776e6ff | |||
| a27bda4720 | |||
| a7e0e1e369 | |||
| 5bb5018cc0 | |||
| a9aab6b7e5 | |||
| 651c06caac | |||
| e0d58085f3 | |||
| cb420c2262 | |||
| 6211f546b1 | |||
| 9070fe7fa3 | |||
| c86d7275ec | |||
| 9e1178b7a1 | |||
| cd76cedb7b | |||
| f273445451 | |||
| 740d9a33cf | |||
| 792d703b6f | |||
| f09832404d | |||
| 134b11e7f0 | |||
| 8c01ec364c | |||
| 27e6dde7c4 | |||
| b04b17c8ae | |||
| b037ecad79 | |||
| 7ec3f25d43 | |||
| 1778ab112d | |||
| 5f70d53c94 | |||
| 4b66e97bda | |||
| f8d8e485f1 | |||
| e21bf531e1 | |||
| 76fdf14e79 | |||
| 96cceafe77 | |||
| 58e34b20e1 | |||
|  | e420b183ce | ||
|  | a08f058806 | ||
| 616491e6d8 | |||
| 05c6d67c03 | |||
| e66130e893 | |||
| 5bb9bbac73 | |||
| 8474fc7160 | |||
| ea8158cb50 | |||
| 65398c5fec | |||
| 5181897463 | |||
| 96c7927632 | |||
| 0eb3ffcdbe | |||
|  | 736db75cfd | ||
| 0b44c4547c | |||
|  | 728ac9c166 | ||
| 360b58885e | |||
| 09d412053f | |||
| e0107f189d | |||
| 42af09034c | |||
| 963470b693 | |||
| da57936d92 | |||
| 78cec27ef0 | |||
| c3f5ed881f | |||
| 1c52b4d661 | |||
| 765be4f214 | |||
| 91de6797c5 | |||
| 4bceb119ea | |||
| 14a5c01a6d | |||
| 83df727f8f | |||
| 3444e27a96 | |||
| 865505f883 | |||
| 0ed47be689 | |||
| d8c1c63e56 | |||
| 2934225a6c | |||
|  | d1e5058dae | ||
|  | cbd58d3e72 | ||
|  | 735268fe46 | ||
| 7ddb904335 | |||
| c514adfbbf | |||
| a32c06552f | |||
|  | aefc1aeb4f | ||
|  | 7fc36b5d22 | ||
| 5fd52e7b9e | |||
| e7d14d4687 | |||
| a57ae840ff | |||
| 009621a456 | |||
| 36ed0dc893 | |||
| 8a1c490907 | |||
| 32054705d0 | |||
| 5859483654 | |||
| d0ca8db162 | |||
| a3e138cc2d | |||
| 1fab398778 | |||
| 77ccc9aeb5 | |||
| a6dfe8712c | |||
| 973b2f81ea | |||
| 554f73b550 | |||
| ee8e9df12e | |||
| 00cdd1bc5d | |||
| f1ea7c1c5a | |||
| d13e18534f | |||
| 1dc33c5bd4 | |||
| e09922c8df | |||
| e85af628bf | |||
| 4f2e18ca27 | |||
| 1105d6f11e | |||
| f2bba64ee5 | |||
| ebbe14f293 | |||
| 681934a0dc | |||
| a52b09b787 | |||
| b0af3af059 | |||
| 6bc5bcfd1a | |||
| 999ba52003 | |||
| e0ebed7c09 | |||
| e50ce2f515 | |||
| 5bb9ed5f04 | |||
| 4a36557714 | |||
| 1a93cdad46 | |||
| 2bbef9b9d1 | |||
| 22101c8280 | |||
| 256c6469a6 | |||
| 7367f372c0 | |||
| 822a339532 | |||
| 5d2ad2479b | |||
| 795ca04d7c | |||
| 111701a2c4 | |||
| a793a03a20 | |||
| d231b5f27e | |||
| 709dc44d57 | |||
| d7a39ab574 | |||
| 18882c08d9 | |||
| ce6f9a174f | |||
| f5c8b75122 | |||
| 165d2e4d93 | |||
| 9e9d0dc563 | |||
| a9a5082e1a | |||
| eca9601a89 | |||
| 6bfe784b3f | |||
| 6524a56eeb | |||
| b7f853d84f | |||
| 473155b68d | |||
| 608b93fb61 | |||
| 4a36b30d6b | |||
| 72b26c6a2c | |||
| 7fc86441d1 | |||
| 1a05f16299 | |||
| db5d631049 | |||
| 2d7dd26882 | |||
| b0834f48d4 | |||
| 7d3236550c | |||
| adf62fb42b | |||
| 14c6913af7 | |||
| 192ea0fcdd | |||
| 189abd4982 | |||
| 3df66dabd9 | |||
| f46f70b33c | |||
| e689d15688 | |||
| 3d236c35c9 | |||
| 665538bdd3 | |||
| be7d7536fc | |||
| a932108c87 | |||
| 71eccbb466 | |||
| 700803f7a6 | |||
| 1f38d827c5 | |||
| 8d73c0f289 | |||
| f9884e32fb | |||
| 27b6f2022f | |||
| 98b5808b09 | |||
| f4df8c0c3b | |||
| 882c14df06 | |||
| b3ed98322b | |||
| 4cfd4387b6 | |||
| 89406870bd | |||
| c747d03aff | |||
| 77df275ac0 | |||
| d7dcb7221f | |||
| 92a8709df0 | |||
| e3499ff283 | |||
| 0306b54a0f | |||
| 3afbeacffb | |||
| 3e7376c1f7 | |||
| fd81e8389c | |||
| 00dda8faf9 | |||
| 6b1dda41bc | |||
| fd1c47196d | |||
| 7383a5cff8 | |||
| 49fe70b0aa | |||
| 8e6e3e6289 | |||
| cb681681e1 | |||
| 1e25982c08 | |||
| e243b0f47a | |||
| 6f0a42820b | |||
| c1fc6837db | |||
| 51697c31cb | |||
| 409c83b030 | |||
| acb293ec8f | |||
| 162967e68b | |||
| 11266ac69a | |||
| 03b4b7f3b9 | |||
| 2649aeeee8 | |||
| 3e76ef62b3 | |||
| 284cb23d4d | |||
| 24f0d8f151 | |||
| 9d63a3b81c | |||
| f1b594bdf2 | |||
| 1f7b19938b | |||
| 05c6410550 | |||
| 4246fea03f | |||
| 83059374e9 | |||
| 28f6893c68 | |||
| d881a75e48 | |||
| fe5a455b68 | |||
| 0d4473da69 | |||
| f1b62d354f | |||
| 6ef1533abf | |||
| 32f7b0221d | |||
| 8b1bb7fcfd | |||
| e31a5ea017 | |||
| 7442b8416f | |||
| c875c82bdc | |||
| 4a0117906a | |||
| f74b1cf46a | |||
| 52addc91df | |||
| e1ebd44ea8 | |||
| e428e04435 | |||
| b405a46005 | |||
| 4c0e0b5ee9 | |||
| e7e6c258e2 | |||
| 05284760a7 | |||
| 4c0d381be2 | |||
| 42b300fefb | |||
| 0c08bfed5b | |||
| 57c72bdfbf | |||
| 1fd3b39c75 | |||
| f80cabfa75 | |||
| 2d728e4b07 | |||
| 7ff9605460 | |||
| d3bf9739b5 | |||
| 4e68ab4ef0 | |||
| 71accd725e | |||
| 46612b28aa | |||
| 02af78ca99 | |||
| f40d1dc1b2 | |||
| b0683576b9 | |||
| eaf0b366d3 | |||
| cf9903e500 | |||
| 186e9c00aa | 
							
								
								
									
										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 | ||||
							
								
								
									
										156
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										156
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| name: Build and Push Microservices | ||||
| name: Aspire Publish Workflow | ||||
|  | ||||
| on: | ||||
|   push: | ||||
| @@ -7,132 +7,54 @@ on: | ||||
|   workflow_dispatch: | ||||
|  | ||||
| jobs: | ||||
|   build-sphere: | ||||
|   publish: | ||||
|     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 DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Sphere Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Sphere/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-sphere:latest | ||||
|           platforms: linux/amd64 | ||||
|  | ||||
|   build-pass: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Setup .NET | ||||
|         uses: actions/setup-dotnet@v3 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|       - name: Setup NBGV | ||||
|         uses: dotnet/nbgv@master | ||||
|         id: nbgv | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|       - name: Log in to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Pass Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Pass/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-pass:latest | ||||
|           platforms: linux/amd64 | ||||
|           dotnet-version: "9.0.x" | ||||
|  | ||||
|   build-pusher: | ||||
|     runs-on: ubuntu-latest | ||||
|     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 DockerHub | ||||
|       - name: Log in to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Pusher Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Pusher/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-pusher:latest | ||||
|           platforms: linux/amd64 | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|   build-drive: | ||||
|     runs-on: ubuntu-latest | ||||
|     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 DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Drive Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Drive/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-drive:latest | ||||
|           platforms: linux/amd64 | ||||
|       - name: Install Aspire CLI | ||||
|         run: dotnet tool install -g Aspire.Cli --prerelease | ||||
|  | ||||
|   build-gateway: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Build and Publish Aspire Application | ||||
|         run: aspire publish --project ./DysonNetwork.Control/DysonNetwork.Control.csproj --output publish | ||||
|  | ||||
|       - name: Tag and Push Images | ||||
|         run: | | ||||
|           IMAGES=( "sphere" "pass" "ring" "drive" "develop" ) | ||||
|  | ||||
|           for image in "${IMAGES[@]}"; do | ||||
|             IMAGE_NAME="ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-$image:alpha" | ||||
|             SOURCE_IMAGE_NAME="$image:latest" # Aspire's default local image name | ||||
|  | ||||
|             echo "Tagging and pushing $SOURCE_IMAGE_NAME to $IMAGE_NAME..." | ||||
|             docker tag $SOURCE_IMAGE_NAME $IMAGE_NAME | ||||
|             docker push $IMAGE_NAME | ||||
|           done | ||||
|  | ||||
|       - name: Upload Aspire Publish Directory | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           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 DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|           name: aspire-publish-output | ||||
|           path: ./publish/ | ||||
|  | ||||
|       - name: Upload Docker Compose file | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Gateway Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Gateway/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-gateway:latest | ||||
|           platforms: linux/amd64 | ||||
|           name: docker-compose-output | ||||
|           path: ./publish/docker-compose.yml | ||||
|   | ||||
							
								
								
									
										77
									
								
								DysonNetwork.Control/AppHost.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								DysonNetwork.Control/AppHost.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| using Aspire.Hosting.Yarp.Transforms; | ||||
|  | ||||
| var builder = DistributedApplication.CreateBuilder(args); | ||||
|  | ||||
| // Database was configured separately in each service. | ||||
| // var database = builder.AddPostgres("database"); | ||||
|  | ||||
| var cache = builder.AddRedis("cache"); | ||||
| var queue = builder.AddNats("queue").WithJetStream(); | ||||
|  | ||||
| var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring") | ||||
|     .WithReference(queue) | ||||
|     .WithHttpHealthCheck() | ||||
|     .WithEndpoint(5001, 5001, "https", name: "grpc"); | ||||
| var passService = builder.AddProject<Projects.DysonNetwork_Pass>("pass") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(queue) | ||||
|     .WithReference(ringService) | ||||
|     .WithHttpHealthCheck() | ||||
|     .WithEndpoint(5001, 5001, "https", name: "grpc"); | ||||
| var driveService = builder.AddProject<Projects.DysonNetwork_Drive>("drive") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(queue) | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService) | ||||
|     .WithHttpHealthCheck() | ||||
|     .WithEndpoint(5001, 5001, "https", name: "grpc"); | ||||
| var sphereService = builder.AddProject<Projects.DysonNetwork_Sphere>("sphere") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(queue) | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService) | ||||
|     .WithHttpHealthCheck() | ||||
|     .WithEndpoint(5001, 5001, "https", name: "grpc"); | ||||
| var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop") | ||||
|     .WithReference(cache) | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService) | ||||
|     .WithHttpHealthCheck() | ||||
|     .WithEndpoint(5001, 5001, "https", name: "grpc"); | ||||
|  | ||||
| // Extra double-ended references | ||||
| ringService.WithReference(passService); | ||||
|  | ||||
| builder.AddYarp("gateway") | ||||
|     .WithHostPort(5000) | ||||
|     .WithConfiguration(yarp => | ||||
|     { | ||||
|         var ringCluster = yarp.AddCluster(ringService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/ws", ringCluster); | ||||
|         yarp.AddRoute("/ring/{**catch-all}", ringCluster) | ||||
|             .WithTransformPathRemovePrefix("/ring") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var passCluster = yarp.AddCluster(passService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/.well-known/openid-configuration", passCluster); | ||||
|         yarp.AddRoute("/.well-known/jwks", passCluster); | ||||
|         yarp.AddRoute("/id/{**catch-all}", passCluster) | ||||
|             .WithTransformPathRemovePrefix("/id") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var driveCluster = yarp.AddCluster(driveService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/api/tus", driveCluster); | ||||
|         yarp.AddRoute("/drive/{**catch-all}", driveCluster) | ||||
|             .WithTransformPathRemovePrefix("/drive") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var sphereCluster = yarp.AddCluster(sphereService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/sphere/{**catch-all}", sphereCluster) | ||||
|             .WithTransformPathRemovePrefix("/sphere") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|         var developCluster = yarp.AddCluster(developService.GetEndpoint("http")); | ||||
|         yarp.AddRoute("/develop/{**catch-all}", developCluster) | ||||
|             .WithTransformPathRemovePrefix("/develop") | ||||
|             .WithTransformPathPrefix("/api"); | ||||
|     }); | ||||
|  | ||||
| builder.AddDockerComposeEnvironment("docker-compose"); | ||||
|  | ||||
| builder.Build().Run(); | ||||
							
								
								
									
										30
									
								
								DysonNetwork.Control/DysonNetwork.Control.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								DysonNetwork.Control/DysonNetwork.Control.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|     <Sdk Name="Aspire.AppHost.Sdk" Version="9.4.2"/> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <OutputType>Exe</OutputType> | ||||
|         <TargetFramework>net9.0</TargetFramework> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId> | ||||
|         <RootNamespace>DysonNetwork.Control</RootNamespace> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="Aspire.Hosting.AppHost" Version="9.4.2"/> | ||||
|         <PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" /> | ||||
|         <PackageReference Include="Aspire.Hosting.Nats" Version="9.4.2" /> | ||||
|         <PackageReference Include="Aspire.Hosting.Redis" Version="9.4.2" /> | ||||
|         <PackageReference Include="Aspire.Hosting.Yarp" Version="9.4.2-preview.1.25428.12" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										29
									
								
								DysonNetwork.Control/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								DysonNetwork.Control/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| { | ||||
|   "$schema": "https://json.schemastore.org/launchsettings.json", | ||||
|   "profiles": { | ||||
|     "https": { | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": true, | ||||
|       "applicationUrl": "https://localhost:17025;http://localhost:15057", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development", | ||||
|         "DOTNET_ENVIRONMENT": "Development", | ||||
|         "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21175", | ||||
|         "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22189" | ||||
|       } | ||||
|     }, | ||||
|     "http": { | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": true, | ||||
|       "applicationUrl": "http://localhost:15057", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development", | ||||
|         "DOTNET_ENVIRONMENT": "Development", | ||||
|         "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163", | ||||
|         "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								DysonNetwork.Control/appsettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								DysonNetwork.Control/appsettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| { | ||||
|   "Logging": { | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|       "Microsoft.AspNetCore": "Warning" | ||||
|     } | ||||
|   }, | ||||
|   "ConnectionStrings": { | ||||
|     "cache": "localhost:6379" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										53
									
								
								DysonNetwork.Develop/AppDatabase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								DysonNetwork.Develop/AppDatabase.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
|  | ||||
| namespace DysonNetwork.Develop; | ||||
|  | ||||
| public class AppDatabase( | ||||
|     DbContextOptions<AppDatabase> options, | ||||
|     IConfiguration configuration | ||||
| ) : DbContext(options) | ||||
| { | ||||
|     public DbSet<Developer> Developers { get; set; } = null!; | ||||
|  | ||||
|     public DbSet<DevProject> DevProjects { get; set; } = null!; | ||||
|      | ||||
|     public DbSet<CustomApp> CustomApps { get; set; } = null!; | ||||
|     public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } = null!; | ||||
|     public DbSet<BotAccount> BotAccounts { get; set; } = null!; | ||||
|  | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|     { | ||||
|         optionsBuilder.UseNpgsql( | ||||
|             configuration.GetConnectionString("App"), | ||||
|             opt => opt | ||||
|                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) | ||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) | ||||
|                 .UseNodaTime() | ||||
|         ).UseSnakeCaseNamingConvention(); | ||||
|  | ||||
|         base.OnConfiguring(optionsBuilder); | ||||
|     } | ||||
|  | ||||
|     protected override void OnModelCreating(ModelBuilder modelBuilder) | ||||
|     { | ||||
|         base.OnModelCreating(modelBuilder); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase> | ||||
| { | ||||
|     public AppDatabase CreateDbContext(string[] args) | ||||
|     { | ||||
|         var configuration = new ConfigurationBuilder() | ||||
|             .SetBasePath(Directory.GetCurrentDirectory()) | ||||
|             .AddJsonFile("appsettings.json") | ||||
|             .Build(); | ||||
|  | ||||
|         var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>(); | ||||
|         return new AppDatabase(optionsBuilder.Options, configuration); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								DysonNetwork.Develop/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								DysonNetwork.Develop/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| 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.Develop/DysonNetwork.Develop.csproj", "DysonNetwork.Develop/"] | ||||
| RUN dotnet restore "DysonNetwork.Develop/DysonNetwork.Develop.csproj" | ||||
| COPY . . | ||||
| WORKDIR "/src/DysonNetwork.Develop" | ||||
| RUN dotnet build "./DysonNetwork.Develop.csproj" -c $BUILD_CONFIGURATION -o /app/build | ||||
|  | ||||
| FROM build AS publish | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| RUN dotnet publish "./DysonNetwork.Develop.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false | ||||
|  | ||||
| FROM base AS final | ||||
| WORKDIR /app | ||||
| COPY --from=publish /app/publish . | ||||
| ENTRYPOINT ["dotnet", "DysonNetwork.Develop.dll"] | ||||
							
								
								
									
										38
									
								
								DysonNetwork.Develop/DysonNetwork.Develop.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								DysonNetwork.Develop/DysonNetwork.Develop.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>net9.0</TargetFramework> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/> | ||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7"> | ||||
|             <PrivateAssets>all</PrivateAssets> | ||||
|             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|         </PackageReference> | ||||
|         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||
|         <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/> | ||||
|         <PackageReference Include="NodaTime" Version="3.2.2"/> | ||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/> | ||||
|         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/> | ||||
|   </ItemGroup> | ||||
|   | ||||
|   <ItemGroup> | ||||
|       <Content Include="..\.dockerignore"> | ||||
|         <Link>.dockerignore</Link> | ||||
|       </Content> | ||||
|     </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||
|     <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||
|   </ItemGroup> | ||||
|   | ||||
| </Project> | ||||
							
								
								
									
										54
									
								
								DysonNetwork.Develop/Identity/BotAccount.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								DysonNetwork.Develop/Identity/BotAccount.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
|  | ||||
| public class BotAccount : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(1024)] public string Slug { get; set; } = null!; | ||||
|  | ||||
|     public bool IsActive { get; set; } = true; | ||||
|  | ||||
|     public Guid ProjectId { get; set; } | ||||
|     public DevProject Project { get; set; } = null!; | ||||
|      | ||||
|     [NotMapped] public AccountReference? Account { get; set; } | ||||
|      | ||||
|     /// <summary> | ||||
|     /// This developer field is to serve the transparent info for user to know which developer | ||||
|     /// published this robot. Not for relationships usage. | ||||
|     /// </summary> | ||||
|     [NotMapped] public Developer? Developer { get; set; } | ||||
|  | ||||
|     public Shared.Proto.BotAccount ToProtoValue() | ||||
|     { | ||||
|         var proto = new Shared.Proto.BotAccount | ||||
|         { | ||||
|             Slug = Slug, | ||||
|             IsActive = IsActive, | ||||
|             AutomatedId = Id.ToString(), | ||||
|             CreatedAt = CreatedAt.ToTimestamp(), | ||||
|             UpdatedAt = UpdatedAt.ToTimestamp() | ||||
|         }; | ||||
|  | ||||
|         return proto; | ||||
|     } | ||||
|  | ||||
|     public static BotAccount FromProto(Shared.Proto.BotAccount proto) | ||||
|     { | ||||
|         var botAccount = new BotAccount | ||||
|         { | ||||
|             Id = Guid.Parse(proto.AutomatedId), | ||||
|             Slug = proto.Slug, | ||||
|             IsActive = proto.IsActive, | ||||
|             CreatedAt = proto.CreatedAt.ToInstant(), | ||||
|             UpdatedAt = proto.UpdatedAt.ToInstant() | ||||
|         }; | ||||
|  | ||||
|         return botAccount; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										460
									
								
								DysonNetwork.Develop/Identity/BotAccountController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								DysonNetwork.Develop/Identity/BotAccountController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,460 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Grpc.Core; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/developers/{pubName}/projects/{projectId:guid}/bots")] | ||||
| [Authorize] | ||||
| public class BotAccountController( | ||||
|     BotAccountService botService, | ||||
|     DeveloperService developerService, | ||||
|     DevProjectService projectService, | ||||
|     ILogger<BotAccountController> logger, | ||||
|     AccountClientHelper accounts, | ||||
|     BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver | ||||
| ) | ||||
|     : ControllerBase | ||||
| { | ||||
|     public class CommonBotRequest | ||||
|     { | ||||
|         [MaxLength(256)] public string? FirstName { get; set; } | ||||
|         [MaxLength(256)] public string? MiddleName { get; set; } | ||||
|         [MaxLength(256)] public string? LastName { get; set; } | ||||
|         [MaxLength(1024)] public string? Gender { get; set; } | ||||
|         [MaxLength(1024)] public string? Pronouns { get; set; } | ||||
|         [MaxLength(1024)] public string? TimeZone { get; set; } | ||||
|         [MaxLength(1024)] public string? Location { get; set; } | ||||
|         [MaxLength(4096)] public string? Bio { get; set; } | ||||
|         public Instant? Birthday { get; set; } | ||||
|  | ||||
|         [MaxLength(32)] public string? PictureId { get; set; } | ||||
|         [MaxLength(32)] public string? BackgroundId { get; set; } | ||||
|     } | ||||
|  | ||||
|     public class BotCreateRequest : CommonBotRequest | ||||
|     { | ||||
|         [Required] | ||||
|         [MinLength(2)] | ||||
|         [MaxLength(256)] | ||||
|         [RegularExpression(@"^[A-Za-z0-9_-]+$", | ||||
|             ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.") | ||||
|         ] | ||||
|         public string Name { get; set; } = string.Empty; | ||||
|  | ||||
|         [Required] [MaxLength(256)] public string Nick { get; set; } = string.Empty; | ||||
|  | ||||
|         [Required] [MaxLength(1024)] public string Slug { get; set; } = string.Empty; | ||||
|  | ||||
|         [MaxLength(128)] public string Language { get; set; } = "en-us"; | ||||
|     } | ||||
|  | ||||
|     public class UpdateBotRequest : CommonBotRequest | ||||
|     { | ||||
|         [MinLength(2)] | ||||
|         [MaxLength(256)] | ||||
|         [RegularExpression(@"^[A-Za-z0-9_-]+$", | ||||
|             ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.") | ||||
|         ] | ||||
|         public string? Name { get; set; } = string.Empty; | ||||
|  | ||||
|         [MaxLength(256)] public string? Nick { get; set; } = string.Empty; | ||||
|  | ||||
|         [Required] [MaxLength(1024)] public string? Slug { get; set; } = string.Empty; | ||||
|  | ||||
|         [MaxLength(128)] public string? Language { get; set; } | ||||
|  | ||||
|         public bool? IsActive { get; set; } | ||||
|     } | ||||
|  | ||||
|     [HttpGet] | ||||
|     public async Task<IActionResult> ListBots( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Viewer)) | ||||
|             return StatusCode(403, "You must be an viewer of the developer to list bots"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var bots = await botService.GetBotsByProjectAsync(projectId); | ||||
|         return Ok(await botService.LoadBotsAccountAsync(bots)); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{botId:guid}")] | ||||
|     public async Task<IActionResult> GetBot( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Viewer)) | ||||
|             return StatusCode(403, "You must be an viewer of the developer to view bot details"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var bot = await botService.GetBotByIdAsync(botId); | ||||
|         if (bot is null || bot.ProjectId != projectId) | ||||
|             return NotFound("Bot not found"); | ||||
|  | ||||
|         return Ok(await botService.LoadBotAccountAsync(bot)); | ||||
|     } | ||||
|  | ||||
|     [HttpPost] | ||||
|     public async Task<IActionResult> CreateBot( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromBody] BotCreateRequest createRequest | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to create a bot"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var accountId = Guid.NewGuid(); | ||||
|         var account = new Account() | ||||
|         { | ||||
|             Id = accountId.ToString(), | ||||
|             Name = createRequest.Name, | ||||
|             Nick = createRequest.Nick, | ||||
|             Language = createRequest.Language, | ||||
|             Profile = new AccountProfile() | ||||
|             { | ||||
|                 Id = Guid.NewGuid().ToString(), | ||||
|                 Bio = createRequest.Bio, | ||||
|                 Gender = createRequest.Gender, | ||||
|                 FirstName = createRequest.FirstName, | ||||
|                 MiddleName = createRequest.MiddleName, | ||||
|                 LastName = createRequest.LastName, | ||||
|                 TimeZone = createRequest.TimeZone, | ||||
|                 Pronouns = createRequest.Pronouns, | ||||
|                 Location = createRequest.Location, | ||||
|                 Birthday = createRequest.Birthday?.ToTimestamp(), | ||||
|                 AccountId = accountId.ToString(), | ||||
|                 CreatedAt = now.ToTimestamp(), | ||||
|                 UpdatedAt = now.ToTimestamp() | ||||
|             }, | ||||
|             CreatedAt = now.ToTimestamp(), | ||||
|             UpdatedAt = now.ToTimestamp() | ||||
|         }; | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var bot = await botService.CreateBotAsync( | ||||
|                 project, | ||||
|                 createRequest.Slug, | ||||
|                 account, | ||||
|                 createRequest.PictureId, | ||||
|                 createRequest.BackgroundId | ||||
|             ); | ||||
|             return Ok(bot); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Error creating bot account"); | ||||
|             return StatusCode(500, "An error occurred while creating the bot account"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpPatch("{botId:guid}")] | ||||
|     public async Task<IActionResult> UpdateBot( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId, | ||||
|         [FromBody] UpdateBotRequest request | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to update a bot"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var bot = await botService.GetBotByIdAsync(botId); | ||||
|         if (bot is null || bot.ProjectId != projectId) | ||||
|             return NotFound("Bot not found"); | ||||
|  | ||||
|         var botAccount = await accounts.GetBotAccount(bot.Id); | ||||
|  | ||||
|         if (request.Name is not null) botAccount.Name = request.Name; | ||||
|         if (request.Nick is not null) botAccount.Nick = request.Nick; | ||||
|         if (request.Language is not null) botAccount.Language = request.Language; | ||||
|         if (request.Bio is not null) botAccount.Profile.Bio = request.Bio; | ||||
|         if (request.Gender is not null) botAccount.Profile.Gender = request.Gender; | ||||
|         if (request.FirstName is not null) botAccount.Profile.FirstName = request.FirstName; | ||||
|         if (request.MiddleName is not null) botAccount.Profile.MiddleName = request.MiddleName; | ||||
|         if (request.LastName is not null) botAccount.Profile.LastName = request.LastName; | ||||
|         if (request.TimeZone is not null) botAccount.Profile.TimeZone = request.TimeZone; | ||||
|         if (request.Pronouns is not null) botAccount.Profile.Pronouns = request.Pronouns; | ||||
|         if (request.Location is not null) botAccount.Profile.Location = request.Location; | ||||
|         if (request.Birthday is not null) botAccount.Profile.Birthday = request.Birthday?.ToTimestamp(); | ||||
|  | ||||
|         if (request.Slug is not null) bot.Slug = request.Slug; | ||||
|         if (request.IsActive is not null) bot.IsActive = request.IsActive.Value; | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var updatedBot = await botService.UpdateBotAsync( | ||||
|                 bot, | ||||
|                 botAccount, | ||||
|                 request.PictureId, | ||||
|                 request.BackgroundId | ||||
|             ); | ||||
|  | ||||
|             return Ok(updatedBot); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Error updating bot account {BotId}", botId); | ||||
|             return StatusCode(500, "An error occurred while updating the bot account"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpDelete("{botId:guid}")] | ||||
|     public async Task<IActionResult> DeleteBot( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to delete a bot"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var bot = await botService.GetBotByIdAsync(botId); | ||||
|         if (bot is null || bot.ProjectId != projectId) | ||||
|             return NotFound("Bot not found"); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             await botService.DeleteBotAsync(bot); | ||||
|             return NoContent(); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Error deleting bot {BotId}", botId); | ||||
|             return StatusCode(500, "An error occurred while deleting the bot account"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{botId:guid}/keys")] | ||||
|     public async Task<ActionResult<List<ApiKeyReference>>> ListBotKeys( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer); | ||||
|         if (developer == null) return NotFound("Developer not found"); | ||||
|         if (project == null) return NotFound("Project not found or you don't have access"); | ||||
|         if (bot == null) return NotFound("Bot not found"); | ||||
|  | ||||
|         var keys = await accountsReceiver.ListApiKeyAsync(new ListApiKeyRequest | ||||
|         { | ||||
|             AutomatedId = bot.Id.ToString() | ||||
|         }); | ||||
|         var data = keys.Data.Select(ApiKeyReference.FromProtoValue).ToList(); | ||||
|  | ||||
|         return Ok(data); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{botId:guid}/keys/{keyId:guid}")] | ||||
|     public async Task<ActionResult<ApiKeyReference>> GetBotKey( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId, | ||||
|         [FromRoute] Guid keyId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer); | ||||
|         if (developer == null) return NotFound("Developer not found"); | ||||
|         if (project == null) return NotFound("Project not found or you don't have access"); | ||||
|         if (bot == null) return NotFound("Bot not found"); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var key = await accountsReceiver.GetApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() }); | ||||
|             if (key == null) return NotFound("API key not found"); | ||||
|             return Ok(ApiKeyReference.FromProtoValue(key)); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) | ||||
|         { | ||||
|             return NotFound("API key not found"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public class CreateApiKeyRequest | ||||
|     { | ||||
|         [Required, MaxLength(1024)] | ||||
|         public string Label { get; set; } = null!; | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{botId:guid}/keys")] | ||||
|     public async Task<ActionResult<ApiKeyReference>> CreateBotKey( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId, | ||||
|         [FromBody] CreateApiKeyRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor); | ||||
|         if (developer == null) return NotFound("Developer not found"); | ||||
|         if (project == null) return NotFound("Project not found or you don't have access"); | ||||
|         if (bot == null) return NotFound("Bot not found"); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var newKey = new ApiKey | ||||
|             { | ||||
|                 AccountId = bot.Id.ToString(), | ||||
|                 Label = request.Label | ||||
|             }; | ||||
|              | ||||
|             var createdKey = await accountsReceiver.CreateApiKeyAsync(newKey); | ||||
|             return Ok(ApiKeyReference.FromProtoValue(createdKey)); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument) | ||||
|         { | ||||
|             return BadRequest(ex.Status.Detail); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{botId:guid}/keys/{keyId:guid}/rotate")] | ||||
|     public async Task<ActionResult<ApiKeyReference>> RotateBotKey( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId, | ||||
|         [FromRoute] Guid keyId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor); | ||||
|         if (developer == null) return NotFound("Developer not found"); | ||||
|         if (project == null) return NotFound("Project not found or you don't have access"); | ||||
|         if (bot == null) return NotFound("Bot not found"); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var rotatedKey = await accountsReceiver.RotateApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() }); | ||||
|             return Ok(ApiKeyReference.FromProtoValue(rotatedKey)); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) | ||||
|         { | ||||
|             return NotFound("API key not found"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpDelete("{botId:guid}/keys/{keyId:guid}")] | ||||
|     public async Task<IActionResult> DeleteBotKey( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId, | ||||
|         [FromRoute] Guid keyId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor); | ||||
|         if (developer == null) return NotFound("Developer not found"); | ||||
|         if (project == null) return NotFound("Project not found or you don't have access"); | ||||
|         if (bot == null) return NotFound("Bot not found"); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             await accountsReceiver.DeleteApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() }); | ||||
|             return NoContent(); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) | ||||
|         { | ||||
|             return NotFound("API key not found"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private async Task<(Developer?, DevProject?, BotAccount?)> ValidateBotAccess( | ||||
|         string pubName, | ||||
|         Guid projectId, | ||||
|         Guid botId, | ||||
|         Account currentUser, | ||||
|         PublisherMemberRole requiredRole) | ||||
|     { | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         if (developer == null) return (null, null, null); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), requiredRole)) | ||||
|             return (null, null, null); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project == null) return (developer, null, null); | ||||
|  | ||||
|         var bot = await botService.GetBotByIdAsync(botId); | ||||
|         if (bot == null || bot.ProjectId != projectId) return (developer, project, null); | ||||
|  | ||||
|         return (developer, project, bot); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										35
									
								
								DysonNetwork.Develop/Identity/BotAccountPublicController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								DysonNetwork.Develop/Identity/BotAccountPublicController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("api/bots")] | ||||
| public class BotAccountPublicController(BotAccountService botService, DeveloperService developerService) : ControllerBase | ||||
| { | ||||
|     [HttpGet("{botId:guid}")] | ||||
|     public async Task<ActionResult<BotAccount>> GetBotTransparentInfo([FromRoute] Guid botId) | ||||
|     { | ||||
|         var bot = await botService.GetBotByIdAsync(botId); | ||||
|         if (bot is null) return NotFound("Bot not found"); | ||||
|         bot = await botService.LoadBotAccountAsync(bot); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperById(bot!.Project.DeveloperId); | ||||
|         if (developer is null) return NotFound("Developer not found"); | ||||
|         bot.Developer = await developerService.LoadDeveloperPublisher(developer); | ||||
|  | ||||
|         return Ok(bot); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{botId:guid}/developer")] | ||||
|     public async Task<ActionResult<Developer>> GetBotDeveloper([FromRoute] Guid botId) | ||||
|     { | ||||
|         var bot = await botService.GetBotByIdAsync(botId); | ||||
|         if (bot is null) return NotFound("Bot not found"); | ||||
|          | ||||
|         var developer = await developerService.GetDeveloperById(bot!.Project.DeveloperId); | ||||
|         if (developer is null) return NotFound("Developer not found"); | ||||
|         developer = await developerService.LoadDeveloperPublisher(developer); | ||||
|  | ||||
|         return Ok(developer); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										174
									
								
								DysonNetwork.Develop/Identity/BotAccountService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								DysonNetwork.Develop/Identity/BotAccountService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Grpc.Core; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
|  | ||||
| public class BotAccountService( | ||||
|     AppDatabase db, | ||||
|     BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver, | ||||
|     AccountClientHelper accounts | ||||
| ) | ||||
| { | ||||
|     public async Task<BotAccount?> GetBotByIdAsync(Guid id) | ||||
|     { | ||||
|         return await db.BotAccounts | ||||
|             .Include(b => b.Project) | ||||
|             .FirstOrDefaultAsync(b => b.Id == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<IEnumerable<BotAccount>> GetBotsByProjectAsync(Guid projectId) | ||||
|     { | ||||
|         return await db.BotAccounts | ||||
|             .Where(b => b.ProjectId == projectId) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<BotAccount> CreateBotAsync( | ||||
|         DevProject project, | ||||
|         string slug, | ||||
|         Account account, | ||||
|         string? pictureId, | ||||
|         string? backgroundId | ||||
|     ) | ||||
|     { | ||||
|         // First, check if a bot with this slug already exists in this project | ||||
|         var existingBot = await db.BotAccounts | ||||
|             .FirstOrDefaultAsync(b => b.ProjectId == project.Id && b.Slug == slug); | ||||
|  | ||||
|         if (existingBot != null) | ||||
|             throw new InvalidOperationException("A bot with this slug already exists in this project."); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var automatedId = Guid.NewGuid(); | ||||
|             var createRequest = new CreateBotAccountRequest | ||||
|             { | ||||
|                 AutomatedId = automatedId.ToString(), | ||||
|                 Account = account, | ||||
|                 PictureId = pictureId, | ||||
|                 BackgroundId = backgroundId | ||||
|             }; | ||||
|  | ||||
|             var createResponse = await accountReceiver.CreateBotAccountAsync(createRequest); | ||||
|             var botAccount = createResponse.Bot; | ||||
|  | ||||
|             // Then create the local bot account | ||||
|             var bot = new BotAccount | ||||
|             { | ||||
|                 Id = automatedId, | ||||
|                 Slug = slug, | ||||
|                 ProjectId = project.Id, | ||||
|                 Project = project, | ||||
|                 IsActive = botAccount.IsActive, | ||||
|                 CreatedAt = botAccount.CreatedAt.ToInstant(), | ||||
|                 UpdatedAt = botAccount.UpdatedAt.ToInstant() | ||||
|             }; | ||||
|  | ||||
|             db.BotAccounts.Add(bot); | ||||
|             await db.SaveChangesAsync(); | ||||
|  | ||||
|             return bot; | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists) | ||||
|         { | ||||
|             throw new InvalidOperationException( | ||||
|                 "A bot account with this ID already exists in the authentication service.", ex); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument) | ||||
|         { | ||||
|             throw new ArgumentException($"Invalid bot account data: {ex.Status.Detail}", ex); | ||||
|         } | ||||
|         catch (RpcException ex) | ||||
|         { | ||||
|             throw new Exception($"Failed to create bot account: {ex.Status.Detail}", ex); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task<BotAccount> UpdateBotAsync( | ||||
|         BotAccount bot, | ||||
|         Account account, | ||||
|         string? pictureId, | ||||
|         string? backgroundId | ||||
|     ) | ||||
|     { | ||||
|         db.Update(bot); | ||||
|         await db.SaveChangesAsync(); | ||||
|          | ||||
|         try | ||||
|         { | ||||
|             // Update the bot account in the Pass service | ||||
|             var updateRequest = new UpdateBotAccountRequest | ||||
|             { | ||||
|                 AutomatedId = bot.Id.ToString(), | ||||
|                 Account = account, | ||||
|                 PictureId = pictureId, | ||||
|                 BackgroundId = backgroundId | ||||
|             }; | ||||
|  | ||||
|             var updateResponse = await accountReceiver.UpdateBotAccountAsync(updateRequest); | ||||
|             var updatedBot = updateResponse.Bot; | ||||
|  | ||||
|             // Update local bot account | ||||
|             bot.UpdatedAt = updatedBot.UpdatedAt.ToInstant(); | ||||
|             bot.IsActive = updatedBot.IsActive; | ||||
|             await db.SaveChangesAsync(); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound) | ||||
|         { | ||||
|             throw new Exception("Bot account not found in the authentication service", ex); | ||||
|         } | ||||
|         catch (RpcException ex) | ||||
|         { | ||||
|             throw new Exception($"Failed to update bot account: {ex.Status.Detail}", ex); | ||||
|         } | ||||
|  | ||||
|         return bot; | ||||
|     } | ||||
|  | ||||
|     public async Task DeleteBotAsync(BotAccount bot) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             // Delete the bot account from the Pass service | ||||
|             var deleteRequest = new DeleteBotAccountRequest | ||||
|             { | ||||
|                 AutomatedId = bot.Id.ToString(), | ||||
|                 Force = false | ||||
|             }; | ||||
|  | ||||
|             await accountReceiver.DeleteBotAccountAsync(deleteRequest); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound) | ||||
|         { | ||||
|             // Account not found in Pass service, continue with local deletion | ||||
|         } | ||||
|  | ||||
|         // Delete the local bot account | ||||
|         db.BotAccounts.Remove(bot); | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<BotAccount?> LoadBotAccountAsync(BotAccount bot) => | ||||
|         (await LoadBotsAccountAsync([bot])).FirstOrDefault(); | ||||
|  | ||||
|     public async Task<List<BotAccount>> LoadBotsAccountAsync(IEnumerable<BotAccount> bots) | ||||
|     { | ||||
|         bots = bots.ToList(); | ||||
|         var automatedIds = bots.Select(b => b.Id).ToList(); | ||||
|         var data = await accounts.GetBotAccountBatch(automatedIds); | ||||
|  | ||||
|         foreach (var bot in bots) | ||||
|         { | ||||
|             bot.Account = data | ||||
|                 .Select(AccountReference.FromProtoValue) | ||||
|                 .FirstOrDefault(e => e.AutomatedId == bot.Id); | ||||
|         } | ||||
|  | ||||
|         return bots as List<BotAccount> ?? []; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										178
									
								
								DysonNetwork.Develop/Identity/CustomApp.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								DysonNetwork.Develop/Identity/CustomApp.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Google.Protobuf; | ||||
| using Google.Protobuf.WellKnownTypes; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
| using NodaTime; | ||||
|  | ||||
| using VerificationMark = DysonNetwork.Shared.Data.VerificationMark; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
|  | ||||
| public enum CustomAppStatus | ||||
| { | ||||
|     Developing, | ||||
|     Staging, | ||||
|     Production, | ||||
|     Suspended | ||||
| } | ||||
|  | ||||
| public class CustomApp : ModelBase, IIdentifiedResource | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(1024)] public string Slug { get; set; } = null!; | ||||
|     [MaxLength(1024)] public string Name { get; set; } = null!; | ||||
|     [MaxLength(4096)] public string? Description { get; set; } | ||||
|     public CustomAppStatus Status { get; set; } = CustomAppStatus.Developing; | ||||
|  | ||||
|     [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; } | ||||
|  | ||||
|     [Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public CustomAppOauthConfig? OauthConfig { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public CustomAppLinks? Links { get; set; } | ||||
|  | ||||
|     [JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>(); | ||||
|  | ||||
|     public Guid ProjectId { get; set; } | ||||
|     public DevProject Project { get; set; } = null!; | ||||
|      | ||||
|     [NotMapped] | ||||
|     public Developer Developer => Project.Developer; | ||||
|  | ||||
|     [NotMapped] public string ResourceIdentifier => "custom-app:" + Id; | ||||
|  | ||||
|     public Shared.Proto.CustomApp ToProto() | ||||
|     { | ||||
|         return new Shared.Proto.CustomApp | ||||
|         { | ||||
|             Id = Id.ToString(), | ||||
|             Slug = Slug, | ||||
|             Name = Name, | ||||
|             Description = Description ?? string.Empty, | ||||
|             Status = Status switch | ||||
|             { | ||||
|                 CustomAppStatus.Developing => Shared.Proto.CustomAppStatus.Developing, | ||||
|                 CustomAppStatus.Staging => Shared.Proto.CustomAppStatus.Staging, | ||||
|                 CustomAppStatus.Production => Shared.Proto.CustomAppStatus.Production, | ||||
|                 CustomAppStatus.Suspended => Shared.Proto.CustomAppStatus.Suspended, | ||||
|                 _ => Shared.Proto.CustomAppStatus.Unspecified | ||||
|             }, | ||||
|             Picture = Picture?.ToProtoValue(), | ||||
|             Background = Background?.ToProtoValue(), | ||||
|             Verification = Verification?.ToProtoValue(), | ||||
|             Links = Links is null ? null : new DysonNetwork.Shared.Proto.CustomAppLinks | ||||
|             { | ||||
|                 HomePage = Links.HomePage ?? string.Empty, | ||||
|                 PrivacyPolicy = Links.PrivacyPolicy ?? string.Empty, | ||||
|                 TermsOfService = Links.TermsOfService ?? string.Empty | ||||
|             }, | ||||
|             OauthConfig = OauthConfig is null ? null : new DysonNetwork.Shared.Proto.CustomAppOauthConfig | ||||
|             { | ||||
|                 ClientUri = OauthConfig.ClientUri ?? string.Empty, | ||||
|                 RedirectUris = { OauthConfig.RedirectUris ?? [] }, | ||||
|                 PostLogoutRedirectUris = { OauthConfig.PostLogoutRedirectUris ?? [] }, | ||||
|                 AllowedScopes = { OauthConfig.AllowedScopes ?? [] }, | ||||
|                 AllowedGrantTypes = { OauthConfig.AllowedGrantTypes ?? [] }, | ||||
|                 RequirePkce = OauthConfig.RequirePkce, | ||||
|                 AllowOfflineAccess = OauthConfig.AllowOfflineAccess | ||||
|             }, | ||||
|             ProjectId = ProjectId.ToString(), | ||||
|             CreatedAt = CreatedAt.ToTimestamp(), | ||||
|             UpdatedAt = UpdatedAt.ToTimestamp() | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public CustomApp FromProtoValue(Shared.Proto.CustomApp p) | ||||
|     { | ||||
|         Id = Guid.Parse(p.Id); | ||||
|         Slug = p.Slug; | ||||
|         Name = p.Name; | ||||
|         Description = string.IsNullOrEmpty(p.Description) ? null : p.Description; | ||||
|         Status = p.Status switch | ||||
|         { | ||||
|             Shared.Proto.CustomAppStatus.Developing => CustomAppStatus.Developing, | ||||
|             Shared.Proto.CustomAppStatus.Staging => CustomAppStatus.Staging, | ||||
|             Shared.Proto.CustomAppStatus.Production => CustomAppStatus.Production, | ||||
|             Shared.Proto.CustomAppStatus.Suspended => CustomAppStatus.Suspended, | ||||
|             _ => CustomAppStatus.Developing | ||||
|         }; | ||||
|         ProjectId = string.IsNullOrEmpty(p.ProjectId) ? Guid.Empty : Guid.Parse(p.ProjectId); | ||||
|         CreatedAt = p.CreatedAt.ToInstant(); | ||||
|         UpdatedAt = p.UpdatedAt.ToInstant(); | ||||
|         if (p.Picture is not null) Picture = CloudFileReferenceObject.FromProtoValue(p.Picture); | ||||
|         if (p.Background is not null) Background = CloudFileReferenceObject.FromProtoValue(p.Background); | ||||
|         if (p.Verification is not null) Verification = VerificationMark.FromProtoValue(p.Verification); | ||||
|         if (p.Links is not null) | ||||
|         { | ||||
|             Links = new CustomAppLinks | ||||
|             { | ||||
|                 HomePage = string.IsNullOrEmpty(p.Links.HomePage) ? null : p.Links.HomePage, | ||||
|                 PrivacyPolicy = string.IsNullOrEmpty(p.Links.PrivacyPolicy) ? null : p.Links.PrivacyPolicy, | ||||
|                 TermsOfService = string.IsNullOrEmpty(p.Links.TermsOfService) ? null : p.Links.TermsOfService | ||||
|             }; | ||||
|         } | ||||
|         return this; | ||||
|     } | ||||
| } | ||||
|  | ||||
| public class CustomAppLinks | ||||
| { | ||||
|     [MaxLength(8192)] public string? HomePage { get; set; } | ||||
|     [MaxLength(8192)] public string? PrivacyPolicy { get; set; } | ||||
|     [MaxLength(8192)] public string? TermsOfService { get; set; } | ||||
| } | ||||
|  | ||||
| public class CustomAppOauthConfig | ||||
| { | ||||
|     [MaxLength(1024)] public string? ClientUri { get; set; } | ||||
|     [MaxLength(4096)] public string[] RedirectUris { get; set; } = []; | ||||
|     [MaxLength(4096)] public string[]? PostLogoutRedirectUris { get; set; } | ||||
|     [MaxLength(256)] public string[]? AllowedScopes { get; set; } = ["openid", "profile", "email"]; | ||||
|     [MaxLength(256)] public string[] AllowedGrantTypes { get; set; } = ["authorization_code", "refresh_token"]; | ||||
|     public bool RequirePkce { get; set; } = true; | ||||
|     public bool AllowOfflineAccess { get; set; } = false; | ||||
| } | ||||
|  | ||||
| public class CustomAppSecret : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(1024)] public string Secret { get; set; } = null!; | ||||
|     [MaxLength(4096)] public string? Description { get; set; } = null!; | ||||
|     public Instant? ExpiredAt { get; set; } | ||||
|     public bool IsOidc { get; set; } = false; // Indicates if this secret is for OIDC/OAuth | ||||
|  | ||||
|     public Guid AppId { get; set; } | ||||
|     public CustomApp App { get; set; } = null!; | ||||
|  | ||||
|  | ||||
|     public static CustomAppSecret FromProtoValue(DysonNetwork.Shared.Proto.CustomAppSecret p) | ||||
|     { | ||||
|         return new CustomAppSecret | ||||
|         { | ||||
|             Id = Guid.Parse(p.Id), | ||||
|             Secret = p.Secret, | ||||
|             Description = p.Description, | ||||
|             ExpiredAt = p.ExpiredAt?.ToInstant(), | ||||
|             IsOidc = p.IsOidc, | ||||
|             AppId = Guid.Parse(p.AppId), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public DysonNetwork.Shared.Proto.CustomAppSecret ToProto() | ||||
|     { | ||||
|         return new DysonNetwork.Shared.Proto.CustomAppSecret | ||||
|         { | ||||
|             Id = Id.ToString(), | ||||
|             Secret = Secret, | ||||
|             Description = Description, | ||||
|             ExpiredAt = ExpiredAt?.ToTimestamp(), | ||||
|             IsOidc = IsOidc, | ||||
|             AppId = Id.ToString(), | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										431
									
								
								DysonNetwork.Develop/Identity/CustomAppController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										431
									
								
								DysonNetwork.Develop/Identity/CustomAppController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,431 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/developers/{pubName}/projects/{projectId:guid}/apps")] | ||||
| public class CustomAppController(CustomAppService customApps, DeveloperService ds, DevProjectService projectService) | ||||
|     : ControllerBase | ||||
| { | ||||
|     public record CustomAppRequest( | ||||
|         [MaxLength(1024)] string? Slug, | ||||
|         [MaxLength(1024)] string? Name, | ||||
|         [MaxLength(4096)] string? Description, | ||||
|         string? PictureId, | ||||
|         string? BackgroundId, | ||||
|         CustomAppStatus? Status, | ||||
|         CustomAppLinks? Links, | ||||
|         CustomAppOauthConfig? OauthConfig | ||||
|     ); | ||||
|  | ||||
|     public record CreateSecretRequest( | ||||
|         [MaxLength(4096)] string? Description, | ||||
|         TimeSpan? ExpiresIn = null, | ||||
|         bool IsOidc = false | ||||
|     ); | ||||
|  | ||||
|     public record SecretResponse( | ||||
|         string Id, | ||||
|         string? Secret, | ||||
|         string? Description, | ||||
|         Instant? ExpiresAt, | ||||
|         bool IsOidc, | ||||
|         Instant CreatedAt, | ||||
|         Instant UpdatedAt | ||||
|     ); | ||||
|  | ||||
|     [HttpGet] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> ListApps([FromRoute] string pubName, [FromRoute] Guid projectId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) return NotFound(); | ||||
|  | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Viewer)) | ||||
|             return StatusCode(403, "You must be a viewer of the developer to list custom apps"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) return NotFound(); | ||||
|  | ||||
|         var apps = await customApps.GetAppsByProjectAsync(projectId); | ||||
|         return Ok(apps); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{appId:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> GetApp([FromRoute] string pubName, [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid appId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|          | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) return NotFound(); | ||||
|          | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Viewer)) | ||||
|             return StatusCode(403, "You must be a viewer of the developer to list custom apps"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) return NotFound(); | ||||
|  | ||||
|         var app = await customApps.GetAppAsync(appId, projectId); | ||||
|         if (app == null) | ||||
|             return NotFound(); | ||||
|  | ||||
|         return Ok(app); | ||||
|     } | ||||
|  | ||||
|     [HttpPost] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> CreateApp( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromBody] CustomAppRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to create a custom app"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Slug)) | ||||
|             return BadRequest("Name and slug are required"); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var app = await customApps.CreateAppAsync(projectId, request); | ||||
|             if (app == null) | ||||
|                 return BadRequest("Failed to create app"); | ||||
|  | ||||
|             return CreatedAtAction( | ||||
|                 nameof(GetApp), | ||||
|                 new { pubName, projectId, appId = app.Id }, | ||||
|                 app | ||||
|             ); | ||||
|         } | ||||
|         catch (InvalidOperationException ex) | ||||
|         { | ||||
|             return BadRequest(ex.Message); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpPatch("{appId:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> UpdateApp( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid appId, | ||||
|         [FromBody] CustomAppRequest request | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to update a custom app"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var app = await customApps.GetAppAsync(appId, projectId); | ||||
|         if (app == null) | ||||
|             return NotFound(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             app = await customApps.UpdateAppAsync(app, request); | ||||
|             return Ok(app); | ||||
|         } | ||||
|         catch (InvalidOperationException ex) | ||||
|         { | ||||
|             return BadRequest(ex.Message); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpDelete("{appId:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> DeleteApp( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid appId | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to delete a custom app"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var app = await customApps.GetAppAsync(appId, projectId); | ||||
|         if (app == null) | ||||
|             return NotFound(); | ||||
|  | ||||
|         var result = await customApps.DeleteAppAsync(appId); | ||||
|         if (!result) | ||||
|             return NotFound(); | ||||
|  | ||||
|         return NoContent(); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{appId:guid}/secrets")] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> ListSecrets( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid appId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to view app secrets"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var app = await customApps.GetAppAsync(appId, projectId); | ||||
|         if (app == null) | ||||
|             return NotFound("App not found"); | ||||
|  | ||||
|         var secrets = await customApps.GetAppSecretsAsync(appId); | ||||
|         return Ok(secrets.Select(s => new SecretResponse( | ||||
|             s.Id.ToString(), | ||||
|             null, | ||||
|             s.Description, | ||||
|             s.ExpiredAt, | ||||
|             s.IsOidc, | ||||
|             s.CreatedAt, | ||||
|             s.UpdatedAt | ||||
|         ))); | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{appId:guid}/secrets")] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> CreateSecret( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid appId, | ||||
|         [FromBody] CreateSecretRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to create app secrets"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var app = await customApps.GetAppAsync(appId, projectId); | ||||
|         if (app == null) | ||||
|             return NotFound("App not found"); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var secret = await customApps.CreateAppSecretAsync(new CustomAppSecret | ||||
|             { | ||||
|                 AppId = appId, | ||||
|                 Description = request.Description, | ||||
|                 ExpiredAt = request.ExpiresIn.HasValue | ||||
|                     ? NodaTime.SystemClock.Instance.GetCurrentInstant() | ||||
|                         .Plus(Duration.FromTimeSpan(request.ExpiresIn.Value)) | ||||
|                     : (NodaTime.Instant?)null, | ||||
|                 IsOidc = request.IsOidc | ||||
|             }); | ||||
|  | ||||
|             return CreatedAtAction( | ||||
|                 nameof(GetSecret), | ||||
|                 new { pubName, projectId, appId, secretId = secret.Id }, | ||||
|                 new SecretResponse( | ||||
|                     secret.Id.ToString(), | ||||
|                     secret.Secret, | ||||
|                     secret.Description, | ||||
|                     secret.ExpiredAt, | ||||
|                     secret.IsOidc, | ||||
|                     secret.CreatedAt, | ||||
|                     secret.UpdatedAt | ||||
|                 ) | ||||
|             ); | ||||
|         } | ||||
|         catch (InvalidOperationException ex) | ||||
|         { | ||||
|             return BadRequest(ex.Message); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{appId:guid}/secrets/{secretId:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> GetSecret( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid appId, | ||||
|         [FromRoute] Guid secretId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to view app secrets"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var app = await customApps.GetAppAsync(appId, projectId); | ||||
|         if (app == null) | ||||
|             return NotFound("App not found"); | ||||
|  | ||||
|         var secret = await customApps.GetAppSecretAsync(secretId, appId); | ||||
|         if (secret == null) | ||||
|             return NotFound("Secret not found"); | ||||
|  | ||||
|         return Ok(new SecretResponse( | ||||
|             secret.Id.ToString(), | ||||
|             null, | ||||
|             secret.Description, | ||||
|             secret.ExpiredAt, | ||||
|             secret.IsOidc, | ||||
|             secret.CreatedAt, | ||||
|             secret.UpdatedAt | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     [HttpDelete("{appId:guid}/secrets/{secretId:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> DeleteSecret( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid appId, | ||||
|         [FromRoute] Guid secretId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to delete app secrets"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var app = await customApps.GetAppAsync(appId, projectId); | ||||
|         if (app == null) | ||||
|             return NotFound("App not found"); | ||||
|  | ||||
|         var secret = await customApps.GetAppSecretAsync(secretId, appId); | ||||
|         if (secret == null) | ||||
|             return NotFound("Secret not found"); | ||||
|  | ||||
|         var result = await customApps.DeleteAppSecretAsync(secretId, appId); | ||||
|         if (!result) | ||||
|             return NotFound("Failed to delete secret"); | ||||
|  | ||||
|         return NoContent(); | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{appId:guid}/secrets/{secretId:guid}/rotate")] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> RotateSecret( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid appId, | ||||
|         [FromRoute] Guid secretId, | ||||
|         [FromBody] CreateSecretRequest? request = null) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to rotate app secrets"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
|         if (project is null) | ||||
|             return NotFound("Project not found or you don't have access"); | ||||
|  | ||||
|         var app = await customApps.GetAppAsync(appId, projectId); | ||||
|         if (app == null) | ||||
|             return NotFound("App not found"); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var secret = await customApps.RotateAppSecretAsync(new CustomAppSecret | ||||
|             { | ||||
|                 Id = secretId, | ||||
|                 AppId = appId, | ||||
|                 Description = request?.Description, | ||||
|                 ExpiredAt = request?.ExpiresIn.HasValue == true | ||||
|                     ? NodaTime.SystemClock.Instance.GetCurrentInstant() | ||||
|                         .Plus(Duration.FromTimeSpan(request.ExpiresIn.Value)) | ||||
|                     : (NodaTime.Instant?)null, | ||||
|                 IsOidc = request?.IsOidc ?? false | ||||
|             }); | ||||
|  | ||||
|             return Ok(new SecretResponse( | ||||
|                 secret.Id.ToString(), | ||||
|                 secret.Secret, | ||||
|                 secret.Description, | ||||
|                 secret.ExpiredAt, | ||||
|                 secret.IsOidc, | ||||
|                 secret.CreatedAt, | ||||
|                 secret.UpdatedAt | ||||
|             )); | ||||
|         } | ||||
|         catch (InvalidOperationException ex) | ||||
|         { | ||||
|             return BadRequest(ex.Message); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,8 +1,11 @@ | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Developer; | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
| 
 | ||||
| public class CustomAppService( | ||||
|     AppDatabase db, | ||||
| @@ -11,10 +14,17 @@ public class CustomAppService( | ||||
| ) | ||||
| { | ||||
|     public async Task<CustomApp?> CreateAppAsync( | ||||
|         Publisher.Publisher pub, | ||||
|         Guid projectId, | ||||
|         CustomAppController.CustomAppRequest request | ||||
|     ) | ||||
|     { | ||||
|         var project = await db.DevProjects | ||||
|             .Include(p => p.Developer) | ||||
|             .FirstOrDefaultAsync(p => p.Id == projectId); | ||||
|              | ||||
|         if (project == null) | ||||
|             return null; | ||||
|              | ||||
|         var app = new CustomApp | ||||
|         { | ||||
|             Slug = request.Slug!, | ||||
| @@ -23,7 +33,7 @@ public class CustomAppService( | ||||
|             Status = request.Status ?? CustomAppStatus.Developing, | ||||
|             Links = request.Links, | ||||
|             OauthConfig = request.OauthConfig, | ||||
|             PublisherId = pub.Id | ||||
|             ProjectId = projectId | ||||
|         }; | ||||
| 
 | ||||
|         if (request.PictureId is not null) | ||||
| @@ -74,17 +84,104 @@ public class CustomAppService( | ||||
|         return app; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<CustomApp?> GetAppAsync(Guid id, Guid? publisherId = null) | ||||
|     public async Task<CustomApp?> GetAppAsync(Guid id, Guid? projectId = null) | ||||
|     { | ||||
|         var query = db.CustomApps.Where(a => a.Id == id).AsQueryable(); | ||||
|         if (publisherId.HasValue) | ||||
|             query = query.Where(a => a.PublisherId == publisherId.Value); | ||||
|         return await query.FirstOrDefaultAsync(); | ||||
|         var query = db.CustomApps.AsQueryable(); | ||||
|          | ||||
|         if (projectId.HasValue) | ||||
|         { | ||||
|             query = query.Where(a => a.ProjectId == projectId.Value); | ||||
|         } | ||||
| 
 | ||||
|     public async Task<List<CustomApp>> GetAppsByPublisherAsync(Guid publisherId) | ||||
|         return await query.FirstOrDefaultAsync(a => a.Id == id); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<List<CustomAppSecret>> GetAppSecretsAsync(Guid appId) | ||||
|     { | ||||
|         return await db.CustomApps.Where(a => a.PublisherId == publisherId).ToListAsync(); | ||||
|         return await db.CustomAppSecrets | ||||
|             .Where(s => s.AppId == appId) | ||||
|             .OrderByDescending(s => s.CreatedAt) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<CustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId) | ||||
|     { | ||||
|         return await db.CustomAppSecrets | ||||
|             .FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<CustomAppSecret> CreateAppSecretAsync(CustomAppSecret secret) | ||||
|     { | ||||
|         if (string.IsNullOrWhiteSpace(secret.Secret)) | ||||
|         { | ||||
|             // Generate a new random secret if not provided | ||||
|             secret.Secret = GenerateRandomSecret(); | ||||
|         } | ||||
| 
 | ||||
|         secret.Id = Guid.NewGuid(); | ||||
|         secret.CreatedAt = NodaTime.SystemClock.Instance.GetCurrentInstant(); | ||||
|         secret.UpdatedAt = secret.CreatedAt; | ||||
| 
 | ||||
|         db.CustomAppSecrets.Add(secret); | ||||
|         await db.SaveChangesAsync(); | ||||
| 
 | ||||
|         return secret; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<bool> DeleteAppSecretAsync(Guid secretId, Guid appId) | ||||
|     { | ||||
|         var secret = await db.CustomAppSecrets | ||||
|             .FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId); | ||||
| 
 | ||||
|         if (secret == null) | ||||
|             return false; | ||||
| 
 | ||||
|         db.CustomAppSecrets.Remove(secret); | ||||
|         await db.SaveChangesAsync(); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<CustomAppSecret> RotateAppSecretAsync(CustomAppSecret secretUpdate) | ||||
|     { | ||||
|         var existingSecret = await db.CustomAppSecrets | ||||
|             .FirstOrDefaultAsync(s => s.Id == secretUpdate.Id && s.AppId == secretUpdate.AppId); | ||||
| 
 | ||||
|         if (existingSecret == null) | ||||
|             throw new InvalidOperationException("Secret not found"); | ||||
| 
 | ||||
|         // Update the existing secret with new values | ||||
|         existingSecret.Secret = GenerateRandomSecret(); | ||||
|         existingSecret.Description = secretUpdate.Description ?? existingSecret.Description; | ||||
|         existingSecret.ExpiredAt = secretUpdate.ExpiredAt ?? existingSecret.ExpiredAt; | ||||
|         existingSecret.IsOidc = secretUpdate.IsOidc; | ||||
|         existingSecret.UpdatedAt = NodaTime.SystemClock.Instance.GetCurrentInstant(); | ||||
| 
 | ||||
|         await db.SaveChangesAsync(); | ||||
|         return existingSecret; | ||||
|     } | ||||
| 
 | ||||
|     private static string GenerateRandomSecret(int length = 64) | ||||
|     { | ||||
|         const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-._~+"; | ||||
|         var res = new StringBuilder(); | ||||
|         using (var rng = RandomNumberGenerator.Create()) | ||||
|         { | ||||
|             var uintBuffer = new byte[sizeof(uint)]; | ||||
|             while (length-- > 0) | ||||
|             { | ||||
|                 rng.GetBytes(uintBuffer); | ||||
|                 var num = BitConverter.ToUInt32(uintBuffer, 0); | ||||
|                 res.Append(valid[(int)(num % (uint)valid.Length)]); | ||||
|             } | ||||
|         } | ||||
|         return res.ToString(); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<List<CustomApp>> GetAppsByProjectAsync(Guid projectId) | ||||
|     { | ||||
|         return await db.CustomApps | ||||
|             .Where(a => a.ProjectId == projectId) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<CustomApp?> UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request) | ||||
							
								
								
									
										68
									
								
								DysonNetwork.Develop/Identity/CustomAppServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								DysonNetwork.Develop/Identity/CustomAppServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
|  | ||||
| public class CustomAppServiceGrpc(AppDatabase db) : Shared.Proto.CustomAppService.CustomAppServiceBase | ||||
| { | ||||
|     public override async Task<GetCustomAppResponse> GetCustomApp(GetCustomAppRequest request, ServerCallContext context) | ||||
|     { | ||||
|         var q = db.CustomApps.AsQueryable(); | ||||
|         switch (request.QueryCase) | ||||
|         { | ||||
|             case GetCustomAppRequest.QueryOneofCase.Id when !string.IsNullOrWhiteSpace(request.Id): | ||||
|             { | ||||
|                 if (!Guid.TryParse(request.Id, out var id)) | ||||
|                     throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid id")); | ||||
|                 var appById = await q.FirstOrDefaultAsync(a => a.Id == id); | ||||
|                 if (appById is null) | ||||
|                     throw new RpcException(new Status(StatusCode.NotFound, "app not found")); | ||||
|                 return new GetCustomAppResponse { App = appById.ToProto() }; | ||||
|             } | ||||
|             case GetCustomAppRequest.QueryOneofCase.Slug when !string.IsNullOrWhiteSpace(request.Slug): | ||||
|             { | ||||
|                 var appBySlug = await q.FirstOrDefaultAsync(a => a.Slug == request.Slug); | ||||
|                 if (appBySlug is null) | ||||
|                     throw new RpcException(new Status(StatusCode.NotFound, "app not found")); | ||||
|                 return new GetCustomAppResponse { App = appBySlug.ToProto() }; | ||||
|             } | ||||
|             default: | ||||
|                 throw new RpcException(new Status(StatusCode.InvalidArgument, "id or slug required")); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override async Task<CheckCustomAppSecretResponse> CheckCustomAppSecret(CheckCustomAppSecretRequest request, ServerCallContext context) | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(request.Secret)) | ||||
|             throw new RpcException(new Status(StatusCode.InvalidArgument, "secret required")); | ||||
|  | ||||
|         IQueryable<CustomAppSecret> q = db.CustomAppSecrets; | ||||
|         switch (request.SecretIdentifierCase) | ||||
|         { | ||||
|             case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.SecretId: | ||||
|             { | ||||
|                 if (!Guid.TryParse(request.SecretId, out var sid)) | ||||
|                     throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid secret_id")); | ||||
|                 q = q.Where(s => s.Id == sid); | ||||
|                 break; | ||||
|             } | ||||
|             case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.AppId: | ||||
|             { | ||||
|                 if (!Guid.TryParse(request.AppId, out var aid)) | ||||
|                     throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid app_id")); | ||||
|                 q = q.Where(s => s.AppId == aid); | ||||
|                 break; | ||||
|             } | ||||
|             default: | ||||
|                 throw new RpcException(new Status(StatusCode.InvalidArgument, "secret_id or app_id required")); | ||||
|         } | ||||
|  | ||||
|         if (request.HasIsOidc) | ||||
|             q = q.Where(s => s.IsOidc == request.IsOidc); | ||||
|  | ||||
|         var now = NodaTime.SystemClock.Instance.GetCurrentInstant(); | ||||
|         var exists = await q.AnyAsync(s => s.Secret == request.Secret && (s.ExpiredAt == null || s.ExpiredAt > now)); | ||||
|         return new CheckCustomAppSecretResponse { Valid = exists }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										79
									
								
								DysonNetwork.Develop/Identity/Developer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								DysonNetwork.Develop/Identity/Developer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using VerificationMark = DysonNetwork.Shared.Data.VerificationMark; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
|  | ||||
| public class Developer | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     public Guid PublisherId { get; set; } | ||||
|      | ||||
|     [JsonIgnore] public List<DevProject> Projects { get; set; } = []; | ||||
|      | ||||
|     [NotMapped] public PublisherInfo? Publisher { get; set; } | ||||
| } | ||||
|  | ||||
| public class PublisherInfo | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     public PublisherType Type { get; set; } | ||||
|     public string Name { get; set; } = string.Empty; | ||||
|     public string Nick { get; set; } = string.Empty; | ||||
|     public string? Bio { get; set; } | ||||
|  | ||||
|     public CloudFileReferenceObject? Picture { get; set; } | ||||
|     public CloudFileReferenceObject? Background { get; set; } | ||||
|  | ||||
|     public VerificationMark? Verification { get; set; } | ||||
|     public Guid? AccountId { get; set; } | ||||
|     public Guid? RealmId { get; set; } | ||||
|  | ||||
|     public static PublisherInfo FromProto(Publisher proto) | ||||
|     { | ||||
|         var info = new PublisherInfo | ||||
|         { | ||||
|             Id = Guid.Parse(proto.Id), | ||||
|             Type = proto.Type == PublisherType.PubIndividual | ||||
|                 ? PublisherType.PubIndividual | ||||
|                 : PublisherType.PubOrganizational, | ||||
|             Name = proto.Name, | ||||
|             Nick = proto.Nick, | ||||
|             Bio = string.IsNullOrEmpty(proto.Bio) ? null : proto.Bio, | ||||
|             Verification = proto.VerificationMark is not null | ||||
|                 ? VerificationMark.FromProtoValue(proto.VerificationMark) | ||||
|                 : null, | ||||
|             AccountId = string.IsNullOrEmpty(proto.AccountId) ? null : Guid.Parse(proto.AccountId), | ||||
|             RealmId = string.IsNullOrEmpty(proto.RealmId) ? null : Guid.Parse(proto.RealmId) | ||||
|         }; | ||||
|  | ||||
|         if (proto.Picture != null) | ||||
|         { | ||||
|             info.Picture = new CloudFileReferenceObject | ||||
|             { | ||||
|                 Id = proto.Picture.Id, | ||||
|                 Name = proto.Picture.Name, | ||||
|                 MimeType = proto.Picture.MimeType, | ||||
|                 Hash = proto.Picture.Hash, | ||||
|                 Size = proto.Picture.Size | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         if (proto.Background != null) | ||||
|         { | ||||
|             info.Background = new CloudFileReferenceObject | ||||
|             { | ||||
|                 Id = proto.Background.Id, | ||||
|                 Name = proto.Background.Name, | ||||
|                 MimeType = proto.Background.MimeType, | ||||
|                 Hash = proto.Background.Hash, | ||||
|                 Size = (long)proto.Background.Size | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return info; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										129
									
								
								DysonNetwork.Develop/Identity/DeveloperController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								DysonNetwork.Develop/Identity/DeveloperController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/developers")] | ||||
| public class DeveloperController( | ||||
|     AppDatabase db, | ||||
|     PublisherService.PublisherServiceClient ps, | ||||
|     ActionLogService.ActionLogServiceClient als, | ||||
|     DeveloperService ds | ||||
| ) | ||||
|     : ControllerBase | ||||
| { | ||||
|     [HttpGet("{name}")] | ||||
|     public async Task<ActionResult<Developer>> GetDeveloper(string name) | ||||
|     { | ||||
|         var developer = await ds.GetDeveloperByName(name); | ||||
|         if (developer is null) return NotFound(); | ||||
|         return Ok(await ds.LoadDeveloperPublisher(developer)); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{name}/stats")] | ||||
|     public async Task<ActionResult<DeveloperStats>> GetDeveloperStats(string name) | ||||
|     { | ||||
|         var developer = await ds.GetDeveloperByName(name); | ||||
|         if (developer is null) return NotFound(); | ||||
|  | ||||
|         // Get custom apps count | ||||
|         var customAppsCount = await db.CustomApps | ||||
|             .Include(a => a.Project) | ||||
|             .Where(a => a.Project.DeveloperId == developer.Id) | ||||
|             .CountAsync(); | ||||
|  | ||||
|         var stats = new DeveloperStats | ||||
|         { | ||||
|             TotalCustomApps = customAppsCount | ||||
|         }; | ||||
|  | ||||
|         return Ok(stats); | ||||
|     } | ||||
|  | ||||
|     [HttpGet] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<Developer>>> ListJoinedDevelopers() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|          | ||||
|         var pubResponse = await ps.ListPublishersAsync(new ListPublishersRequest { AccountId = currentUser.Id }); | ||||
|         var pubIds = pubResponse.Publishers.Select(p => p.Id).Select(Guid.Parse).ToList(); | ||||
|  | ||||
|         var developerQuery = db.Developers | ||||
|             .Where(d => pubIds.Contains(d.PublisherId)) | ||||
|             .AsQueryable(); | ||||
|          | ||||
|         var totalCount = await developerQuery.CountAsync();  | ||||
|         Response.Headers.Append("X-Total", totalCount.ToString()); | ||||
|          | ||||
|         var developers = await developerQuery.ToListAsync(); | ||||
|  | ||||
|         return Ok(await ds.LoadDeveloperPublisher(developers)); | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{name}/enroll")] | ||||
|     [Authorize] | ||||
|     [RequiredPermission("global", "developers.create")] | ||||
|     public async Task<ActionResult<Developer>> EnrollDeveloperProgram(string name) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|  | ||||
|         PublisherInfo? pub; | ||||
|         try | ||||
|         { | ||||
|             var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name }); | ||||
|             pub = PublisherInfo.FromProto(pubResponse.Publisher); | ||||
|         } catch (RpcException ex) | ||||
|         { | ||||
|             return NotFound(ex.Status.Detail); | ||||
|         } | ||||
|  | ||||
|         // Check if the user is an owner of the publisher | ||||
|         var permResponse = await ps.IsPublisherMemberAsync(new IsPublisherMemberRequest | ||||
|         { | ||||
|             PublisherId = pub.Id.ToString(), | ||||
|             AccountId = currentUser.Id, | ||||
|             Role = PublisherMemberRole.Owner | ||||
|         }); | ||||
|         if (!permResponse.Valid) return StatusCode(403, "You must be the owner of the publisher to join the developer program"); | ||||
|  | ||||
|         var hasDeveloper = await db.Developers.AnyAsync(d => d.PublisherId == pub.Id); | ||||
|         if (hasDeveloper) return BadRequest("Publisher is already in the developer program"); | ||||
|          | ||||
|         var developer = new Developer | ||||
|         { | ||||
|             Id = Guid.NewGuid(), | ||||
|             PublisherId = pub.Id | ||||
|         }; | ||||
|  | ||||
|         db.Developers.Add(developer); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         _ = als.CreateActionLogAsync(new CreateActionLogRequest | ||||
|         { | ||||
|             Action = "developers.enroll", | ||||
|             Meta =  | ||||
|             {  | ||||
|                 { "publisher_id", Google.Protobuf.WellKnownTypes.Value.ForString(pub.Id.ToString()) }, | ||||
|                 { "publisher_name", Google.Protobuf.WellKnownTypes.Value.ForString(pub.Name) } | ||||
|             }, | ||||
|             AccountId = currentUser.Id, | ||||
|             UserAgent = Request.Headers.UserAgent, | ||||
|             IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() | ||||
|         }); | ||||
|  | ||||
|         return Ok(await ds.LoadDeveloperPublisher(developer)); | ||||
|     } | ||||
|  | ||||
|     public class DeveloperStats | ||||
|     { | ||||
|         public int TotalCustomApps { get; set; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										75
									
								
								DysonNetwork.Develop/Identity/DeveloperService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								DysonNetwork.Develop/Identity/DeveloperService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
|  | ||||
| public class DeveloperService( | ||||
|     AppDatabase db, | ||||
|     PublisherService.PublisherServiceClient ps, | ||||
|     ILogger<DeveloperService> logger) | ||||
| { | ||||
|     public async Task<Developer> LoadDeveloperPublisher(Developer developer) | ||||
|     { | ||||
|         var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() }); | ||||
|         developer.Publisher = PublisherInfo.FromProto(pubResponse.Publisher); | ||||
|         return developer; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public async Task<IEnumerable<Developer>> LoadDeveloperPublisher(IEnumerable<Developer> developers) | ||||
|     { | ||||
|         var enumerable = developers.ToList(); | ||||
|         var pubIds = enumerable.Select(d => d.PublisherId).ToList(); | ||||
|         var pubRequest = new GetPublisherBatchRequest(); | ||||
|         pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString())); | ||||
|         var pubResponse = await ps.GetPublisherBatchAsync(pubRequest); | ||||
|         var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), PublisherInfo.FromProto); | ||||
|  | ||||
|         return enumerable.Select(d => | ||||
|         { | ||||
|             d.Publisher = pubs[d.PublisherId]; | ||||
|             return d; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public async Task<Developer?> GetDeveloperByName(string name) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name }); | ||||
|             var pubId = Guid.Parse(pubResponse.Publisher.Id); | ||||
|  | ||||
|             var developer = await db.Developers.FirstOrDefaultAsync(d => d.PublisherId == pubId); | ||||
|             return developer; | ||||
|         } | ||||
|         catch (RpcException ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Developer {name} not found", name); | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task<Developer?> GetDeveloperById(Guid id) | ||||
|     { | ||||
|         return await db.Developers.FirstOrDefaultAsync(d => d.Id == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, PublisherMemberRole role) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             var permResponse = await ps.IsPublisherMemberAsync(new IsPublisherMemberRequest | ||||
|             { | ||||
|                 PublisherId = pubId.ToString(), | ||||
|                 AccountId = accountId.ToString(), | ||||
|                 Role = role | ||||
|             }); | ||||
|             return permResponse.Valid; | ||||
|         } | ||||
|         catch (RpcException) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										203
									
								
								DysonNetwork.Develop/Migrations/20250807133702_InitialMigration.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								DysonNetwork.Develop/Migrations/20250807133702_InitialMigration.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,203 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| 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.Develop.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250807133702_InitialMigration")] | ||||
|     partial class InitialMigration | ||||
|     { | ||||
|         /// <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.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
|                     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<Guid>("DeveloperId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("developer_id"); | ||||
|  | ||||
|                     b.Property<CustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
|                     b.Property<string>("Slug") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("slug"); | ||||
|  | ||||
|                     b.Property<int>("Status") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("status"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_custom_apps"); | ||||
|  | ||||
|                     b.HasIndex("DeveloperId") | ||||
|                         .HasDatabaseName("ix_custom_apps_developer_id"); | ||||
|  | ||||
|                     b.ToTable("custom_apps", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("AppId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("app_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<bool>("IsOidc") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_oidc"); | ||||
|  | ||||
|                     b.Property<string>("Secret") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("secret"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_custom_app_secrets"); | ||||
|  | ||||
|                     b.HasIndex("AppId") | ||||
|                         .HasDatabaseName("ix_custom_app_secrets_app_id"); | ||||
|  | ||||
|                     b.ToTable("custom_app_secrets", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("PublisherId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("publisher_id"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_developers"); | ||||
|  | ||||
|                     b.ToTable("developers", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("DeveloperId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_custom_apps_developers_developer_id"); | ||||
|  | ||||
|                     b.Navigation("Developer"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App") | ||||
|                         .WithMany("Secrets") | ||||
|                         .HasForeignKey("AppId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id"); | ||||
|  | ||||
|                     b.Navigation("App"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.Navigation("Secrets"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,108 @@ | ||||
| using System; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Develop.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class InitialMigration : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "developers", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     publisher_id = table.Column<Guid>(type: "uuid", nullable: false) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_developers", x => x.id); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "custom_apps", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||
|                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||
|                     description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), | ||||
|                     status = table.Column<int>(type: "integer", nullable: false), | ||||
|                     picture = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||
|                     background = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||
|                     verification = table.Column<VerificationMark>(type: "jsonb", nullable: true), | ||||
|                     oauth_config = table.Column<CustomAppOauthConfig>(type: "jsonb", nullable: true), | ||||
|                     links = table.Column<CustomAppLinks>(type: "jsonb", nullable: true), | ||||
|                     developer_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_custom_apps", x => x.id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_custom_apps_developers_developer_id", | ||||
|                         column: x => x.developer_id, | ||||
|                         principalTable: "developers", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "custom_app_secrets", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     secret = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||
|                     description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), | ||||
|                     expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||
|                     is_oidc = table.Column<bool>(type: "boolean", nullable: false), | ||||
|                     app_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_custom_app_secrets", x => x.id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_custom_app_secrets_custom_apps_app_id", | ||||
|                         column: x => x.app_id, | ||||
|                         principalTable: "custom_apps", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_custom_app_secrets_app_id", | ||||
|                 table: "custom_app_secrets", | ||||
|                 column: "app_id"); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_custom_apps_developer_id", | ||||
|                 table: "custom_apps", | ||||
|                 column: "developer_id"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "custom_app_secrets"); | ||||
|  | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "custom_apps"); | ||||
|  | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "developers"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										270
									
								
								DysonNetwork.Develop/Migrations/20250818124844_AddDevProject.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								DysonNetwork.Develop/Migrations/20250818124844_AddDevProject.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,270 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| 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.Develop.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250818124844_AddDevProject")] | ||||
|     partial class AddDevProject | ||||
|     { | ||||
|         /// <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.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
|                     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<CustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
|                     b.Property<Guid>("ProjectId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("project_id"); | ||||
|  | ||||
|                     b.Property<string>("Slug") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("slug"); | ||||
|  | ||||
|                     b.Property<int>("Status") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("status"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_custom_apps"); | ||||
|  | ||||
|                     b.HasIndex("ProjectId") | ||||
|                         .HasDatabaseName("ix_custom_apps_project_id"); | ||||
|  | ||||
|                     b.ToTable("custom_apps", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("AppId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("app_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<bool>("IsOidc") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_oidc"); | ||||
|  | ||||
|                     b.Property<string>("Secret") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("secret"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_custom_app_secrets"); | ||||
|  | ||||
|                     b.HasIndex("AppId") | ||||
|                         .HasDatabaseName("ix_custom_app_secrets_app_id"); | ||||
|  | ||||
|                     b.ToTable("custom_app_secrets", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("PublisherId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("publisher_id"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_developers"); | ||||
|  | ||||
|                     b.ToTable("developers", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", 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<string>("Description") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(4096) | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<Guid>("DeveloperId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("developer_id"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     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_dev_projects"); | ||||
|  | ||||
|                     b.HasIndex("DeveloperId") | ||||
|                         .HasDatabaseName("ix_dev_projects_developer_id"); | ||||
|  | ||||
|                     b.ToTable("dev_projects", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("ProjectId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_custom_apps_dev_projects_project_id"); | ||||
|  | ||||
|                     b.Navigation("Project"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App") | ||||
|                         .WithMany("Secrets") | ||||
|                         .HasForeignKey("AppId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id"); | ||||
|  | ||||
|                     b.Navigation("App"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer") | ||||
|                         .WithMany("Projects") | ||||
|                         .HasForeignKey("DeveloperId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_dev_projects_developers_developer_id"); | ||||
|  | ||||
|                     b.Navigation("Developer"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.Navigation("Secrets"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => | ||||
|                 { | ||||
|                     b.Navigation("Projects"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,96 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Develop.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddDevProject : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropForeignKey( | ||||
|                 name: "fk_custom_apps_developers_developer_id", | ||||
|                 table: "custom_apps"); | ||||
|  | ||||
|             migrationBuilder.RenameColumn( | ||||
|                 name: "developer_id", | ||||
|                 table: "custom_apps", | ||||
|                 newName: "project_id"); | ||||
|  | ||||
|             migrationBuilder.RenameIndex( | ||||
|                 name: "ix_custom_apps_developer_id", | ||||
|                 table: "custom_apps", | ||||
|                 newName: "ix_custom_apps_project_id"); | ||||
|  | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "dev_projects", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||
|                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||
|                     description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false), | ||||
|                     developer_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_dev_projects", x => x.id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_dev_projects_developers_developer_id", | ||||
|                         column: x => x.developer_id, | ||||
|                         principalTable: "developers", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_dev_projects_developer_id", | ||||
|                 table: "dev_projects", | ||||
|                 column: "developer_id"); | ||||
|  | ||||
|             migrationBuilder.AddForeignKey( | ||||
|                 name: "fk_custom_apps_dev_projects_project_id", | ||||
|                 table: "custom_apps", | ||||
|                 column: "project_id", | ||||
|                 principalTable: "dev_projects", | ||||
|                 principalColumn: "id", | ||||
|                 onDelete: ReferentialAction.Cascade); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropForeignKey( | ||||
|                 name: "fk_custom_apps_dev_projects_project_id", | ||||
|                 table: "custom_apps"); | ||||
|  | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "dev_projects"); | ||||
|  | ||||
|             migrationBuilder.RenameColumn( | ||||
|                 name: "project_id", | ||||
|                 table: "custom_apps", | ||||
|                 newName: "developer_id"); | ||||
|  | ||||
|             migrationBuilder.RenameIndex( | ||||
|                 name: "ix_custom_apps_project_id", | ||||
|                 table: "custom_apps", | ||||
|                 newName: "ix_custom_apps_developer_id"); | ||||
|  | ||||
|             migrationBuilder.AddForeignKey( | ||||
|                 name: "fk_custom_apps_developers_developer_id", | ||||
|                 table: "custom_apps", | ||||
|                 column: "developer_id", | ||||
|                 principalTable: "developers", | ||||
|                 principalColumn: "id", | ||||
|                 onDelete: ReferentialAction.Cascade); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										324
									
								
								DysonNetwork.Develop/Migrations/20250819163227_AddBotAccount.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								DysonNetwork.Develop/Migrations/20250819163227_AddBotAccount.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,324 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| 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.Develop.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250819163227_AddBotAccount")] | ||||
|     partial class AddBotAccount | ||||
|     { | ||||
|         /// <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.Develop.Identity.BotAccount", 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<bool>("IsActive") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_active"); | ||||
|  | ||||
|                     b.Property<Guid>("ProjectId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("project_id"); | ||||
|  | ||||
|                     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_bot_accounts"); | ||||
|  | ||||
|                     b.HasIndex("ProjectId") | ||||
|                         .HasDatabaseName("ix_bot_accounts_project_id"); | ||||
|  | ||||
|                     b.ToTable("bot_accounts", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
|                     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<CustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
|                     b.Property<Guid>("ProjectId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("project_id"); | ||||
|  | ||||
|                     b.Property<string>("Slug") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("slug"); | ||||
|  | ||||
|                     b.Property<int>("Status") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("status"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_custom_apps"); | ||||
|  | ||||
|                     b.HasIndex("ProjectId") | ||||
|                         .HasDatabaseName("ix_custom_apps_project_id"); | ||||
|  | ||||
|                     b.ToTable("custom_apps", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("AppId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("app_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<bool>("IsOidc") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_oidc"); | ||||
|  | ||||
|                     b.Property<string>("Secret") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("secret"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_custom_app_secrets"); | ||||
|  | ||||
|                     b.HasIndex("AppId") | ||||
|                         .HasDatabaseName("ix_custom_app_secrets_app_id"); | ||||
|  | ||||
|                     b.ToTable("custom_app_secrets", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("PublisherId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("publisher_id"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_developers"); | ||||
|  | ||||
|                     b.ToTable("developers", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", 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<string>("Description") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(4096) | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<Guid>("DeveloperId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("developer_id"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     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_dev_projects"); | ||||
|  | ||||
|                     b.HasIndex("DeveloperId") | ||||
|                         .HasDatabaseName("ix_dev_projects_developer_id"); | ||||
|  | ||||
|                     b.ToTable("dev_projects", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("ProjectId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_bot_accounts_dev_projects_project_id"); | ||||
|  | ||||
|                     b.Navigation("Project"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("ProjectId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_custom_apps_dev_projects_project_id"); | ||||
|  | ||||
|                     b.Navigation("Project"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App") | ||||
|                         .WithMany("Secrets") | ||||
|                         .HasForeignKey("AppId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id"); | ||||
|  | ||||
|                     b.Navigation("App"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer") | ||||
|                         .WithMany("Projects") | ||||
|                         .HasForeignKey("DeveloperId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_dev_projects_developers_developer_id"); | ||||
|  | ||||
|                     b.Navigation("Developer"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.Navigation("Secrets"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => | ||||
|                 { | ||||
|                     b.Navigation("Projects"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,51 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Develop.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddBotAccount : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "bot_accounts", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||
|                     is_active = table.Column<bool>(type: "boolean", nullable: false), | ||||
|                     project_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_bot_accounts", x => x.id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_bot_accounts_dev_projects_project_id", | ||||
|                         column: x => x.project_id, | ||||
|                         principalTable: "dev_projects", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_bot_accounts_project_id", | ||||
|                 table: "bot_accounts", | ||||
|                 column: "project_id"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "bot_accounts"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										321
									
								
								DysonNetwork.Develop/Migrations/AppDatabaseModelSnapshot.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								DysonNetwork.Develop/Migrations/AppDatabaseModelSnapshot.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,321 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using NodaTime; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Develop.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     partial class AppDatabaseModelSnapshot : ModelSnapshot | ||||
|     { | ||||
|         protected override void BuildModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "9.0.7") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", 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<bool>("IsActive") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_active"); | ||||
|  | ||||
|                     b.Property<Guid>("ProjectId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("project_id"); | ||||
|  | ||||
|                     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_bot_accounts"); | ||||
|  | ||||
|                     b.HasIndex("ProjectId") | ||||
|                         .HasDatabaseName("ix_bot_accounts_project_id"); | ||||
|  | ||||
|                     b.ToTable("bot_accounts", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
|                     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<CustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
|                     b.Property<Guid>("ProjectId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("project_id"); | ||||
|  | ||||
|                     b.Property<string>("Slug") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("slug"); | ||||
|  | ||||
|                     b.Property<int>("Status") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("status"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_custom_apps"); | ||||
|  | ||||
|                     b.HasIndex("ProjectId") | ||||
|                         .HasDatabaseName("ix_custom_apps_project_id"); | ||||
|  | ||||
|                     b.ToTable("custom_apps", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("AppId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("app_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<bool>("IsOidc") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("is_oidc"); | ||||
|  | ||||
|                     b.Property<string>("Secret") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("secret"); | ||||
|  | ||||
|                     b.Property<Instant>("UpdatedAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_custom_app_secrets"); | ||||
|  | ||||
|                     b.HasIndex("AppId") | ||||
|                         .HasDatabaseName("ix_custom_app_secrets_app_id"); | ||||
|  | ||||
|                     b.ToTable("custom_app_secrets", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("PublisherId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("publisher_id"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_developers"); | ||||
|  | ||||
|                     b.ToTable("developers", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", 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<string>("Description") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(4096) | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<Guid>("DeveloperId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("developer_id"); | ||||
|  | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     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_dev_projects"); | ||||
|  | ||||
|                     b.HasIndex("DeveloperId") | ||||
|                         .HasDatabaseName("ix_dev_projects_developer_id"); | ||||
|  | ||||
|                     b.ToTable("dev_projects", (string)null); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("ProjectId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_bot_accounts_dev_projects_project_id"); | ||||
|  | ||||
|                     b.Navigation("Project"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("ProjectId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_custom_apps_dev_projects_project_id"); | ||||
|  | ||||
|                     b.Navigation("Project"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App") | ||||
|                         .WithMany("Secrets") | ||||
|                         .HasForeignKey("AppId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id"); | ||||
|  | ||||
|                     b.Navigation("App"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer") | ||||
|                         .WithMany("Projects") | ||||
|                         .HasForeignKey("DeveloperId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_dev_projects_developers_developer_id"); | ||||
|  | ||||
|                     b.Navigation("Developer"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b => | ||||
|                 { | ||||
|                     b.Navigation("Secrets"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b => | ||||
|                 { | ||||
|                     b.Navigation("Projects"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										34
									
								
								DysonNetwork.Develop/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								DysonNetwork.Develop/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using DysonNetwork.Develop.Startup; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| builder.AddServiceDefaults(); | ||||
|  | ||||
| builder.ConfigureAppKestrel(builder.Configuration); | ||||
|  | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| builder.Services.AddAppSwagger(); | ||||
| builder.Services.AddDysonAuth(); | ||||
| builder.Services.AddPublisherService(); | ||||
| builder.Services.AddAccountService(); | ||||
| builder.Services.AddDriveService(); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| app.MapDefaultEndpoints(); | ||||
|  | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
|     var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||
|     await db.Database.MigrateAsync(); | ||||
| } | ||||
|  | ||||
| app.ConfigureAppMiddleware(builder.Configuration); | ||||
|  | ||||
| app.Run(); | ||||
							
								
								
									
										16
									
								
								DysonNetwork.Develop/Project/DevProject.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								DysonNetwork.Develop/Project/DevProject.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Project; | ||||
|  | ||||
| public class DevProject : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(1024)] public string Slug { get; set; } = string.Empty; | ||||
|     [MaxLength(1024)] public string Name { get; set; } = string.Empty; | ||||
|     [MaxLength(4096)] public string Description { get; set; } = string.Empty; | ||||
|      | ||||
|     public Developer Developer { get; set; } = null!; | ||||
|     public Guid DeveloperId { get; set; } | ||||
| } | ||||
							
								
								
									
										107
									
								
								DysonNetwork.Develop/Project/DevProjectController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								DysonNetwork.Develop/Project/DevProjectController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using DysonNetwork.Shared.Proto; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Project; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/developers/{pubName}/projects")] | ||||
| public class DevProjectController(DevProjectService projectService, DeveloperService developerService) : ControllerBase | ||||
| { | ||||
|     public record DevProjectRequest( | ||||
|         [MaxLength(1024)] string? Slug, | ||||
|         [MaxLength(1024)] string? Name, | ||||
|         [MaxLength(4096)] string? Description | ||||
|     ); | ||||
|  | ||||
|     [HttpGet] | ||||
|     public async Task<IActionResult> ListProjects([FromRoute] string pubName) | ||||
|     { | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         if (developer is null) return NotFound(); | ||||
|          | ||||
|         var projects = await projectService.GetProjectsByDeveloperAsync(developer.Id); | ||||
|         return Ok(projects); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{id:guid}")] | ||||
|     public async Task<IActionResult> GetProject([FromRoute] string pubName, Guid id) | ||||
|     { | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         if (developer is null) return NotFound(); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(id, developer.Id); | ||||
|         if (project is null) return NotFound(); | ||||
|  | ||||
|         return Ok(project); | ||||
|     } | ||||
|  | ||||
|     [HttpPost] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> CreateProject([FromRoute] string pubName, [FromBody] DevProjectRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)  | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|              | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|             return StatusCode(403, "You must be an editor of the developer to create a project"); | ||||
|  | ||||
|         if (string.IsNullOrWhiteSpace(request.Slug) || string.IsNullOrWhiteSpace(request.Name)) | ||||
|             return BadRequest("Slug and Name are required"); | ||||
|  | ||||
|         var project = await projectService.CreateProjectAsync(developer, request); | ||||
|         return CreatedAtAction( | ||||
|             nameof(GetProject),  | ||||
|             new { pubName, id = project.Id }, | ||||
|             project | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     [HttpPut("{id:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> UpdateProject( | ||||
|         [FromRoute] string pubName,  | ||||
|         [FromRoute] Guid id, | ||||
|         [FromBody] DevProjectRequest request | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)  | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|         if (developer is null || developer.Id != accountId) | ||||
|             return Forbid(); | ||||
|  | ||||
|         var project = await projectService.UpdateProjectAsync(id, developer.Id, request); | ||||
|         if (project is null) | ||||
|             return NotFound(); | ||||
|  | ||||
|         return Ok(project); | ||||
|     } | ||||
|  | ||||
|     [HttpDelete("{id:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<IActionResult> DeleteProject([FromRoute] string pubName, [FromRoute] Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser)  | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|         if (developer is null || developer.Id != accountId) | ||||
|             return Forbid(); | ||||
|  | ||||
|         var success = await projectService.DeleteProjectAsync(id, developer.Id); | ||||
|         if (!success) | ||||
|             return NotFound(); | ||||
|  | ||||
|         return NoContent(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										77
									
								
								DysonNetwork.Develop/Project/DevProjectService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								DysonNetwork.Develop/Project/DevProjectService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using DysonNetwork.Shared.Proto; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Project; | ||||
|  | ||||
| public class DevProjectService( | ||||
|     AppDatabase db, | ||||
|     FileReferenceService.FileReferenceServiceClient fileRefs, | ||||
|     FileService.FileServiceClient files | ||||
| ) | ||||
| { | ||||
|     public async Task<DevProject> CreateProjectAsync( | ||||
|         Developer developer, | ||||
|         DevProjectController.DevProjectRequest request | ||||
|     ) | ||||
|     { | ||||
|         var project = new DevProject | ||||
|         { | ||||
|             Slug = request.Slug!, | ||||
|             Name = request.Name!, | ||||
|             Description = request.Description ?? string.Empty, | ||||
|             DeveloperId = developer.Id | ||||
|         }; | ||||
|  | ||||
|         db.DevProjects.Add(project); | ||||
|         await db.SaveChangesAsync(); | ||||
|          | ||||
|         return project; | ||||
|     } | ||||
|  | ||||
|     public async Task<DevProject?> GetProjectAsync(Guid id, Guid? developerId = null) | ||||
|     { | ||||
|         var query = db.DevProjects.AsQueryable(); | ||||
|          | ||||
|         if (developerId.HasValue) | ||||
|         { | ||||
|             query = query.Where(p => p.DeveloperId == developerId.Value); | ||||
|         } | ||||
|  | ||||
|         return await query.FirstOrDefaultAsync(p => p.Id == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<List<DevProject>> GetProjectsByDeveloperAsync(Guid developerId) | ||||
|     { | ||||
|         return await db.DevProjects | ||||
|             .Where(p => p.DeveloperId == developerId) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<DevProject?> UpdateProjectAsync( | ||||
|         Guid id, | ||||
|         Guid developerId, | ||||
|         DevProjectController.DevProjectRequest request | ||||
|     ) | ||||
|     { | ||||
|         var project = await GetProjectAsync(id, developerId); | ||||
|         if (project == null) return null; | ||||
|  | ||||
|         if (request.Slug != null) project.Slug = request.Slug; | ||||
|         if (request.Name != null) project.Name = request.Name; | ||||
|         if (request.Description != null) project.Description = request.Description; | ||||
|  | ||||
|         await db.SaveChangesAsync(); | ||||
|         return project; | ||||
|     } | ||||
|  | ||||
|     public async Task<bool> DeleteProjectAsync(Guid id, Guid developerId) | ||||
|     { | ||||
|         var project = await GetProjectAsync(id, developerId); | ||||
|         if (project == null) return false; | ||||
|  | ||||
|         db.DevProjects.Remove(project); | ||||
|         await db.SaveChangesAsync(); | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
| @@ -5,7 +5,7 @@ | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "http://localhost:5094", | ||||
|       "applicationUrl": "http://localhost:5156", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
| @@ -14,7 +14,7 @@ | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "https://localhost:7034;http://0.0.0.0:5094", | ||||
|       "applicationUrl": "https://localhost:7192;http://localhost:5156", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
							
								
								
									
										34
									
								
								DysonNetwork.Develop/Startup/ApplicationConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								DysonNetwork.Develop/Startup/ApplicationConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| using System.Net; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using Microsoft.AspNetCore.HttpOverrides; | ||||
| using Prometheus; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Startup; | ||||
|  | ||||
| public static class ApplicationConfiguration | ||||
| { | ||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration) | ||||
|     { | ||||
|         app.MapMetrics(); | ||||
|         app.MapOpenApi(); | ||||
|  | ||||
|         app.UseSwagger(); | ||||
|         app.UseSwaggerUI(); | ||||
|          | ||||
|         app.UseRequestLocalization(); | ||||
|  | ||||
|         app.ConfigureForwardedHeaders(configuration); | ||||
|  | ||||
|         app.UseAuthentication(); | ||||
|         app.UseAuthorization(); | ||||
|         app.UseMiddleware<PermissionMiddleware>(); | ||||
|  | ||||
|         app.MapControllers(); | ||||
|          | ||||
|         app.MapGrpcService<CustomAppServiceGrpc>(); | ||||
|  | ||||
|         return app; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										79
									
								
								DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								DysonNetwork.Develop/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| using System.Globalization; | ||||
| using Microsoft.OpenApi.Models; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using StackExchange.Redis; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Startup; | ||||
|  | ||||
| public static class ServiceCollectionExtensions | ||||
| { | ||||
|     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddLocalization(); | ||||
|  | ||||
|         services.AddDbContext<AppDatabase>(); | ||||
|         services.AddSingleton<IClock>(SystemClock.Instance); | ||||
|         services.AddHttpContextAccessor(); | ||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); | ||||
|  | ||||
|         services.AddHttpClient(); | ||||
|  | ||||
|         services.AddControllers().AddJsonOptions(options => | ||||
|         { | ||||
|             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|              | ||||
|             options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||
|         }); | ||||
|  | ||||
|         services.AddGrpc(options => { options.EnableDetailedErrors = true; }); | ||||
|  | ||||
|         services.Configure<RequestLocalizationOptions>(options => | ||||
|         { | ||||
|             var supportedCultures = new[] | ||||
|             { | ||||
|                 new CultureInfo("en-US"), | ||||
|                 new CultureInfo("zh-Hans"), | ||||
|             }; | ||||
|  | ||||
|             options.SupportedCultures = supportedCultures; | ||||
|             options.SupportedUICultures = supportedCultures; | ||||
|         }); | ||||
|  | ||||
|         services.AddScoped<DeveloperService>(); | ||||
|         services.AddScoped<CustomAppService>(); | ||||
|         services.AddScoped<DevProjectService>(); | ||||
|         services.AddScoped<BotAccountService>(); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddCors(); | ||||
|         services.AddAuthorization(); | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppSwagger(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddEndpointsApiExplorer(); | ||||
|         services.AddSwaggerGen(options => | ||||
|         { | ||||
|             options.SwaggerDoc("v1", new OpenApiInfo | ||||
|             { | ||||
|                 Version = "v1", | ||||
|                 Title = "Develop API", | ||||
|             }); | ||||
|         }); | ||||
|         services.AddOpenApi(); | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										26
									
								
								DysonNetwork.Develop/appsettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								DysonNetwork.Develop/appsettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| { | ||||
|   "Debug": true, | ||||
|   "BaseUrl": "http://localhost:5071", | ||||
|   "SiteUrl": "https://solian.app", | ||||
|   "Logging": { | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|       "Microsoft.AspNetCore": "Warning" | ||||
|     } | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" | ||||
|   }, | ||||
|   "KnownProxies": [ | ||||
|     "127.0.0.1", | ||||
|     "::1" | ||||
|   ], | ||||
|   "Etcd": { | ||||
|     "Insecure": true | ||||
|   }, | ||||
|   "Service": { | ||||
|     "Name": "DysonNetwork.Develop", | ||||
|     "Url": "https://localhost:7192" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										1
									
								
								DysonNetwork.Drive/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								DysonNetwork.Drive/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | ||||
| /Uploads/ | ||||
| /Client/node_modules/ | ||||
| /wwwroot/dist | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System.Linq.Expressions; | ||||
| using System.Reflection; | ||||
| using DysonNetwork.Drive.Billing; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -16,6 +17,9 @@ public class AppDatabase( | ||||
| ) : DbContext(options) | ||||
| { | ||||
|     public DbSet<FilePool> Pools { get; set; } = null!; | ||||
|     public DbSet<FileBundle> Bundles { get; set; } = null!; | ||||
|      | ||||
|     public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!; | ||||
|      | ||||
|     public DbSet<CloudFile> Files { get; set; } = null!; | ||||
|     public DbSet<CloudFileReference> FileReferences { get; set; } = null!; | ||||
| @@ -27,7 +31,6 @@ public class AppDatabase( | ||||
|             opt => opt | ||||
|                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) | ||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) | ||||
|                 .UseNetTopologySuite() | ||||
|                 .UseNodaTime() | ||||
|         ).UseSnakeCaseNamingConvention(); | ||||
|  | ||||
|   | ||||
							
								
								
									
										28
									
								
								DysonNetwork.Drive/Billing/Quota.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								DysonNetwork.Drive/Billing/Quota.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| using DysonNetwork.Shared.Data; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Billing; | ||||
|  | ||||
| /// <summary> | ||||
| /// The quota record stands for the extra quota that a user has. | ||||
| /// For normal users, the quota is 1GiB. | ||||
| /// For stellar program t1 users, the quota is 5GiB | ||||
| /// For stellar program t2 users, the quota is 10GiB | ||||
| /// For stellar program t3 users, the quota is 15GiB | ||||
| /// | ||||
| /// If users want to increase the quota, they need to pay for it. | ||||
| /// Each 1NSD they paid for one GiB. | ||||
| /// | ||||
| /// But the quota record unit is MiB, the minimal billable unit. | ||||
| /// </summary> | ||||
| public class QuotaRecord : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     public Guid AccountId { get; set; } | ||||
|     public string Name { get; set; } = string.Empty; | ||||
|     public string Description { get; set; } = string.Empty; | ||||
|      | ||||
|     public long Quota { get; set; } | ||||
|      | ||||
|     public Instant? ExpiredAt { get; set; } | ||||
| } | ||||
							
								
								
									
										66
									
								
								DysonNetwork.Drive/Billing/QuotaController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								DysonNetwork.Drive/Billing/QuotaController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Billing; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/billing/quota")] | ||||
| public class QuotaController(AppDatabase db, QuotaService quota) : ControllerBase | ||||
| { | ||||
|     public class QuotaDetails | ||||
|     { | ||||
|         public long BasedQuota { get; set; } | ||||
|         public long ExtraQuota { get; set; } | ||||
|         public long TotalQuota { get; set; } | ||||
|     } | ||||
|      | ||||
|     [HttpGet] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<QuotaDetails>> GetQuota() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|          | ||||
|         var (based, extra) = await quota.GetQuotaVerbose(accountId); | ||||
|         return Ok(new QuotaDetails | ||||
|         { | ||||
|             BasedQuota = based, | ||||
|             ExtraQuota = extra, | ||||
|             TotalQuota = based + extra | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     [HttpGet("records")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<QuotaRecord>>> GetQuotaRecords( | ||||
|         [FromQuery] bool expired = false, | ||||
|         [FromQuery] int offset = 0, | ||||
|         [FromQuery] int take = 20 | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var query = db.QuotaRecords | ||||
|             .Where(r => r.AccountId == accountId) | ||||
|             .AsQueryable(); | ||||
|         if (!expired) | ||||
|             query = query | ||||
|                 .Where(r => !r.ExpiredAt.HasValue || r.ExpiredAt > now); | ||||
|  | ||||
|         var total = await query.CountAsync(); | ||||
|         Response.Headers.Append("X-Total", total.ToString()); | ||||
|  | ||||
|         var records = await query | ||||
|             .OrderByDescending(r => r.CreatedAt) | ||||
|             .Skip(offset) | ||||
|             .Take(take) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         return Ok(records); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										69
									
								
								DysonNetwork.Drive/Billing/QuotaService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								DysonNetwork.Drive/Billing/QuotaService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Billing; | ||||
|  | ||||
| public class QuotaService( | ||||
|     AppDatabase db, | ||||
|     UsageService usage, | ||||
|     AccountService.AccountServiceClient accounts, | ||||
|     ICacheService cache | ||||
| ) | ||||
| { | ||||
|     public async Task<(bool ok, long billable, long quota)> IsFileAcceptable(Guid accountId, double costMultiplier, long newFileSize) | ||||
|     { | ||||
|         // The billable unit is MiB | ||||
|         var billableUnit = (long)Math.Ceiling(newFileSize / 1024.0 / 1024.0 * costMultiplier); | ||||
|         var totalBillableUsage = await usage.GetTotalBillableUsage(accountId); | ||||
|         var quota = await GetQuota(accountId); | ||||
|         return (totalBillableUsage + billableUnit <= quota, billableUnit, quota); | ||||
|     } | ||||
|  | ||||
|     public async Task<long> GetQuota(Guid accountId) | ||||
|     { | ||||
|         var cacheKey = $"file:quota:{accountId}"; | ||||
|         var cachedResult = await cache.GetAsync<long?>(cacheKey); | ||||
|         if (cachedResult.HasValue) return cachedResult.Value; | ||||
|          | ||||
|         var (based, extra) = await GetQuotaVerbose(accountId); | ||||
|         var quota = based + extra; | ||||
|         await cache.SetAsync(cacheKey, quota); | ||||
|         return quota; | ||||
|     } | ||||
|      | ||||
|     public async Task<(long based, long extra)> GetQuotaVerbose(Guid accountId) | ||||
|     { | ||||
|          | ||||
|  | ||||
|         var response = await accounts.GetAccountAsync(new GetAccountRequest { Id = accountId.ToString() }); | ||||
|         var perkSubscription = response.PerkSubscription; | ||||
|  | ||||
|         // The base quota is 1GiB, T1 is 5GiB, T2 is 10GiB, T3 is 15GiB | ||||
|         var basedQuota = 1L; | ||||
|         if (perkSubscription != null) | ||||
|         { | ||||
|             var privilege = PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(perkSubscription.Identifier); | ||||
|             basedQuota = privilege switch | ||||
|             { | ||||
|                 1 => 5L, | ||||
|                 2 => 10L, | ||||
|                 3 => 15L, | ||||
|                 _ => basedQuota | ||||
|             }; | ||||
|         } | ||||
|          | ||||
|         // The based quota is in GiB, we need to convert it to MiB | ||||
|         basedQuota *= 1024L; | ||||
|          | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var extraQuota = await db.QuotaRecords | ||||
|             .Where(e => e.AccountId == accountId) | ||||
|             .Where(e => !e.ExpiredAt.HasValue || e.ExpiredAt > now) | ||||
|             .SumAsync(e => e.Quota); | ||||
|          | ||||
|         return (basedQuota, extraQuota); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										49
									
								
								DysonNetwork.Drive/Billing/UsageController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								DysonNetwork.Drive/Billing/UsageController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Billing; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("api/billing/usage")] | ||||
| public class UsageController(UsageService usage, QuotaService quota, ICacheService cache) : ControllerBase | ||||
| { | ||||
|     [HttpGet] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<TotalUsageDetails>> GetTotalUsage() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|          | ||||
|         var cacheKey = $"file:usage:{accountId}"; | ||||
|          | ||||
|         // Try to get from cache first | ||||
|         var (found, cachedResult) = await cache.GetAsyncWithStatus<TotalUsageDetails>(cacheKey); | ||||
|         if (found && cachedResult != null) | ||||
|             return Ok(cachedResult); | ||||
|  | ||||
|         // If not in cache, get from services | ||||
|         var result = await usage.GetTotalUsage(accountId); | ||||
|         var totalQuota = await quota.GetQuota(accountId); | ||||
|         result.TotalQuota = totalQuota; | ||||
|  | ||||
|         // Cache the result for 5 minutes | ||||
|         await cache.SetAsync(cacheKey, result, TimeSpan.FromMinutes(5)); | ||||
|  | ||||
|         return Ok(result); | ||||
|     } | ||||
|  | ||||
|     [Authorize] | ||||
|     [HttpGet("{poolId:guid}")] | ||||
|     public async Task<ActionResult<UsageDetails>> GetPoolUsage(Guid poolId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|          | ||||
|         var usageDetails = await usage.GetPoolUsage(poolId, accountId); | ||||
|         if (usageDetails == null) | ||||
|             return NotFound(); | ||||
|         return usageDetails; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										121
									
								
								DysonNetwork.Drive/Billing/UsageService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								DysonNetwork.Drive/Billing/UsageService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Billing; | ||||
|  | ||||
| public class UsageDetails | ||||
| { | ||||
|     public required Guid PoolId { get; set; } | ||||
|     public required string PoolName { get; set; } | ||||
|     public required long UsageBytes { get; set; } | ||||
|     public required double Cost { get; set; } | ||||
|     public required long FileCount { get; set; } | ||||
| } | ||||
|  | ||||
| public class TotalUsageDetails | ||||
| { | ||||
|     public required List<UsageDetails> PoolUsages { get; set; } | ||||
|     public required long TotalUsageBytes { get; set; } | ||||
|     public required long TotalFileCount { get; set; } | ||||
|      | ||||
|     // Quota, cannot be loaded in the service, cause circular dependency | ||||
|     // Let the controller do the calculation | ||||
|     public long? TotalQuota { get; set; } | ||||
|     public long? UsedQuota { get; set; } | ||||
| } | ||||
|  | ||||
| public class UsageService(AppDatabase db) | ||||
| { | ||||
|     public async Task<TotalUsageDetails> GetTotalUsage(Guid accountId) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var fileQuery = db.Files | ||||
|             .Where(f => !f.IsMarkedRecycle) | ||||
|             .Where(f => !f.ExpiredAt.HasValue || f.ExpiredAt > now) | ||||
|             .Where(f => f.AccountId == accountId) | ||||
|             .AsQueryable(); | ||||
|          | ||||
|         var poolUsages = await db.Pools | ||||
|             .Select(p => new UsageDetails | ||||
|             { | ||||
|                 PoolId = p.Id, | ||||
|                 PoolName = p.Name, | ||||
|                 UsageBytes = fileQuery | ||||
|                     .Where(f => f.PoolId == p.Id) | ||||
|                     .Sum(f => f.Size), | ||||
|                 Cost = fileQuery | ||||
|                            .Where(f => f.PoolId == p.Id) | ||||
|                            .Sum(f => f.Size) / 1024.0 / 1024.0 * | ||||
|                        (p.BillingConfig.CostMultiplier ?? 1.0), | ||||
|                 FileCount = fileQuery | ||||
|                     .Count(f => f.PoolId == p.Id) | ||||
|             }) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var totalUsage = poolUsages.Sum(p => p.UsageBytes); | ||||
|         var totalFileCount = poolUsages.Sum(p => p.FileCount); | ||||
|  | ||||
|         return new TotalUsageDetails | ||||
|         { | ||||
|             PoolUsages = poolUsages, | ||||
|             TotalUsageBytes = totalUsage, | ||||
|             TotalFileCount = totalFileCount, | ||||
|             UsedQuota = await GetTotalBillableUsage(accountId) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public async Task<UsageDetails?> GetPoolUsage(Guid poolId, Guid accountId) | ||||
|     { | ||||
|         var pool = await db.Pools.FindAsync(poolId); | ||||
|         if (pool == null) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|          | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var fileQuery = db.Files | ||||
|             .Where(f => !f.IsMarkedRecycle) | ||||
|             .Where(f => f.ExpiredAt.HasValue && f.ExpiredAt > now) | ||||
|             .Where(f => f.AccountId == accountId) | ||||
|             .AsQueryable(); | ||||
|  | ||||
|         var usageBytes = await fileQuery | ||||
|             .SumAsync(f => f.Size); | ||||
|  | ||||
|         var fileCount = await fileQuery | ||||
|             .CountAsync(); | ||||
|  | ||||
|         var cost = usageBytes / 1024.0 / 1024.0 * | ||||
|                    (pool.BillingConfig.CostMultiplier ?? 1.0); | ||||
|  | ||||
|         return new UsageDetails | ||||
|         { | ||||
|             PoolId = pool.Id, | ||||
|             PoolName = pool.Name, | ||||
|             UsageBytes = usageBytes, | ||||
|             Cost = cost, | ||||
|             FileCount = fileCount | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public async Task<long> GetTotalBillableUsage(Guid accountId) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var files = await db.Files | ||||
|             .Where(f => f.AccountId == accountId) | ||||
|             .Where(f => f.PoolId.HasValue) | ||||
|             .Where(f => !f.IsMarkedRecycle) | ||||
|             .Include(f => f.Pool) | ||||
|             .Where(f => !f.ExpiredAt.HasValue || f.ExpiredAt > now) | ||||
|             .Select(f => new | ||||
|             { | ||||
|                 f.Size, | ||||
|                 Multiplier = f.Pool!.BillingConfig.CostMultiplier ?? 1.0 | ||||
|             }) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var totalCost = files.Sum(f => f.Size * f.Multiplier) / 1024.0 / 1024.0; | ||||
|  | ||||
|         return (long)Math.Ceiling(totalCost); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										1
									
								
								DysonNetwork.Drive/Client/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								DysonNetwork.Drive/Client/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -8,6 +8,7 @@ pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
|  | ||||
| node_modules | ||||
| **/node_modules/highlight.js/ | ||||
| .DS_Store | ||||
| dist | ||||
| dist-ssr | ||||
|   | ||||
| @@ -11,10 +11,12 @@ | ||||
|         "@vueuse/core": "^13.5.0", | ||||
|         "aspnet-prerendering": "^3.0.1", | ||||
|         "cfturnstile-vue3": "^2.0.0", | ||||
|         "chart.js": "^4.5.0", | ||||
|         "pinia": "^3.0.3", | ||||
|         "tailwindcss": "^4.1.11", | ||||
|         "tus-js-client": "^4.3.1", | ||||
|         "vue": "^3.5.17", | ||||
|         "vue-chartjs": "^5.3.2", | ||||
|         "vue-router": "^4.5.1", | ||||
|       }, | ||||
|       "devDependencies": { | ||||
| @@ -162,6 +164,8 @@ | ||||
|  | ||||
|     "@juggle/resize-observer": ["@juggle/resize-observer@3.4.0", "", {}, "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="], | ||||
|  | ||||
|     "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], | ||||
|  | ||||
|     "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], | ||||
|  | ||||
|     "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], | ||||
| @@ -400,6 +404,8 @@ | ||||
|  | ||||
|     "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], | ||||
|  | ||||
|     "chart.js": ["chart.js@4.5.0", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ=="], | ||||
|  | ||||
|     "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], | ||||
|  | ||||
|     "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], | ||||
| @@ -854,6 +860,8 @@ | ||||
|  | ||||
|     "vue": ["vue@3.5.17", "", { "dependencies": { "@vue/compiler-dom": "3.5.17", "@vue/compiler-sfc": "3.5.17", "@vue/runtime-dom": "3.5.17", "@vue/server-renderer": "3.5.17", "@vue/shared": "3.5.17" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g=="], | ||||
|  | ||||
|     "vue-chartjs": ["vue-chartjs@5.3.2", "", { "peerDependencies": { "chart.js": "^4.1.1", "vue": "^3.0.0-0 || ^2.7.0" } }, "sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw=="], | ||||
|  | ||||
|     "vue-eslint-parser": ["vue-eslint-parser@10.2.0", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw=="], | ||||
|  | ||||
|     "vue-router": ["vue-router@4.5.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw=="], | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| <html lang=""> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" href="/favicon.ico" /> | ||||
|     <link rel="icon" href="/favicon.png" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Solar Network Drive</title> | ||||
|     <app-data /> | ||||
|   | ||||
| @@ -22,10 +22,12 @@ | ||||
|     "@vueuse/core": "^13.5.0", | ||||
|     "aspnet-prerendering": "^3.0.1", | ||||
|     "cfturnstile-vue3": "^2.0.0", | ||||
|     "chart.js": "^4.5.0", | ||||
|     "pinia": "^3.0.3", | ||||
|     "tailwindcss": "^4.1.11", | ||||
|     "tus-js-client": "^4.3.1", | ||||
|     "vue": "^3.5.17", | ||||
|     "vue-chartjs": "^5.3.2", | ||||
|     "vue-router": "^4.5.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								DysonNetwork.Drive/Client/public/favicon.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								DysonNetwork.Drive/Client/public/favicon.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 70 KiB | 
							
								
								
									
										50
									
								
								DysonNetwork.Drive/Client/src/components/BundleSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								DysonNetwork.Drive/Client/src/components/BundleSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| <template> | ||||
|   <n-select | ||||
|     v-model:value="selectedBundle" | ||||
|     :options="options" | ||||
|     placeholder="Select a bundle" | ||||
|     @update:value="handleBundleChange" | ||||
|     filterable | ||||
|     remote | ||||
|     :loading="loading" | ||||
|     @search="handleSearch" | ||||
|     clearable | ||||
|   /> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { NSelect } from 'naive-ui' | ||||
| import { ref, onMounted } from 'vue' | ||||
|  | ||||
| const emit = defineEmits(['update:bundle']) | ||||
|  | ||||
| const selectedBundle = ref<string | null>(null) | ||||
| const loading = ref(false) | ||||
| const options = ref<any[]>([]) | ||||
|  | ||||
| async function fetchBundles(term: string | null = null) { | ||||
|   loading.value = true | ||||
|   try { | ||||
|     const resp = await fetch(`/api/bundles/me?${term ? `term=${term}` : ''}`) | ||||
|     const data = await resp.json() | ||||
|     options.value = data.map((bundle: any) => ({ | ||||
|       label: bundle.name, | ||||
|       value: bundle.id, | ||||
|     })) | ||||
|   } catch (error) { | ||||
|     console.error('Failed to fetch bundles:', error) | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
|  | ||||
| function handleSearch(query: string) { | ||||
|   fetchBundles(query) | ||||
| } | ||||
|  | ||||
| function handleBundleChange(value: string) { | ||||
|   emit('update:bundle', value) | ||||
| } | ||||
|  | ||||
| onMounted(() => fetchBundles()) | ||||
| </script> | ||||
							
								
								
									
										199
									
								
								DysonNetwork.Drive/Client/src/components/FilePoolSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								DysonNetwork.Drive/Client/src/components/FilePoolSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | ||||
| <template> | ||||
|   <n-select | ||||
|     :value="modelValue" | ||||
|     @update:value="onUpdate" | ||||
|     :options="pools ?? []" | ||||
|     :render-label="renderPoolSelectLabel" | ||||
|     :render-tag="renderSingleSelectTag" | ||||
|     value-field="id" | ||||
|     label-field="name" | ||||
|     :placeholder="props.placeholder || 'Select a file pool to upload'" | ||||
|     :size="props.size || 'large'" | ||||
|     clearable | ||||
|   /> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { | ||||
|   NSelect, | ||||
|   NTag, | ||||
|   NDivider, | ||||
|   NTooltip, | ||||
|   type SelectOption, | ||||
|   type SelectRenderTag, | ||||
| } from 'naive-ui' | ||||
| import { h, onMounted, ref, watch } from 'vue' | ||||
| import type { SnFilePool } from '@/types/pool' | ||||
| import { formatBytes } from '@/views/format' | ||||
|  | ||||
| const props = defineProps<{ | ||||
|   modelValue: string | null | ||||
|   placeholder?: string | undefined | ||||
|   size?: 'tiny' | 'small' | 'medium' | 'large' | undefined | ||||
| }>() | ||||
|  | ||||
| const emit = defineEmits(['update:modelValue', 'update:pool']) | ||||
|  | ||||
| type SnFilePoolOption = SnFilePool & any | ||||
|  | ||||
| const pools = ref<SnFilePoolOption[] | undefined>() | ||||
| async function fetchPools() { | ||||
|   const resp = await fetch('/api/pools') | ||||
|   pools.value = await resp.json() | ||||
| } | ||||
| onMounted(() => fetchPools()) | ||||
|  | ||||
| function onUpdate(value: string | null) { | ||||
|   emit('update:modelValue', value) | ||||
|   if (value === null) { | ||||
|     emit('update:pool', null) | ||||
|     return | ||||
|   } | ||||
|   if (pools.value) { | ||||
|     const pool = pools.value.find((p) => p.id === value) ?? null | ||||
|     emit('update:pool', pool) | ||||
|   } | ||||
| } | ||||
|  | ||||
| watch(pools, (newPools) => { | ||||
|   if (props.modelValue && newPools) { | ||||
|     const pool = newPools.find((p) => p.id === props.modelValue) ?? null | ||||
|     emit('update:pool', pool) | ||||
|   } | ||||
| }) | ||||
|  | ||||
| const renderSingleSelectTag: SelectRenderTag = ({ option }) => { | ||||
|   return h( | ||||
|     'div', | ||||
|     { | ||||
|       style: { | ||||
|         display: 'flex', | ||||
|         alignItems: 'center', | ||||
|       }, | ||||
|     }, | ||||
|     [option.name as string], | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const perkPrivilegeList = ['Stellar', 'Nova', 'Supernova'] | ||||
|  | ||||
| function renderPoolSelectLabel(option: SelectOption & SnFilePool) { | ||||
|   const policy: any = option.policy_config | ||||
|   return h( | ||||
|     'div', | ||||
|     { | ||||
|       style: { | ||||
|         padding: '8px 2px', | ||||
|       }, | ||||
|     }, | ||||
|     [ | ||||
|       h('div', null, [option.name as string]), | ||||
|       option.description && | ||||
|         h( | ||||
|           'div', | ||||
|           { | ||||
|             style: { | ||||
|               fontSize: '0.875rem', | ||||
|               opacity: '0.75', | ||||
|             }, | ||||
|           }, | ||||
|           option.description, | ||||
|         ), | ||||
|       h( | ||||
|         'div', | ||||
|         { | ||||
|           style: { | ||||
|             display: 'flex', | ||||
|             marginBottom: '4px', | ||||
|             fontSize: '0.75rem', | ||||
|             opacity: '0.75', | ||||
|           }, | ||||
|         }, | ||||
|         [ | ||||
|           policy.max_file_size && h('span', `Max ${formatBytes(policy.max_file_size)}`), | ||||
|           policy.accept_types && | ||||
|             h( | ||||
|               NTooltip, | ||||
|               {}, | ||||
|               { | ||||
|                 trigger: () => h('span', `Accept limited types`), | ||||
|                 default: () => h('span', policy.accept_types.join(', ')), | ||||
|               }, | ||||
|             ), | ||||
|           policy.require_privilege && | ||||
|             h('span', `Require ${perkPrivilegeList[policy.require_privilege - 1]} Program`), | ||||
|           h('span', `Cost x${option.billing_config.cost_multiplier.toFixed(1)}`), | ||||
|         ] | ||||
|           .filter((el) => el) | ||||
|           .flatMap((el, idx, arr) => | ||||
|             idx < arr.length - 1 ? [el, h(NDivider, { vertical: true })] : [el], | ||||
|           ), | ||||
|       ), | ||||
|       h( | ||||
|         'div', | ||||
|         { | ||||
|           style: { | ||||
|             display: 'flex', | ||||
|             gap: '0.25rem', | ||||
|             marginTop: '2px', | ||||
|             marginLeft: '-2px', | ||||
|             marginRight: '-2px', | ||||
|           }, | ||||
|         }, | ||||
|         [ | ||||
|           policy.public_usable && | ||||
|             h( | ||||
|               NTag, | ||||
|               { | ||||
|                 type: 'info', | ||||
|                 size: 'small', | ||||
|                 round: true, | ||||
|               }, | ||||
|               { default: () => 'Public Shared' }, | ||||
|             ), | ||||
|           policy.public_indexable && | ||||
|             h( | ||||
|               NTag, | ||||
|               { | ||||
|                 type: 'success', | ||||
|                 size: 'small', | ||||
|                 round: true, | ||||
|               }, | ||||
|               { default: () => 'Public Indexable' }, | ||||
|             ), | ||||
|           policy.allow_encryption && | ||||
|             h( | ||||
|               NTag, | ||||
|               { | ||||
|                 type: 'warning', | ||||
|                 size: 'small', | ||||
|                 round: true, | ||||
|               }, | ||||
|               { default: () => 'Allow Encryption' }, | ||||
|             ), | ||||
|           policy.allow_anonymous && | ||||
|             h( | ||||
|               NTag, | ||||
|               { | ||||
|                 type: 'info', | ||||
|                 size: 'small', | ||||
|                 round: true, | ||||
|               }, | ||||
|               { default: () => 'Allow Anonymous' }, | ||||
|             ), | ||||
|           policy.enable_recycle && | ||||
|             h( | ||||
|               NTag, | ||||
|               { | ||||
|                 type: 'info', | ||||
|                 size: 'small', | ||||
|                 round: true, | ||||
|               }, | ||||
|               { default: () => 'Recycle Enabled' }, | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|     ], | ||||
|   ) | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										271
									
								
								DysonNetwork.Drive/Client/src/components/UploadArea.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										271
									
								
								DysonNetwork.Drive/Client/src/components/UploadArea.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,271 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-collapse-transition :show="showRecycleHint"> | ||||
|       <n-alert size="small" type="warning" title="Recycle Enabled" class="mb-3"> | ||||
|         You're uploading to a pool which enabled recycle. If the file you uploaded didn't referenced | ||||
|         from the Solar Network. It will be marked and will be deleted some while later. | ||||
|       </n-alert> | ||||
|     </n-collapse-transition> | ||||
|  | ||||
|     <n-collapse-transition :show="modeAdvanced"> | ||||
|       <n-card title="Advance Options" size="small" class="mb-3"> | ||||
|         <div class="flex flex-col gap-3"> | ||||
|           <div> | ||||
|             <p class="pl-1 mb-0.5">File Password</p> | ||||
|             <n-input | ||||
|               v-model:value="filePass" | ||||
|               :disabled="!currentFilePool?.allow_encryption" | ||||
|               placeholder="Enter password to protect the file" | ||||
|               show-password-toggle | ||||
|               size="large" | ||||
|               type="password" | ||||
|               class="mb-2" | ||||
|             /> | ||||
|             <p class="pl-1 text-xs opacity-75 mt-[-4px]"> | ||||
|               Only available for Stellar Program and certian file pool. | ||||
|             </p> | ||||
|           </div> | ||||
|           <div> | ||||
|             <p class="pl-1 mb-0.5">File Expiration Date</p> | ||||
|             <n-date-picker | ||||
|               v-model:value="fileExpire" | ||||
|               type="datetime" | ||||
|               clearable | ||||
|               :is-date-disabled="disablePreviousDate" | ||||
|             /> | ||||
|           </div> | ||||
|           <div | ||||
|             v-if="currentFilePool?.policy_config?.enable_fast_upload || route.query.pool" | ||||
|             class="flex items-center gap-2" | ||||
|           > | ||||
|             <p class="pl-1 mb-0.5">Fast Upload</p> | ||||
|             <n-switch v-model:value="fastUpload" /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </n-card> | ||||
|     </n-collapse-transition> | ||||
|  | ||||
|     <n-upload | ||||
|       multiple | ||||
|       directory-dnd | ||||
|       with-credentials | ||||
|       show-preview-button | ||||
|       list-type="image" | ||||
|       show-download-button | ||||
|       :custom-request="customRequest" | ||||
|       :custom-download="customDownload" | ||||
|       :create-thumbnail-url="createThumbnailUrl" | ||||
|       @preview="customPreview" | ||||
|     > | ||||
|       <n-upload-dragger> | ||||
|         <div style="margin-bottom: 12px"> | ||||
|           <n-icon size="48" :depth="3"> | ||||
|             <cloud-upload-round /> | ||||
|           </n-icon> | ||||
|         </div> | ||||
|         <n-text style="font-size: 16px"> Click or drag a file to this area to upload </n-text> | ||||
|         <n-p depth="3" style="margin: 8px 0 0 0"> | ||||
|           Strictly prohibit from uploading sensitive information. For example, your bank card PIN or | ||||
|           your credit card expiry date. | ||||
|         </n-p> | ||||
|       </n-upload-dragger> | ||||
|     </n-upload> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { | ||||
|   NUpload, | ||||
|   NUploadDragger, | ||||
|   NIcon, | ||||
|   NText, | ||||
|   NP, | ||||
|   NInput, | ||||
|   NCollapseTransition, | ||||
|   NDatePicker, | ||||
|   NAlert, | ||||
|   NCard, | ||||
|   NSwitch, | ||||
|   type UploadCustomRequestOptions, | ||||
|   type UploadSettledFileInfo, | ||||
|   type UploadFileInfo, | ||||
|   useMessage, | ||||
| } from 'naive-ui' | ||||
| import { computed, ref } from 'vue' | ||||
| import { useRoute } from 'vue-router' | ||||
| import { CloudUploadRound } from '@vicons/material' | ||||
| import type { SnFilePool } from '@/types/pool' | ||||
|  | ||||
| import * as tus from 'tus-js-client' | ||||
|  | ||||
| const props = defineProps<{ | ||||
|   filePool: string | null | ||||
|   modeAdvanced: boolean | ||||
|   pools: SnFilePool[] | ||||
|   bundleId?: string | ||||
| }>() | ||||
|  | ||||
| const route = useRoute() | ||||
|  | ||||
| const filePass = ref<string>('') | ||||
| const fileExpire = ref<number | null>(null) | ||||
| const fastUpload = ref<boolean>(false) | ||||
|  | ||||
| const effectiveFilePool = computed(() => (route.query.pool as string) || props.filePool) | ||||
|  | ||||
| const currentFilePool = computed(() => { | ||||
|   if (!effectiveFilePool.value) return null | ||||
|   return props.pools?.find((pool) => pool.id === effectiveFilePool.value) ?? null | ||||
| }) | ||||
| const showRecycleHint = computed(() => { | ||||
|   if (!effectiveFilePool.value) return true | ||||
|   return currentFilePool.value?.policy_config?.enable_recycle || false | ||||
| }) | ||||
|  | ||||
| const messageDisplay = useMessage() | ||||
|  | ||||
| async function customRequest({ | ||||
|   file, | ||||
|   headers, | ||||
|   withCredentials, | ||||
|   onFinish, | ||||
|   onError, | ||||
|   onProgress, | ||||
| }: UploadCustomRequestOptions) { | ||||
|   if (fastUpload.value) { | ||||
|     const hash = await crypto.subtle.digest('SHA-256', await file.file!.arrayBuffer()) | ||||
|     const hashString = Array.from(new Uint8Array(hash)) | ||||
|       .map((b) => b.toString(16).padStart(2, '0')) | ||||
|       .join('') | ||||
|  | ||||
|     const resp = await fetch('/api/files/fast', { | ||||
|       method: 'POST', | ||||
|       headers: { 'Content-Type': 'application/json' }, | ||||
|       body: JSON.stringify({ | ||||
|         name: file.name, | ||||
|         size: file.file?.size, | ||||
|         hash: hashString, | ||||
|         mime_type: file.file?.type, | ||||
|         pool_id: effectiveFilePool.value, | ||||
|       }), | ||||
|     }) | ||||
|  | ||||
|     if (!resp.ok) { | ||||
|       messageDisplay.error(`Failed to get presigned URL: ${await resp.text()}`) | ||||
|       onError() | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     const respData = await resp.json() | ||||
|     const url = respData.fast_upload_link | ||||
|  | ||||
|     try { | ||||
|       const xhr = new XMLHttpRequest() | ||||
|       xhr.open('PUT', url, true) | ||||
|       xhr.upload.onprogress = (event) => { | ||||
|         if (event.lengthComputable) { | ||||
|           onProgress({ percent: (event.loaded / event.total) * 100 }) | ||||
|         } | ||||
|       } | ||||
|       xhr.onload = () => { | ||||
|         if (xhr.status >= 200 && xhr.status < 300) { | ||||
|           onFinish() | ||||
|         } else { | ||||
|           messageDisplay.error(`Upload failed: ${xhr.responseText}`) | ||||
|           onError() | ||||
|         } | ||||
|       } | ||||
|       xhr.onerror = () => { | ||||
|         messageDisplay.error('Upload failed due to a network error.') | ||||
|         onError() | ||||
|       } | ||||
|       xhr.send(file.file) | ||||
|     } catch (e) { | ||||
|       console.error(e) | ||||
|       messageDisplay.error(`Upload failed: ${e}`) | ||||
|       onError() | ||||
|     } | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   const requestHeaders: Record<string, string> = {} | ||||
|   if (effectiveFilePool.value) requestHeaders['X-FilePool'] = effectiveFilePool.value | ||||
|   if (filePass.value) requestHeaders['X-FilePass'] = filePass.value | ||||
|   if (fileExpire.value) requestHeaders['X-FileExpire'] = fileExpire.value.toString() | ||||
|   if (props.bundleId) requestHeaders['X-FileBundle'] = props.bundleId | ||||
|   const upload = new tus.Upload(file.file as any, { | ||||
|     endpoint: '/api/tus', | ||||
|     retryDelays: [0, 3000, 5000, 10000, 20000], | ||||
|     removeFingerprintOnSuccess: false, | ||||
|     uploadDataDuringCreation: false, | ||||
|     metadata: { | ||||
|       filename: file.name, | ||||
|       'content-type': file.type ?? 'application/octet-stream', | ||||
|     }, | ||||
|     headers: { | ||||
|       'X-DirectUpload': 'true', | ||||
|       ...requestHeaders, | ||||
|       ...headers, | ||||
|     }, | ||||
|     onShouldRetry: () => false, | ||||
|     onError: function (error) { | ||||
|       if (error instanceof tus.DetailedError) { | ||||
|         const failedBody = error.originalResponse?.getBody() | ||||
|         if (failedBody != null) | ||||
|           messageDisplay.error(`Upload failed: ${failedBody}`, { | ||||
|             duration: 10000, | ||||
|             closable: true, | ||||
|           }) | ||||
|       } | ||||
|       console.error('[DRIVE] Upload failed:', error) | ||||
|       onError() | ||||
|     }, | ||||
|     onProgress: function (bytesUploaded, bytesTotal) { | ||||
|       onProgress({ percent: (bytesUploaded / bytesTotal) * 100 }) | ||||
|     }, | ||||
|     onSuccess: function (payload) { | ||||
|       const rawInfo = payload.lastResponse.getHeader('x-fileinfo') | ||||
|       const jsonInfo = JSON.parse(rawInfo as string) | ||||
|       console.log('[DRIVE] Upload successful: ', jsonInfo) | ||||
|       file.url = `/api/files/${jsonInfo.id}` | ||||
|       file.type = jsonInfo.mime_type | ||||
|       onFinish() | ||||
|     }, | ||||
|     onBeforeRequest: function (req) { | ||||
|       const xhr = req.getUnderlyingObject() | ||||
|       xhr.withCredentials = withCredentials | ||||
|     }, | ||||
|   }) | ||||
|   upload.findPreviousUploads().then(function (previousUploads) { | ||||
|     if (previousUploads.length) { | ||||
|       upload.resumeFromPreviousUpload(previousUploads[0]) | ||||
|     } | ||||
|     upload.start() | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function createThumbnailUrl( | ||||
|   _file: File | null, | ||||
|   fileInfo: UploadSettledFileInfo, | ||||
| ): string | undefined { | ||||
|   if (!fileInfo) return undefined | ||||
|   return fileInfo.url ?? undefined | ||||
| } | ||||
|  | ||||
| function customDownload(file: UploadFileInfo) { | ||||
|   const { url } = file | ||||
|   if (!url) return | ||||
|   window.open(url.replace('/api', ''), '_blank') | ||||
| } | ||||
|  | ||||
| function customPreview(file: UploadFileInfo, detail: { event: MouseEvent }) { | ||||
|   detail.event.preventDefault() | ||||
|   const { url } = file | ||||
|   if (!url) return | ||||
|   window.open(url.replace('/api', ''), '_blank') | ||||
| } | ||||
|  | ||||
| function disablePreviousDate(ts: number) { | ||||
|   return ts <= Date.now() | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										75
									
								
								DysonNetwork.Drive/Client/src/components/form/BundleForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								DysonNetwork.Drive/Client/src/components/form/BundleForm.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| <template> | ||||
|   <n-form :model="formValue" :rules="rules" ref="formRef"> | ||||
|     <n-form-item label="Slug" path="slug"> | ||||
|       <n-input v-model:value="formValue.slug" placeholder="Input Slug" /> | ||||
|     </n-form-item> | ||||
|     <n-form-item label="Name" path="name"> | ||||
|       <n-input v-model:value="formValue.name" placeholder="Input Name" /> | ||||
|     </n-form-item> | ||||
|     <n-form-item label="Description" path="description"> | ||||
|       <n-input | ||||
|         v-model:value="formValue.description" | ||||
|         placeholder="Input Description" | ||||
|         type="textarea" | ||||
|       /> | ||||
|     </n-form-item> | ||||
|     <n-form-item label="Passcode" path="passcode"> | ||||
|       <n-input | ||||
|         v-model:value="formValue.passcode" | ||||
|         placeholder="Input Passcode" | ||||
|         type="password" | ||||
|       /> | ||||
|     </n-form-item> | ||||
|     <n-form-item label="Expired At" path="expiredAt"> | ||||
|       <n-date-picker v-model:value="formValue.expiredAt" type="datetime" /> | ||||
|     </n-form-item> | ||||
|   </n-form> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { | ||||
|   NForm, | ||||
|   NFormItem, | ||||
|   NInput, | ||||
|   NDatePicker, | ||||
|   type FormInst, | ||||
|   type FormRules, | ||||
| } from 'naive-ui' | ||||
| import { ref } from 'vue' | ||||
|  | ||||
| const formRef = ref<FormInst | null>(null) | ||||
|  | ||||
| const props = defineProps<{ value: any }>() | ||||
| const formValue = ref(props.value) | ||||
|  | ||||
| const rules: FormRules = { | ||||
|   slug: [ | ||||
|     { | ||||
|       max: 1024, | ||||
|       message: 'Slug can be at most 1024 characters long', | ||||
|     }, | ||||
|   ], | ||||
|   name: [ | ||||
|     { | ||||
|       max: 1024, | ||||
|       message: 'Name can be at most 1024 characters long', | ||||
|     }, | ||||
|   ], | ||||
|   description: [ | ||||
|     { | ||||
|       max: 8192, | ||||
|       message: 'Description can be at most 8192 characters long', | ||||
|     }, | ||||
|   ], | ||||
|   passcode: [ | ||||
|     { | ||||
|       max: 256, | ||||
|       message: 'Passcode can be at most 256 characters long', | ||||
|     }, | ||||
|   ], | ||||
| } | ||||
|  | ||||
| defineExpose({ | ||||
|   formRef, | ||||
| }) | ||||
| </script> | ||||
							
								
								
									
										7
									
								
								DysonNetwork.Drive/Client/src/dy-prefetch.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								DysonNetwork.Drive/Client/src/dy-prefetch.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| export {} | ||||
|  | ||||
| declare global { | ||||
|   interface Window { | ||||
|     DyPrefetch?: any | ||||
|   } | ||||
| } | ||||
							
								
								
									
										62
									
								
								DysonNetwork.Drive/Client/src/layouts/dashboard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								DysonNetwork.Drive/Client/src/layouts/dashboard.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| <template> | ||||
|   <n-layout has-sider class="h-full"> | ||||
|     <n-layout-sider bordered collapse-mode="width" :collapsed-width="64" :width="240" show-trigger> | ||||
|       <n-menu | ||||
|         :collapsed-width="64" | ||||
|         :collapsed-icon-size="22" | ||||
|         :options="menuOptions" | ||||
|         :value="route.name as string" | ||||
|         @update:value="updateMenuSelect" | ||||
|       /> | ||||
|     </n-layout-sider> | ||||
|     <n-layout> | ||||
|       <router-view /> | ||||
|     </n-layout> | ||||
|   </n-layout> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { | ||||
|   DataUsageRound, | ||||
|   AllInboxFilled, | ||||
|   PermDataSettingRound, | ||||
|   ShoppingBagRound, | ||||
| } from '@vicons/material' | ||||
| import { NIcon, NLayout, NLayoutSider, NMenu, type MenuOption } from 'naive-ui' | ||||
| import { h, type Component } from 'vue' | ||||
| import { RouterView, useRoute, useRouter } from 'vue-router' | ||||
|  | ||||
| const route = useRoute() | ||||
| const router = useRouter() | ||||
|  | ||||
| function renderIcon(icon: Component) { | ||||
|   return () => h(NIcon, null, { default: () => h(icon) }) | ||||
| } | ||||
|  | ||||
| const menuOptions: MenuOption[] = [ | ||||
|   { | ||||
|     label: 'Usage', | ||||
|     key: 'dashboardUsage', | ||||
|     icon: renderIcon(DataUsageRound), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Files', | ||||
|     key: 'dashboardFiles', | ||||
|     icon: renderIcon(AllInboxFilled), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Bundles', | ||||
|     key: 'dashboardBundles', | ||||
|     icon: renderIcon(ShoppingBagRound), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Quota', | ||||
|     key: 'dashboardQuota', | ||||
|     icon: renderIcon(PermDataSettingRound), | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| function updateMenuSelect(key: string) { | ||||
|   router.push({ name: key }) | ||||
| } | ||||
| </script> | ||||
| @@ -15,7 +15,7 @@ | ||||
|         </n-dropdown> | ||||
|       </div> | ||||
|     </n-layout-header> | ||||
|     <n-layout-content embedded content-style="padding: 24px;"> | ||||
|     <n-layout-content embedded> | ||||
|       <router-view /> | ||||
|     </n-layout-content> | ||||
|   </n-layout> | ||||
| @@ -28,12 +28,15 @@ import { | ||||
|   LogInOutlined, | ||||
|   PersonAddAlt1Outlined, | ||||
|   PersonOutlineRound, | ||||
|   DataUsageRound, | ||||
| } from '@vicons/material' | ||||
| import { useUserStore } from '@/stores/user' | ||||
| import { useRoute, useRouter } from 'vue-router' | ||||
| import { useServicesStore } from '@/stores/services' | ||||
|  | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| const router = useRouter() | ||||
| const route = useRoute() | ||||
|  | ||||
| const hideUserMenu = computed(() => { | ||||
| @@ -60,6 +63,14 @@ const guestOptions = [ | ||||
| ] | ||||
|  | ||||
| const userOptions = computed(() => [ | ||||
|   { | ||||
|     label: 'Dashboard', | ||||
|     key: 'dashboardUsage', | ||||
|     icon: () => | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(DataUsageRound), | ||||
|       }), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Profile', | ||||
|     key: 'profile', | ||||
| @@ -67,7 +78,7 @@ const userOptions = computed(() => [ | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(PersonOutlineRound), | ||||
|       }), | ||||
|   } | ||||
|   }, | ||||
| ]) | ||||
|  | ||||
| const servicesStore = useServicesStore() | ||||
| @@ -83,6 +94,8 @@ function handleGuestMenuSelect(key: string) { | ||||
| function handleUserMenuSelect(key: string) { | ||||
|   if (key === 'profile') { | ||||
|     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'accounts/me')!, '_blank') | ||||
|   } else { | ||||
|     router.push({ name: key }) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|   | ||||
| @@ -2,7 +2,15 @@ | ||||
| import LayoutDefault from './layouts/default.vue' | ||||
|  | ||||
| import { RouterView } from 'vue-router' | ||||
| import { NGlobalStyle, NConfigProvider, NMessageProvider, lightTheme, darkTheme } from 'naive-ui' | ||||
| import { | ||||
|   NGlobalStyle, | ||||
|   NConfigProvider, | ||||
|   NMessageProvider, | ||||
|   NDialogProvider, | ||||
|   NLoadingBarProvider, | ||||
|   lightTheme, | ||||
|   darkTheme, | ||||
| } from 'naive-ui' | ||||
| import { usePreferredDark } from '@vueuse/core' | ||||
| import { useUserStore } from './stores/user' | ||||
| import { onMounted } from 'vue' | ||||
| @@ -34,10 +42,14 @@ onMounted(() => { | ||||
| <template> | ||||
|   <n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme"> | ||||
|     <n-global-style /> | ||||
|     <n-loading-bar-provider> | ||||
|       <n-dialog-provider> | ||||
|         <n-message-provider placement="bottom"> | ||||
|           <layout-default> | ||||
|             <router-view /> | ||||
|           </layout-default> | ||||
|         </n-message-provider> | ||||
|       </n-dialog-provider> | ||||
|     </n-loading-bar-provider> | ||||
|   </n-config-provider> | ||||
| </template> | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { createRouter, createWebHistory } from 'vue-router' | ||||
| import { useUserStore } from '@/stores/user' | ||||
| import { useServicesStore } from '@/stores/services' | ||||
|  | ||||
| const router = createRouter({ | ||||
|   history: createWebHistory(import.meta.env.BASE_URL), | ||||
| @@ -7,26 +8,76 @@ const router = createRouter({ | ||||
|     { | ||||
|       path: '/', | ||||
|       name: 'index', | ||||
|       component: () => import('../views/index.vue') | ||||
|       component: () => import('../views/index.vue'), | ||||
|     }, | ||||
|     { | ||||
|       path: '/files', | ||||
|       path: '/files/:fileId', | ||||
|       name: 'files', | ||||
|       component: () => import('../views/files.vue'), | ||||
|     } | ||||
|   ] | ||||
|     }, | ||||
|     { | ||||
|       path: '/bundles/:bundleId', | ||||
|       name: 'bundleDetails', | ||||
|       component: () => import('../views/bundles.vue'), | ||||
|     }, | ||||
|     { | ||||
|       path: '/dashboard', | ||||
|       name: 'dashboard', | ||||
|       component: () => import('../layouts/dashboard.vue'), | ||||
|       meta: { requiresAuth: true }, | ||||
|       children: [ | ||||
|         { | ||||
|           path: 'usage', | ||||
|           name: 'dashboardUsage', | ||||
|           component: () => import('../views/dashboard/usage.vue'), | ||||
|           meta: { requiresAuth: true }, | ||||
|         }, | ||||
|         { | ||||
|           path: 'files', | ||||
|           name: 'dashboardFiles', | ||||
|           component: () => import('../views/dashboard/files.vue'), | ||||
|           meta: { requiresAuth: true }, | ||||
|         }, | ||||
|         { | ||||
|           path: 'bundles', | ||||
|           name: 'dashboardBundles', | ||||
|           component: () => import('../views/dashboard/bundles.vue'), | ||||
|           meta: { requiresAuth: true }, | ||||
|         }, | ||||
|         { | ||||
|           path: 'quotas', | ||||
|           name: 'dashboardQuota', | ||||
|           component: () => import('../views/dashboard/quotas.vue'), | ||||
|           meta: { requiresAuth: true }, | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|     { | ||||
|       path: '/:notFound(.*)', | ||||
|       name: 'errorNotFound', | ||||
|       component: () => import('../views/not-found.vue'), | ||||
|     }, | ||||
|   ], | ||||
| }) | ||||
|  | ||||
| router.beforeEach(async (to, from, next) => { | ||||
|   const userStore = useUserStore() | ||||
|   const servicesStore = useServicesStore() | ||||
|  | ||||
|   // Initialize user state if not already initialized | ||||
|   if (!userStore.user && localStorage.getItem('authToken')) { | ||||
|     await userStore.initialize() | ||||
|   if (!userStore.user) { | ||||
|     await userStore.fetchUser() | ||||
|   } | ||||
|  | ||||
|   if (to.matched.some((record) => record.meta.requiresAuth) && !userStore.isAuthenticated) { | ||||
|     next({ name: 'login', query: { redirect: to.fullPath } }) | ||||
|     window.open( | ||||
|       servicesStore.getSerivceUrl( | ||||
|         'DysonNetwork.Pass', | ||||
|         'login?redirect=' + encodeURIComponent(window.location.href), | ||||
|       )!, | ||||
|       '_blank', | ||||
|     ) | ||||
|     next('/') | ||||
|   } else { | ||||
|     next() | ||||
|   } | ||||
|   | ||||
| @@ -19,7 +19,7 @@ export const useServicesStore = defineStore('services', () => { | ||||
|   } | ||||
|  | ||||
|   function getSerivceUrl(serviceName: string, ...parts: string[]): string | null { | ||||
|     let baseUrl = services.value[serviceName] || null | ||||
|     const baseUrl = services.value[serviceName] || null | ||||
|     return baseUrl ? `${baseUrl}/${parts.join('/')}` : null | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,8 @@ export const useUserStore = defineStore('user', () => { | ||||
|   const isAuthenticated = computed(() => !!user.value) | ||||
|  | ||||
|   // Actions | ||||
|   async function fetchUser() { | ||||
|   async function fetchUser(reload = true) { | ||||
|     if (!reload && user.value) return | ||||
|     isLoading.value = true | ||||
|     error.value = null | ||||
|     try { | ||||
| @@ -21,9 +22,6 @@ export const useUserStore = defineStore('user', () => { | ||||
|  | ||||
|       if (!response.ok) { | ||||
|         // If the token is invalid, clear it and the user state | ||||
|         if (response.status === 401) { | ||||
|           logout() | ||||
|         } | ||||
|         throw new Error('Failed to fetch user information.') | ||||
|       } | ||||
|  | ||||
| @@ -36,13 +34,6 @@ export const useUserStore = defineStore('user', () => { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function logout() { | ||||
|     user.value = null | ||||
|     localStorage.removeItem('authToken') | ||||
|     // Optionally, redirect to login page | ||||
|     // router.push('/login') | ||||
|   } | ||||
|  | ||||
|   function initialize() { | ||||
|     const allowedOrigin = import.meta.env.DEV ? window.location.origin : 'https://id.solian.app' | ||||
|     window.addEventListener('message', (event) => { | ||||
| @@ -69,7 +60,6 @@ export const useUserStore = defineStore('user', () => { | ||||
|     error, | ||||
|     isAuthenticated, | ||||
|     fetchUser, | ||||
|     logout, | ||||
|     initialize, | ||||
|   } | ||||
| }) | ||||
|   | ||||
							
								
								
									
										37
									
								
								DysonNetwork.Drive/Client/src/types/pool.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								DysonNetwork.Drive/Client/src/types/pool.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| export interface SnFilePool { | ||||
|   id: string | ||||
|   name: string | ||||
|   description: string | ||||
|   storage_config: StorageConfig | ||||
|   billing_config: BillingConfig | ||||
|   policy_config: any | ||||
|   public_indexable: boolean | ||||
|   public_usable: boolean | ||||
|   no_optimization: boolean | ||||
|   no_metadata: boolean | ||||
|   allow_encryption: boolean | ||||
|   allow_anonymous: boolean | ||||
|   require_privilege: number | ||||
|   account_id: null | ||||
|   resource_identifier: string | ||||
|   created_at: Date | ||||
|   updated_at: Date | ||||
|   deleted_at: null | ||||
| } | ||||
|  | ||||
| export interface BillingConfig { | ||||
|   cost_multiplier: number | ||||
| } | ||||
|  | ||||
| export interface StorageConfig { | ||||
|   region: string | ||||
|   bucket: string | ||||
|   endpoint: string | ||||
|   secret_id: string | ||||
|   secret_key: string | ||||
|   enable_signed: boolean | ||||
|   enable_ssl: boolean | ||||
|   image_proxy: null | ||||
|   access_proxy: null | ||||
|   expiration: null | ||||
| } | ||||
							
								
								
									
										255
									
								
								DysonNetwork.Drive/Client/src/views/bundles.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								DysonNetwork.Drive/Client/src/views/bundles.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,255 @@ | ||||
| <template> | ||||
|   <section class="min-h-full relative flex items-center justify-center"> | ||||
|     <n-spin v-if="!bundleInfo && !error" /> | ||||
|     <n-result | ||||
|       status="404" | ||||
|       title="No bundle was found" | ||||
|       :description="error" | ||||
|       v-else-if="error === '404'" | ||||
|     /> | ||||
|  | ||||
|     <n-card class="max-w-md my-4 mx-8" v-else-if="error === '403'"> | ||||
|       <n-result | ||||
|         status="403" | ||||
|         title="Access Denied" | ||||
|         description="This bundle is protected by a passcode" | ||||
|         class="mt-5 mb-2" | ||||
|       > | ||||
|         <template #footer> | ||||
|           <n-alert v-if="passcodeError" type="error" class="mb-3"> | ||||
|             {{ passcodeError }} | ||||
|           </n-alert> | ||||
|           <n-input | ||||
|             v-model:value="passcode" | ||||
|             type="password" | ||||
|             show-password-on="mousedown" | ||||
|             placeholder="Passcode" | ||||
|             @keyup.enter="fetchBundleInfo" | ||||
|             class="mb-3" | ||||
|           /> | ||||
|           <n-button type="primary" block @click="fetchBundleInfo">Access Bundle</n-button> | ||||
|         </template> | ||||
|       </n-result> | ||||
|     </n-card> | ||||
|  | ||||
|     <n-card class="max-w-4xl my-4 mx-8" v-else> | ||||
|       <n-grid cols="1 m:2" x-gap="16" y-gap="16" responsive="screen"> | ||||
|         <n-gi> | ||||
|           <n-card title="Content" size="small"> | ||||
|             <n-list | ||||
|               size="small" | ||||
|               v-if="bundleInfo.files && bundleInfo.files.length > 0" | ||||
|               style="padding: 0" | ||||
|             > | ||||
|               <n-list-item v-for="file in bundleInfo.files" :key="file.id"> | ||||
|                 <n-thing :title="file.name" :description="formatBytes(file.size)"> | ||||
|                   <template #header-extra> | ||||
|                     <n-button text type="primary" @click="goToFileDetails(file.id)">View</n-button> | ||||
|                   </template> | ||||
|                 </n-thing> | ||||
|               </n-list-item> | ||||
|             </n-list> | ||||
|             <n-empty v-else description="No files in this bundle" /> | ||||
|             <template #footer> | ||||
|               <n-collapse-transition :show="!!downloadProgress"> | ||||
|                 <n-progress | ||||
|                   type="line" | ||||
|                   :percentage="downloadProgress" | ||||
|                   indicator-placement="inside" | ||||
|                   :status="downloadStatus" | ||||
|                   processing | ||||
|                   class="mb-4" | ||||
|                 /> | ||||
|               </n-collapse-transition> | ||||
|               <n-button | ||||
|                 type="primary" | ||||
|                 block | ||||
|                 :disabled="!bundleInfo.files || bundleInfo.files.length === 0 || downloading" | ||||
|                 @click="downloadAllFiles" | ||||
|               > | ||||
|                 Download All | ||||
|               </n-button> | ||||
|             </template> | ||||
|           </n-card> | ||||
|         </n-gi> | ||||
|  | ||||
|         <n-gi> | ||||
|           <n-card size="small"> | ||||
|             <h3 class="text-lg">{{ bundleInfo.name }}</h3> | ||||
|             <p class="mb-3" v-if="bundleInfo.description">{{ bundleInfo.description }}</p> | ||||
|             <div class="flex gap-2"> | ||||
|               <span class="flex-grow-1 flex items-center gap-2"> | ||||
|                 <n-icon> | ||||
|                   <calendar-today-round /> | ||||
|                 </n-icon> | ||||
|                 Expires At | ||||
|               </span> | ||||
|               <span>{{ | ||||
|                 bundleInfo.expiredAt ? new Date(bundleInfo.expiredAt).toLocaleString() : 'Never' | ||||
|               }}</span> | ||||
|             </div> | ||||
|             <div class="flex gap-2"> | ||||
|               <span class="flex-grow-1 flex items-center gap-2"> | ||||
|                 <n-icon> | ||||
|                   <lock-round /> | ||||
|                 </n-icon> | ||||
|                 Passcode Protected | ||||
|               </span> | ||||
|               <span>{{ bundleInfo.passcode ? 'Yes' : 'No' }}</span> | ||||
|             </div> | ||||
|           </n-card> | ||||
|           <n-input | ||||
|             v-model:value="filePass" | ||||
|             type="password" | ||||
|             size="large" | ||||
|             placeholder="File password file decrypt" | ||||
|             class="mt-3" | ||||
|           /> | ||||
|         </n-gi> | ||||
|       </n-grid> | ||||
|     </n-card> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { | ||||
|   NCard, | ||||
|   NResult, | ||||
|   NSpin, | ||||
|   NIcon, | ||||
|   NGrid, | ||||
|   NGi, | ||||
|   NList, | ||||
|   NListItem, | ||||
|   NThing, | ||||
|   NButton, | ||||
|   NEmpty, | ||||
|   NInput, | ||||
|   NAlert, | ||||
|   NProgress, | ||||
|   NCollapseTransition, | ||||
|   useMessage, | ||||
| } from 'naive-ui' | ||||
| import { CalendarTodayRound, LockRound } from '@vicons/material' | ||||
| import { useRoute, useRouter } from 'vue-router' | ||||
| import { onMounted, ref, watch } from 'vue' | ||||
|  | ||||
| import { formatBytes } from './format' // Assuming format.ts is in the same directory | ||||
| import { downloadAndDecryptFile } from './secure' | ||||
|  | ||||
| const route = useRoute() | ||||
| const router = useRouter() | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
| const bundleId = route.params.bundleId | ||||
| const passcode = ref<string>('') | ||||
| const passcodeError = ref<string | null>(null) | ||||
|  | ||||
| const filePass = ref<string>('') | ||||
|  | ||||
| const downloading = ref(false) | ||||
| const downloadProgress = ref<number | undefined>() | ||||
| const downloadStatus = ref<'success' | 'error' | 'info'>('info') | ||||
|  | ||||
| watch( | ||||
|   route, | ||||
|   (value) => { | ||||
|     if (value.query.passcode) passcode.value = value.query.passcode.toString() | ||||
|   }, | ||||
|   { immediate: true, deep: true }, | ||||
| ) | ||||
|  | ||||
| const bundleInfo = ref<any>(null) | ||||
| async function fetchBundleInfo() { | ||||
|   try { | ||||
|     let url = '/api/bundles/' + bundleId | ||||
|     if (passcode.value) { | ||||
|       url += `?passcode=${passcode.value}` | ||||
|     } | ||||
|     const resp = await fetch(url) | ||||
|     if (resp.status === 403) { | ||||
|       error.value = '403' | ||||
|       bundleInfo.value = null | ||||
|       if (passcode.value) { | ||||
|         passcodeError.value = 'Incorrect passcode.' | ||||
|       } | ||||
|       return | ||||
|     } | ||||
|     if (!resp.ok) { | ||||
|       throw new Error('Failed to fetch bundle info: ' + resp.statusText) | ||||
|     } | ||||
|     bundleInfo.value = await resp.json() | ||||
|     error.value = null | ||||
|     passcodeError.value = null | ||||
|   } catch (err) { | ||||
|     error.value = (err as Error).message | ||||
|   } | ||||
| } | ||||
| onMounted(() => fetchBundleInfo()) | ||||
|  | ||||
| function goToFileDetails(fileId: string) { | ||||
|   router.push({ path: `/files/${fileId}`, query: { passcode: passcode.value } }) | ||||
| } | ||||
|  | ||||
| const messageDisplay = useMessage() | ||||
|  | ||||
| async function downloadAllFiles() { | ||||
|   if (!bundleInfo.value || !bundleInfo.value.files || bundleInfo.value.files.length === 0) { | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   downloading.value = true | ||||
|   downloadProgress.value = 0 | ||||
|   downloadStatus.value = 'info' | ||||
|  | ||||
|   const totalFiles = bundleInfo.value.files.length | ||||
|   let completedDownloads = 0 | ||||
|  | ||||
|   for (const file of bundleInfo.value.files) { | ||||
|     let url = `/api/files/${file.id}` | ||||
|     if (passcode.value) { | ||||
|       url += `?passcode=${passcode.value}` | ||||
|     } | ||||
|  | ||||
|     if (file.is_encrypted) { | ||||
|       downloadAndDecryptFile(file, filePass.value, file.name, () => {}) | ||||
|         .catch((err) => { | ||||
|           messageDisplay.error('Download failed: ' + err.message, { | ||||
|             closable: true, | ||||
|             duration: 10000, | ||||
|           }) | ||||
|         }) | ||||
|         .finally(() => { | ||||
|           completedDownloads++ | ||||
|           downloadProgress.value = (completedDownloads / totalFiles) * 100 | ||||
|         }) | ||||
|     } else { | ||||
|       try { | ||||
|         const res = await fetch(url) | ||||
|         if (!res.ok) { | ||||
|           throw new Error(`Failed to download ${file.name}: ${res.statusText}`) | ||||
|         } | ||||
|         const blob = await res.blob() | ||||
|         const blobUrl = window.URL.createObjectURL(blob) | ||||
|         const a = document.createElement('a') | ||||
|         a.href = blobUrl | ||||
|         a.download = file.name || 'download' // fallback name | ||||
|         document.body.appendChild(a) | ||||
|         a.click() | ||||
|         a.remove() | ||||
|         window.URL.revokeObjectURL(blobUrl) | ||||
|  | ||||
|         if (completedDownloads === totalFiles) { | ||||
|           downloadStatus.value = 'success' | ||||
|         } | ||||
|       } catch (err) { | ||||
|         messageDisplay.error(`Download failed for ${file.name}: ${err}`) | ||||
|         downloadStatus.value = 'error' | ||||
|       } finally { | ||||
|         completedDownloads++ | ||||
|         downloadProgress.value = (completedDownloads / totalFiles) * 100 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										180
									
								
								DysonNetwork.Drive/Client/src/views/dashboard/bundles.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								DysonNetwork.Drive/Client/src/views/dashboard/bundles.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| <template> | ||||
|   <section class="h-full px-5 py-4"> | ||||
|     <n-data-table | ||||
|       remote | ||||
|       :row-key="(row) => row.id" | ||||
|       :columns="tableColumns" | ||||
|       :data="bundles" | ||||
|       :loading="loading" | ||||
|       :pagination="tablePagination" | ||||
|       @page-change="handlePageChange" | ||||
|     /> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { | ||||
|   NDataTable, | ||||
|   type DataTableColumns, | ||||
|   type PaginationProps, | ||||
|   useMessage, | ||||
|   useLoadingBar, | ||||
|   NButton, | ||||
|   NIcon, | ||||
|   NSpace, | ||||
|   useDialog, | ||||
| } from 'naive-ui' | ||||
| import { h, onMounted, ref } from 'vue' | ||||
| import { useRouter } from 'vue-router' | ||||
| import { DeleteRound } from '@vicons/material' | ||||
|  | ||||
| const router = useRouter() | ||||
|  | ||||
| const bundles = ref<any[]>([]) | ||||
|  | ||||
| const tableColumns: DataTableColumns<any> = [ | ||||
|   { | ||||
|     title: 'Name', | ||||
|     key: 'name', | ||||
|     render(row: any) { | ||||
|       return h( | ||||
|         NButton, | ||||
|         { | ||||
|           text: true, | ||||
|           onClick: () => { | ||||
|             router.push(`/bundles/${row.id}`) | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           default: () => row.name, | ||||
|         }, | ||||
|       ) | ||||
|     }, | ||||
|     maxWidth: 80, | ||||
|     ellipsis: true, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Description', | ||||
|     key: 'description', | ||||
|     maxWidth: 180, | ||||
|     ellipsis: true, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Expired At', | ||||
|     key: 'expired_at', | ||||
|     render(row: any) { | ||||
|       if (!row.expired_at) return 'Never' | ||||
|       return new Date(row.expired_at).toLocaleString() | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Created At', | ||||
|     key: 'created_at', | ||||
|     render(row: any) { | ||||
|       return new Date(row.created_at).toLocaleString() | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Updated At', | ||||
|     key: 'updated_at', | ||||
|     render(row: any) { | ||||
|       return new Date(row.updated_at).toLocaleString() | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Action', | ||||
|     key: 'action', | ||||
|     render(row: any) { | ||||
|       return h(NSpace, {}, [ | ||||
|         h( | ||||
|           NButton, | ||||
|           { | ||||
|             circle: true, | ||||
|             text: true, | ||||
|             type: 'error', | ||||
|             onClick: () => { | ||||
|               askDeleteBundle(row) | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             icon: () => h(NIcon, {}, { default: () => h(DeleteRound) }), | ||||
|           }, | ||||
|         ), | ||||
|       ]) | ||||
|     }, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const tablePagination = ref<PaginationProps>({ | ||||
|   page: 1, | ||||
|   itemCount: 0, | ||||
|   pageSize: 10, | ||||
|   showSizePicker: true, | ||||
|   pageSizes: [10, 20, 30, 40, 50], | ||||
| }) | ||||
|  | ||||
| async function fetchBundles() { | ||||
|   if (loading.value) return | ||||
|   try { | ||||
|     loading.value = true | ||||
|     const pag = tablePagination.value | ||||
|     const response = await fetch( | ||||
|       `/api/bundles/me?take=${pag.pageSize}&offset=${(pag.page! - 1) * pag.pageSize!}`, | ||||
|     ) | ||||
|     if (!response.ok) { | ||||
|       throw new Error('Network response was not ok') | ||||
|     } | ||||
|     const data = await response.json() | ||||
|     bundles.value = data | ||||
|     tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0') | ||||
|   } catch (error) { | ||||
|     messageDialog.error('Failed to fetch bundles: ' + (error as Error).message) | ||||
|     console.error('Failed to fetch bundles:', error) | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| onMounted(() => fetchBundles()) | ||||
|  | ||||
| function handlePageChange(page: number) { | ||||
|   tablePagination.value.page = page | ||||
|   fetchBundles() | ||||
| } | ||||
|  | ||||
| const loading = ref(false) | ||||
|  | ||||
| const messageDialog = useMessage() | ||||
| const loadingBar = useLoadingBar() | ||||
| const dialog = useDialog() | ||||
|  | ||||
| function askDeleteBundle(bundle: any) { | ||||
|   dialog.warning({ | ||||
|     title: 'Confirm', | ||||
|     content: `Are you sure you want to delete the bundle ${bundle.name}?`, | ||||
|     positiveText: 'Sure', | ||||
|     negativeText: 'Not Sure', | ||||
|     onPositiveClick: () => { | ||||
|       deleteBundle(bundle) | ||||
|     }, | ||||
|   }) | ||||
| } | ||||
|  | ||||
| async function deleteBundle(bundle: any) { | ||||
|   try { | ||||
|     loadingBar.start() | ||||
|     const response = await fetch(`/api/bundles/${bundle.id}`, { | ||||
|       method: 'DELETE', | ||||
|     }) | ||||
|     if (!response.ok) { | ||||
|       throw new Error('Network response was not ok') | ||||
|     } | ||||
|     tablePagination.value.page = 1 | ||||
|     await fetchBundles() | ||||
|     loadingBar.finish() | ||||
|     messageDialog.success('Bundle deleted successfully') | ||||
|   } catch (error) { | ||||
|     loadingBar.error() | ||||
|     messageDialog.error('Failed to delete bundle: ' + (error as Error).message) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										304
									
								
								DysonNetwork.Drive/Client/src/views/dashboard/files.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										304
									
								
								DysonNetwork.Drive/Client/src/views/dashboard/files.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,304 @@ | ||||
| <template> | ||||
|   <section class="h-full px-5 py-4"> | ||||
|     <div class="flex items-center gap-4 mb-3"> | ||||
|       <file-pool-select | ||||
|         v-model="filePool" | ||||
|         placeholder="Filter by file pool" | ||||
|         size="medium" | ||||
|         class="max-w-[480px]" | ||||
|         @update:pool="fetchFiles" | ||||
|       /> | ||||
|       <div class="flex items-center gap-2.5"> | ||||
|         <n-switch size="large" v-model:value="showRecycled"> | ||||
|           <template #checked>Recycled</template> | ||||
|           <template #unchecked>Unrecycled</template> | ||||
|         </n-switch> | ||||
|         <n-button | ||||
|           @click="askDeleteRecycledFiles" | ||||
|           v-if="showRecycled" | ||||
|           type="error" | ||||
|           circle | ||||
|           size="small" | ||||
|         > | ||||
|           <n-icon> | ||||
|             <delete-sweep-round /> | ||||
|           </n-icon> | ||||
|         </n-button> | ||||
|       </div> | ||||
|     </div> | ||||
|     <n-data-table | ||||
|       remote | ||||
|       :row-key="(row) => row.id" | ||||
|       :columns="tableColumns" | ||||
|       :data="files" | ||||
|       :loading="loading" | ||||
|       :pagination="tablePagination" | ||||
|       @page-change="handlePageChange" | ||||
|     /> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { | ||||
|   NDataTable, | ||||
|   NIcon, | ||||
|   NImage, | ||||
|   NButton, | ||||
|   NSpace, | ||||
|   type DataTableColumns, | ||||
|   type PaginationProps, | ||||
|   useDialog, | ||||
|   useMessage, | ||||
|   useLoadingBar, | ||||
|   NSwitch, | ||||
|   NTooltip, | ||||
| } from 'naive-ui' | ||||
| import { | ||||
|   AudioFileRound, | ||||
|   InsertDriveFileRound, | ||||
|   VideoFileRound, | ||||
|   FileDownloadOutlined, | ||||
|   DeleteRound, | ||||
|   DeleteSweepRound, | ||||
| } from '@vicons/material' | ||||
| import { h, onMounted, ref, watch } from 'vue' | ||||
| import { useRouter } from 'vue-router' | ||||
| import { formatBytes } from '../format' | ||||
| import FilePoolSelect from '@/components/FilePoolSelect.vue' | ||||
|  | ||||
| const router = useRouter() | ||||
|  | ||||
| const files = ref<any[]>([]) | ||||
|  | ||||
| const filePool = ref<string | null>(null) | ||||
| const showRecycled = ref(false) | ||||
|  | ||||
| const tableColumns: DataTableColumns<any> = [ | ||||
|   { | ||||
|     title: 'Preview', | ||||
|     key: 'preview', | ||||
|     render(row: any) { | ||||
|       switch (row.mime_type.split('/')[0]) { | ||||
|         case 'image': | ||||
|           return h(NImage, { | ||||
|             src: '/api/files/' + row.id, | ||||
|             width: 32, | ||||
|             height: 32, | ||||
|             objectFit: 'contain', | ||||
|             style: { aspectRatio: 1 }, | ||||
|           }) | ||||
|         case 'video': | ||||
|           return h(NIcon, { size: 32 }, { default: () => h(VideoFileRound) }) | ||||
|         case 'audio': | ||||
|           return h(NIcon, { size: 32 }, { default: () => h(AudioFileRound) }) | ||||
|         default: | ||||
|           return h(NIcon, { size: 32 }, { default: () => h(InsertDriveFileRound) }) | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Name', | ||||
|     key: 'name', | ||||
|     maxWidth: 180, | ||||
|     ellipsis: true, | ||||
|     render(row: any) { | ||||
|       return h( | ||||
|         NButton, | ||||
|         { | ||||
|           text: true, | ||||
|           onClick: () => { | ||||
|             router.push(`/files/${row.id}`) | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           default: () => row.name, | ||||
|         }, | ||||
|       ) | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Size', | ||||
|     key: 'size', | ||||
|     render(row: any) { | ||||
|       return formatBytes(row.size) | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Pool', | ||||
|     key: 'pool', | ||||
|     render(row: any) { | ||||
|       if (!row.pool) return 'Unstored' | ||||
|       return h( | ||||
|         NTooltip, | ||||
|         {}, | ||||
|         { | ||||
|           default: () => h('span', row.pool.id), | ||||
|           trigger: () => h('span', row.pool.name), | ||||
|         }, | ||||
|       ) | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Expired At', | ||||
|     key: 'expired_at', | ||||
|     render(row: any) { | ||||
|       if (!row.expired_at) return 'Never' | ||||
|       return new Date(row.expired_at).toLocaleString() | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Uploaded At', | ||||
|     key: 'created_at', | ||||
|     render(row: any) { | ||||
|       return new Date(row.created_at).toLocaleString() | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Action', | ||||
|     key: 'action', | ||||
|     render(row: any) { | ||||
|       return h(NSpace, {}, [ | ||||
|         h( | ||||
|           NButton, | ||||
|           { | ||||
|             circle: true, | ||||
|             text: true, | ||||
|             onClick: () => { | ||||
|               window.open(`/api/files/${row.id}`, '_blank') | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             icon: () => h(NIcon, {}, { default: () => h(FileDownloadOutlined) }), | ||||
|           }, | ||||
|         ), | ||||
|         h( | ||||
|           NButton, | ||||
|           { | ||||
|             circle: true, | ||||
|             text: true, | ||||
|             type: 'error', | ||||
|             onClick: () => { | ||||
|               askDeleteFile(row) | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             icon: () => h(NIcon, {}, { default: () => h(DeleteRound) }), | ||||
|           }, | ||||
|         ), | ||||
|       ]) | ||||
|     }, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const tablePagination = ref<PaginationProps>({ | ||||
|   page: 1, | ||||
|   itemCount: 0, | ||||
|   pageSize: 10, | ||||
|   showSizePicker: true, | ||||
|   pageSizes: [10, 20, 30, 40, 50], | ||||
| }) | ||||
|  | ||||
| async function fetchFiles() { | ||||
|   if (loading.value) return | ||||
|   try { | ||||
|     loading.value = true | ||||
|     const pag = tablePagination.value | ||||
|     const response = await fetch( | ||||
|       `/api/files/me?take=${pag.pageSize}&offset=${(pag.page! - 1) * pag.pageSize!}&recycled=${showRecycled.value}${filePool.value ? '&pool=' + filePool.value : ''}`, | ||||
|     ) | ||||
|     if (!response.ok) { | ||||
|       throw new Error('Network response was not ok') | ||||
|     } | ||||
|     const data = await response.json() | ||||
|     files.value = data | ||||
|     tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0') | ||||
|   } catch (error) { | ||||
|     console.error('Failed to fetch files:', error) | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| onMounted(() => fetchFiles()) | ||||
|  | ||||
| watch(showRecycled, () => { | ||||
|   tablePagination.value.itemCount = 0 | ||||
|   tablePagination.value.page = 1 | ||||
|   fetchFiles() | ||||
| }) | ||||
|  | ||||
| function handlePageChange(page: number) { | ||||
|   tablePagination.value.page = page | ||||
|   fetchFiles() | ||||
| } | ||||
|  | ||||
| const loading = ref(false) | ||||
|  | ||||
| const dialog = useDialog() | ||||
| const messageDialog = useMessage() | ||||
| const loadingBar = useLoadingBar() | ||||
|  | ||||
| function askDeleteFile(file: any) { | ||||
|   dialog.warning({ | ||||
|     title: 'Confirm', | ||||
|     content: `Are you sure you want delete ${file.name}? This will delete the stored file data immediately, there is no return.`, | ||||
|     positiveText: 'Sure', | ||||
|     negativeText: 'Not Sure', | ||||
|     draggable: true, | ||||
|     onPositiveClick: () => { | ||||
|       deleteFile(file) | ||||
|     }, | ||||
|   }) | ||||
| } | ||||
|  | ||||
| async function deleteFile(file: any) { | ||||
|   try { | ||||
|     loadingBar.start() | ||||
|     const response = await fetch(`/api/files/${file.id}`, { | ||||
|       method: 'DELETE', | ||||
|     }) | ||||
|     if (!response.ok) { | ||||
|       throw new Error('Network response was not ok') | ||||
|     } | ||||
|     tablePagination.value.page = 1 | ||||
|     await fetchFiles() | ||||
|     loadingBar.finish() | ||||
|     messageDialog.success('File deleted successfully') | ||||
|   } catch (error) { | ||||
|     loadingBar.error() | ||||
|     messageDialog.error('Failed to delete file: ' + (error as Error).message) | ||||
|   } | ||||
| } | ||||
|  | ||||
| function askDeleteRecycledFiles() { | ||||
|   dialog.warning({ | ||||
|     title: 'Confirm', | ||||
|     content: `Are you sure you want to delete all ${tablePagination.value.itemCount} marked recycled file(s) by system?`, | ||||
|     positiveText: 'Sure', | ||||
|     negativeText: 'Not Sure', | ||||
|     draggable: true, | ||||
|     onPositiveClick: () => { | ||||
|       deleteRecycledFiles() | ||||
|     }, | ||||
|   }) | ||||
| } | ||||
|  | ||||
| async function deleteRecycledFiles() { | ||||
|   try { | ||||
|     loadingBar.start() | ||||
|     const response = await fetch('/api/files/me/recycle', { | ||||
|       method: 'DELETE', | ||||
|     }) | ||||
|     if (!response.ok) { | ||||
|       throw new Error('Network response was not ok') | ||||
|     } | ||||
|     const resp = await response.json() | ||||
|     tablePagination.value.page = 1 | ||||
|     await fetchFiles() | ||||
|     loadingBar.finish() | ||||
|     messageDialog.success(`Recycled files deleted successfully, deleted count: ${resp.count}`) | ||||
|   } catch (error) { | ||||
|     loadingBar.error() | ||||
|     messageDialog.error('Failed to delete recycled files: ' + (error as Error).message) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										101
									
								
								DysonNetwork.Drive/Client/src/views/dashboard/quotas.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								DysonNetwork.Drive/Client/src/views/dashboard/quotas.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| <template> | ||||
|   <section class="h-full px-5 py-4"> | ||||
|     <n-data-table | ||||
|       remote | ||||
|       :row-key="(row) => row.id" | ||||
|       :columns="tableColumns" | ||||
|       :data="quotas" | ||||
|       :loading="loading" | ||||
|       :pagination="tablePagination" | ||||
|       @page-change="handlePageChange" | ||||
|     /> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { NDataTable, type DataTableColumns, type PaginationProps, useMessage } from 'naive-ui' | ||||
| import { onMounted, ref } from 'vue' | ||||
| import { formatBytes } from '../format' | ||||
|  | ||||
| const quotas = ref<any[]>([]) | ||||
|  | ||||
| const tableColumns: DataTableColumns<any> = [ | ||||
|   { | ||||
|     title: 'Name', | ||||
|     key: 'name', | ||||
|   }, | ||||
|   { | ||||
|     title: 'Description', | ||||
|     key: 'description', | ||||
|   }, | ||||
|   { | ||||
|     title: 'Quota', | ||||
|     key: 'quota', | ||||
|     render(row: any) { | ||||
|       return formatBytes(row.quota * 1024 * 1024) | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Expired At', | ||||
|     key: 'expired_at', | ||||
|     render(row: any) { | ||||
|       if (!row.expired_at) return 'Never' | ||||
|       return new Date(row.expired_at).toLocaleString() | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Created At', | ||||
|     key: 'created_at', | ||||
|     render(row: any) { | ||||
|       return new Date(row.created_at).toLocaleString() | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Updated At', | ||||
|     key: 'updated_at', | ||||
|     render(row: any) { | ||||
|       return new Date(row.updated_at).toLocaleString() | ||||
|     }, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const tablePagination = ref<PaginationProps>({ | ||||
|   page: 1, | ||||
|   itemCount: 0, | ||||
|   pageSize: 10, | ||||
|   showSizePicker: true, | ||||
|   pageSizes: [10, 20, 30, 40, 50], | ||||
| }) | ||||
|  | ||||
| async function fetchQuotas() { | ||||
|   if (loading.value) return | ||||
|   try { | ||||
|     loading.value = true | ||||
|     const pag = tablePagination.value | ||||
|     const response = await fetch( | ||||
|       `/api/billing/quota/records?take=${pag.pageSize}&offset=${(pag.page! - 1) * pag.pageSize!}`, | ||||
|     ) | ||||
|     if (!response.ok) { | ||||
|       throw new Error('Network response was not ok') | ||||
|     } | ||||
|     const data = await response.json() | ||||
|     quotas.value = data | ||||
|     tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0') | ||||
|   } catch (error) { | ||||
|     messageDialog.error('Failed to fetch quotas: ' + (error as Error).message) | ||||
|     console.error('Failed to fetch quotas:', error) | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| onMounted(() => fetchQuotas()) | ||||
|  | ||||
| function handlePageChange(page: number) { | ||||
|   tablePagination.value.page = page | ||||
|   fetchQuotas() | ||||
| } | ||||
|  | ||||
| const loading = ref(false) | ||||
|  | ||||
| const messageDialog = useMessage() | ||||
| </script> | ||||
							
								
								
									
										164
									
								
								DysonNetwork.Drive/Client/src/views/dashboard/usage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								DysonNetwork.Drive/Client/src/views/dashboard/usage.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| <template> | ||||
|   <section class="h-full container-fluid mx-auto py-4 px-5"> | ||||
|     <div class="h-full flex justify-center items-center" v-if="!usage"> | ||||
|       <n-spin /> | ||||
|     </div> | ||||
|     <n-grid cols="1 s:2 l:4" responsive="screen" :x-gap="16" :y-gap="16" v-else> | ||||
|       <n-gi span="4"> | ||||
|         <n-alert title="Billing Tips" size="small" type="info" closable> | ||||
|           <p> | ||||
|             The minimal billable unit is MiB, if your file is not enough 1 MiB it will be counted as | ||||
|             1 MiB. | ||||
|           </p> | ||||
|           <p>The <b>1 MiB = 1024 KiB = 1,048,576 B</b></p> | ||||
|         </n-alert> | ||||
|       </n-gi> | ||||
|       <n-gi> | ||||
|         <n-card class="h-stats"> | ||||
|           <n-statistic label="All Uploads" tabular-nums> | ||||
|             <n-number-animation | ||||
|               :from="0" | ||||
|               :to="toGigabytes(usage.total_usage_bytes)" | ||||
|               :precision="3" | ||||
|             /> | ||||
|             <template #suffix>GiB</template> | ||||
|           </n-statistic> | ||||
|         </n-card> | ||||
|       </n-gi> | ||||
|       <n-gi> | ||||
|         <n-card class="h-stats"> | ||||
|           <n-statistic label="All Files" tabular-nums> | ||||
|             <n-number-animation :from="0" :to="usage.total_file_count" /> | ||||
|           </n-statistic> | ||||
|         </n-card> | ||||
|       </n-gi> | ||||
|       <n-gi> | ||||
|         <n-card class="h-stats"> | ||||
|           <n-statistic label="Quota" tabular-nums> | ||||
|             <n-number-animation :from="0" :to="usage.total_quota" /> | ||||
|             <template #suffix>MiB</template> | ||||
|           </n-statistic> | ||||
|         </n-card> | ||||
|       </n-gi> | ||||
|       <n-gi> | ||||
|         <n-card class="h-stats"> | ||||
|           <div class="flex gap-2 justify-between items-end"> | ||||
|             <n-statistic label="Used Quota" tabular-nums> | ||||
|               <n-number-animation :from="0" :to="quotaUsagePercentage" :precision="2" /> | ||||
|               <template #suffix>%</template> | ||||
|             </n-statistic> | ||||
|             <n-progress | ||||
|               type="circle" | ||||
|               :percentage="quotaUsagePercentage" | ||||
|               :show-indicator="false" | ||||
|               :stroke-width="16" | ||||
|               style="width: 40px" | ||||
|             /> | ||||
|           </div> | ||||
|         </n-card> | ||||
|       </n-gi> | ||||
|       <n-gi span="2"> | ||||
|         <n-card class="aspect-video" title="Pool Usage"> | ||||
|           <pie | ||||
|             :data="poolChartData" | ||||
|             :options="{ | ||||
|               maintainAspectRatio: false, | ||||
|               responsive: true, | ||||
|               plugins: { legend: { position: isDesktop ? 'right' : 'bottom' } }, | ||||
|             }" | ||||
|           /> | ||||
|         </n-card> | ||||
|       </n-gi> | ||||
|       <n-gi span="2"> | ||||
|         <n-card class="aspect-video h-full" title="Verbose Quota"> | ||||
|           <pie | ||||
|             :data="quotaChartData" | ||||
|             :options="{ | ||||
|               maintainAspectRatio: false, | ||||
|               responsive: true, | ||||
|               plugins: { legend: { position: isDesktop ? 'right' : 'bottom' } }, | ||||
|             }" | ||||
|           /> | ||||
|         </n-card> | ||||
|       </n-gi> | ||||
|     </n-grid> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { NSpin, NCard, NStatistic, NGrid, NGi, NNumberAnimation, NAlert, NProgress } from 'naive-ui' | ||||
| import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement } from 'chart.js' | ||||
| import { Pie } from 'vue-chartjs' | ||||
| import { computed, onMounted, ref } from 'vue' | ||||
| import { breakpointsTailwind, useBreakpoints } from '@vueuse/core' | ||||
|  | ||||
| ChartJS.register(Title, Tooltip, Legend, ArcElement) | ||||
|  | ||||
| const breakpoints = useBreakpoints(breakpointsTailwind) | ||||
| const isDesktop = breakpoints.greaterOrEqual('md') | ||||
|  | ||||
| const poolChartData = computed(() => ({ | ||||
|   labels: usage.value.pool_usages.map((pool: any) => pool.pool_name), | ||||
|   datasets: [ | ||||
|     { | ||||
|       label: 'Pool Usage', | ||||
|       backgroundColor: '#7D80BAFF', | ||||
|       data: usage.value.pool_usages.map((pool: any) => pool.usage_bytes), | ||||
|     }, | ||||
|   ], | ||||
| })) | ||||
|  | ||||
| const usage = ref<any>() | ||||
| async function fetchUsage() { | ||||
|   try { | ||||
|     const response = await fetch('/api/billing/usage') | ||||
|     if (!response.ok) { | ||||
|       throw new Error('Network response was not ok') | ||||
|     } | ||||
|     usage.value = await response.json() | ||||
|   } catch (error) { | ||||
|     console.error('Failed to fetch usage data:', error) | ||||
|   } | ||||
| } | ||||
| onMounted(() => fetchUsage()) | ||||
|  | ||||
| const verboseQuota = ref< | ||||
|   { based_quota: number; extra_quota: number; total_quota: number } | undefined | ||||
| >() | ||||
| async function fetchVerboseQuota() { | ||||
|   try { | ||||
|     const response = await fetch('/api/billing/quota') | ||||
|     if (!response.ok) { | ||||
|       throw new Error('Network response was not ok') | ||||
|     } | ||||
|     verboseQuota.value = await response.json() | ||||
|   } catch (error) { | ||||
|     console.error('Failed to fetch verbose data:', error) | ||||
|   } | ||||
| } | ||||
| onMounted(() => fetchVerboseQuota()) | ||||
|  | ||||
| const quotaChartData = computed(() => ({ | ||||
|   labels: ['Base Quota', 'Extra Quota'], | ||||
|   datasets: [ | ||||
|     { | ||||
|       label: 'Verbose Quota', | ||||
|       backgroundColor: '#7D80BAFF', | ||||
|       data: [verboseQuota.value?.based_quota ?? 0, verboseQuota.value?.extra_quota ?? 0], | ||||
|     }, | ||||
|   ], | ||||
| })) | ||||
| const quotaUsagePercentage = computed( | ||||
|   () => (usage.value.used_quota / usage.value.total_quota) * 100, | ||||
| ) | ||||
|  | ||||
| function toGigabytes(bytes: number): number { | ||||
|   return bytes / (1024 * 1024 * 1024) | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .h-stats { | ||||
|   height: 105px; | ||||
| } | ||||
| </style> | ||||
| @@ -1,36 +1,262 @@ | ||||
| <template> | ||||
|   <section class="h-full relative flex items-center justify-center"> | ||||
|     <n-card class="max-w-lg" title="Download file"> | ||||
|       <div class="flex flex-col gap-3" v-if="!progress"> | ||||
|         <n-input placeholder="File ID" v-model:value="fileId" /> | ||||
|         <n-input placeholder="Password" v-model:value="filePass" type="password" /> | ||||
|         <n-button @click="downloadFile">Download</n-button> | ||||
|   <section class="min-h-full relative flex items-center justify-center"> | ||||
|     <n-spin v-if="!fileInfo && !error" /> | ||||
|     <n-result status="404" title="No file was found" :description="error" v-else-if="error" /> | ||||
|     <n-card class="max-w-4xl my-4 mx-8" v-else> | ||||
|       <n-grid cols="1 m:2" x-gap="16" y-gap="16" responsive="screen"> | ||||
|         <n-gi> | ||||
|           <div v-if="fileInfo.is_encrypted"> | ||||
|             <n-alert type="info" size="small" title="Encrypted file"> | ||||
|               The file has been encrypted. Preview not available. Please enter the password to | ||||
|               download it. | ||||
|             </n-alert> | ||||
|           </div> | ||||
|           <div v-else> | ||||
|         <n-progress :percentage="progress" /> | ||||
|             <n-image v-if="fileType === 'image'" :src="fileSource" class="w-full" /> | ||||
|             <video v-else-if="fileType === 'video'" :src="fileSource" controls class="w-full" /> | ||||
|             <audio v-else-if="fileType === 'audio'" :src="fileSource" controls class="w-full" /> | ||||
|             <n-result | ||||
|               status="418" | ||||
|               title="Preview Unavailable" | ||||
|               description="How can you preview this file?" | ||||
|               size="small" | ||||
|               class="py-6" | ||||
|               v-else | ||||
|             /> | ||||
|           </div> | ||||
|         </n-gi> | ||||
|  | ||||
|         <n-gi> | ||||
|           <div class="mb-3"> | ||||
|             <n-card title="File Infomation" size="small"> | ||||
|               <div class="flex gap-2"> | ||||
|                 <span class="flex-grow-1 flex items-center gap-2"> | ||||
|                   <n-icon> | ||||
|                     <info-round /> | ||||
|                   </n-icon> | ||||
|                   File Type | ||||
|                 </span> | ||||
|                 <span>{{ fileInfo.mime_type }} ({{ fileType }})</span> | ||||
|               </div> | ||||
|               <div class="flex gap-2"> | ||||
|                 <span class="flex-grow-1 flex items-center gap-2"> | ||||
|                   <n-icon> | ||||
|                     <data-usage-round /> | ||||
|                   </n-icon> | ||||
|                   File Size | ||||
|                 </span> | ||||
|                 <span>{{ formatBytes(fileInfo.size) }}</span> | ||||
|               </div> | ||||
|               <div class="flex gap-2"> | ||||
|                 <span class="flex-grow-1 flex items-center gap-2"> | ||||
|                   <n-icon> | ||||
|                     <file-upload-outlined /> | ||||
|                   </n-icon> | ||||
|                   Uploaded At | ||||
|                 </span> | ||||
|                 <span>{{ new Date(fileInfo.created_at).toLocaleString() }}</span> | ||||
|               </div> | ||||
|               <div class="flex gap-2"> | ||||
|                 <span class="flex-grow-1 flex items-center gap-2"> | ||||
|                   <n-icon> | ||||
|                     <details-round /> | ||||
|                   </n-icon> | ||||
|                   Techical Info | ||||
|                 </span> | ||||
|                 <n-button text size="small" @click="showTechDetails = !showTechDetails"> | ||||
|                   {{ showTechDetails ? 'Hide' : 'Show' }} | ||||
|                 </n-button> | ||||
|               </div> | ||||
|  | ||||
|               <n-collapse-transition :show="showTechDetails"> | ||||
|                 <div v-if="showTechDetails" class="mt-2 flex flex-col gap-1"> | ||||
|                   <p class="text-xs opacity-75">#{{ fileInfo.id }}</p> | ||||
|  | ||||
|                   <n-card size="small" content-style="padding: 0" embedded> | ||||
|                     <div class="overflow-x-auto px-4 py-2"> | ||||
|                       <n-code | ||||
|                         :code="JSON.stringify(fileInfo.file_meta, null, 4)" | ||||
|                         language="json" | ||||
|                         :hljs="hljs" | ||||
|                       /> | ||||
|                     </div> | ||||
|                   </n-card> | ||||
|                 </div> | ||||
|               </n-collapse-transition> | ||||
|             </n-card> | ||||
|           </div> | ||||
|  | ||||
|           <div class="flex flex-col gap-3"> | ||||
|             <n-input | ||||
|               v-if="fileInfo.is_encrypted" | ||||
|               placeholder="Password" | ||||
|               v-model:value="filePass" | ||||
|               type="password" | ||||
|             /> | ||||
|             <div class="flex gap-2"> | ||||
|               <n-button class="flex-grow-1" @click="downloadFile">Download</n-button> | ||||
|               <n-popover placement="bottom" trigger="hover"> | ||||
|                 <template #trigger> | ||||
|                   <n-button> | ||||
|                     <n-icon> | ||||
|                       <qr-code-round /> | ||||
|                     </n-icon> | ||||
|                   </n-button> | ||||
|                 </template> | ||||
|                 <n-qr-code | ||||
|                   type="svg" | ||||
|                   :value="currentUrl" | ||||
|                   :size="160" | ||||
|                   icon-src="/favicon.png" | ||||
|                   error-correction-level="H" | ||||
|                 /> | ||||
|               </n-popover> | ||||
|             </div> | ||||
|           </div> | ||||
|           <n-collapse-transition :show="!!progress"> | ||||
|             <n-progress | ||||
|               :processing="!!progress && progress < 100" | ||||
|               :percentage="progress" | ||||
|               indicator-placement="inside" | ||||
|               class="mt-4" | ||||
|             /> | ||||
|           </n-collapse-transition> | ||||
|         </n-gi> | ||||
|       </n-grid> | ||||
|     </n-card> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { NCard, NInput, NButton, NProgress, useMessage } from 'naive-ui' | ||||
| import { ref } from 'vue' | ||||
| import { | ||||
|   NCard, | ||||
|   NInput, | ||||
|   NButton, | ||||
|   NProgress, | ||||
|   NResult, | ||||
|   NSpin, | ||||
|   NImage, | ||||
|   NAlert, | ||||
|   NIcon, | ||||
|   NCollapseTransition, | ||||
|   NCode, | ||||
|   NGrid, | ||||
|   NGi, | ||||
|   NPopover, | ||||
|   NQrCode, | ||||
|   useMessage, | ||||
| } from 'naive-ui' | ||||
| import { | ||||
|   DataUsageRound, | ||||
|   InfoRound, | ||||
|   DetailsRound, | ||||
|   FileUploadOutlined, | ||||
|   QrCodeRound, | ||||
| } from '@vicons/material' | ||||
| import { useRoute } from 'vue-router' | ||||
| import { computed, onMounted, ref } from 'vue' | ||||
|  | ||||
| import { downloadAndDecryptFile } from './secure' | ||||
| import { formatBytes } from './format' | ||||
|  | ||||
| import hljs from 'highlight.js/lib/core' | ||||
| import json from 'highlight.js/lib/languages/json' | ||||
|  | ||||
| hljs.registerLanguage('json', json) | ||||
|  | ||||
| const route = useRoute() | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
|  | ||||
| const filePass = ref<string>('') | ||||
| const fileId = ref<string>('') | ||||
| const fileId = route.params.fileId | ||||
| const passcode = route.query.passcode as string | undefined | ||||
|  | ||||
| const progress = ref<number | undefined>(0) | ||||
|  | ||||
| const showTechDetails = ref<boolean>(false) | ||||
|  | ||||
| const messageDisplay = useMessage() | ||||
|  | ||||
| function downloadFile() { | ||||
|   downloadAndDecryptFile('/api/files/' + fileId.value, filePass.value, (p: number) => { | ||||
| const currentUrl = window.location.href | ||||
|  | ||||
| const fileInfo = ref<any>(null) | ||||
| async function fetchFileInfo() { | ||||
|   try { | ||||
|     let url = '/api/files/' + fileId + '/info' | ||||
|     if (passcode) { | ||||
|       url += `?passcode=${passcode}` | ||||
|     } | ||||
|     const resp = await fetch(url) | ||||
|     if (!resp.ok) { | ||||
|       throw new Error('Failed to fetch file info: ' + resp.statusText) | ||||
|     } | ||||
|     fileInfo.value = await resp.json() | ||||
|   } catch (err) { | ||||
|     error.value = (err as Error).message | ||||
|   } | ||||
| } | ||||
| onMounted(() => fetchFileInfo()) | ||||
|  | ||||
| const fileType = computed(() => { | ||||
|   if (!fileInfo.value) return 'unknown' | ||||
|   return fileInfo.value.mime_type?.split('/')[0] || 'unknown' | ||||
| }) | ||||
| const fileSource = computed(() => { | ||||
|   let url = `/api/files/${fileId}` | ||||
|   if (passcode) { | ||||
|     url += `?passcode=${passcode}` | ||||
|   } | ||||
|   return url | ||||
| }) | ||||
|  | ||||
| async function downloadFile() { | ||||
|   if (fileInfo.value.is_encrypted && !filePass.value) { | ||||
|     messageDisplay.error('Please enter the password to download the file.') | ||||
|     return | ||||
|   } | ||||
|   if (fileInfo.value.is_encrypted) { | ||||
|     downloadAndDecryptFile(fileSource.value, filePass.value, fileInfo.value.name, (p: number) => { | ||||
|       progress.value = p * 100 | ||||
|     }).catch((err) => { | ||||
|       messageDisplay.error('Download failed: ' + err.message, { closable: true, duration: 10000 }) | ||||
|       progress.value = undefined | ||||
|     }) | ||||
|   } else { | ||||
|     const res = await fetch(fileSource.value) | ||||
|     if (!res.ok) { | ||||
|       throw new Error(`Failed to download ${fileInfo.value.name}: ${res.statusText}`) | ||||
|     } | ||||
|  | ||||
|     const contentLength = res.headers.get('content-length') | ||||
|     if (!contentLength) { | ||||
|       throw new Error('Content-Length response header is missing.') | ||||
|     } | ||||
|  | ||||
|     const total = parseInt(contentLength, 10) | ||||
|     const reader = res.body!.getReader() | ||||
|     const chunks: Uint8Array[] = [] | ||||
|     let received = 0 | ||||
|  | ||||
|     while (true) { | ||||
|       const { done, value } = await reader.read() | ||||
|       if (done) break | ||||
|       if (value) { | ||||
|         chunks.push(value) | ||||
|         received += value.length | ||||
|         progress.value = (received / total) * 100 | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     const blob = new Blob(chunks) | ||||
|     const blobUrl = window.URL.createObjectURL(blob) | ||||
|     const a = document.createElement('a') | ||||
|     a.href = blobUrl | ||||
|     a.download = fileInfo.value.name || 'download' | ||||
|     document.body.appendChild(a) | ||||
|     a.click() | ||||
|     a.remove() | ||||
|     window.URL.revokeObjectURL(blobUrl) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|   | ||||
							
								
								
									
										8
									
								
								DysonNetwork.Drive/Client/src/views/format.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								DysonNetwork.Drive/Client/src/views/format.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| export function formatBytes(bytes: number, decimals = 2): string { | ||||
|   if (bytes === 0) return '0 Bytes' | ||||
|   const k = 1024 | ||||
|   const dm = decimals < 0 ? 0 : decimals | ||||
|   const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] | ||||
|   const i = Math.floor(Math.log(bytes) / Math.log(k)) | ||||
|   return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] | ||||
| } | ||||
| @@ -1,60 +1,70 @@ | ||||
| <template> | ||||
|   <section class="h-full relative flex items-center justify-center"> | ||||
|     <n-card class="max-w-lg" title="About" v-if="!userStore.user"> | ||||
|   <section class="h-full relative flex flex-col items-center justify-center"> | ||||
|     <n-card class="max-w-lg my-4 mx-8" title="About" v-if="!userStore.user"> | ||||
|       <p>Welcome to the <b>Solar Drive</b></p> | ||||
|       <p>We help you upload, collect, and share files with ease in mind.</p> | ||||
|       <p>To continue, login first.</p> | ||||
|  | ||||
|       <p class="mt-4 opacity-75 text-xs"> | ||||
|         <span v-if="version == null">Loading...</span> | ||||
|         <span v-else> | ||||
|           v{{ version.version }} @ | ||||
|           {{ version.commit.substring(0, 6) }} | ||||
|           {{ version.updatedAt }} | ||||
|         </span> | ||||
|       </p> | ||||
|     </n-card> | ||||
|     <n-card class="max-w-2xl" title="Upload to Solar Network" v-else> | ||||
|       <template #header-extra> | ||||
|         <div class="flex gap-2 items-center"> | ||||
|  | ||||
|     <n-card class="max-w-2xl" v-else content-style="padding: 0;"> | ||||
|       <n-tabs type="line" animated :tabs-padding="20" pane-style="padding: 20px"> | ||||
|         <template #suffix> | ||||
|           <div class="flex gap-2 items-center me-4"> | ||||
|             <p>Advance Mode</p> | ||||
|             <n-switch v-model:value="modeAdvanced" size="small" /> | ||||
|           </div> | ||||
|         </template> | ||||
|  | ||||
|       <div class="mb-3" v-if="modeAdvanced"> | ||||
|         <n-input | ||||
|           v-model:value="filePass" | ||||
|           placeholder="Enter password to protect the file" | ||||
|           clearable | ||||
|           size="large" | ||||
|           type="password" | ||||
|           class="mb-2" | ||||
|         <n-tab-pane name="direct" tab="Direct Upload" :disabled="isBundleMode"> | ||||
|           <div class="mb-3"> | ||||
|             <file-pool-select v-model="filePool" @update:pool="currentFilePool = $event" /> | ||||
|           </div> | ||||
|           <upload-area | ||||
|             :filePool="filePool" | ||||
|             :pools="pools as SnFilePool[]" | ||||
|             :modeAdvanced="modeAdvanced" | ||||
|           /> | ||||
|         </n-tab-pane> | ||||
|         <n-tab-pane name="bundle" tab="Bundle Upload"> | ||||
|           <div class="mb-3"> | ||||
|             <bundle-select v-model:bundle="selectedBundleId" :disabled="isBundleMode" /> | ||||
|           </div> | ||||
|  | ||||
|       <n-upload | ||||
|         multiple | ||||
|         directory-dnd | ||||
|         with-credentials | ||||
|         show-preview-button | ||||
|         list-type="image" | ||||
|         :custom-request="customRequest" | ||||
|         :create-thumbnail-url="createThumbnailUrl" | ||||
|           <n-modal v-model:show="showCreateBundleModal" preset="dialog" title="Create New Bundle"> | ||||
|             <bundle-form ref="bundleFormRef" :value="newBundle" /> | ||||
|             <template #action> | ||||
|               <n-button @click="showCreateBundleModal = false">Cancel</n-button> | ||||
|               <n-button type="primary" @click="createBundle">Create</n-button> | ||||
|             </template> | ||||
|           </n-modal> | ||||
|  | ||||
|           <div class="flex justify-between"> | ||||
|             <n-button @click="showCreateBundleModal = true" class="mb-3" :disabled="isBundleMode"> | ||||
|               Create New Bundle | ||||
|             </n-button> | ||||
|             <n-button | ||||
|               type="primary" | ||||
|               :disabled="!selectedBundleId && !newBundleId && !isBundleMode" | ||||
|               @click="isBundleMode ? cancelBundleUpload() : proceedToBundleUpload()" | ||||
|             > | ||||
|         <n-upload-dragger> | ||||
|           <div style="margin-bottom: 12px"> | ||||
|             <n-icon size="48" :depth="3"> | ||||
|               <upload-outlined /> | ||||
|             </n-icon> | ||||
|               {{ isBundleMode ? 'Cancel' : 'Proceed to Upload' }} | ||||
|             </n-button> | ||||
|           </div> | ||||
|           <n-text style="font-size: 16px"> Click or drag a file to this area to upload </n-text> | ||||
|           <n-p depth="3" style="margin: 8px 0 0 0"> | ||||
|             Strictly prohibit from uploading sensitive information. For example, your bank card PIN | ||||
|             or your credit card expiry date. | ||||
|           </n-p> | ||||
|         </n-upload-dragger> | ||||
|       </n-upload> | ||||
|  | ||||
|           <div v-if="bundleUploadMode" class="mt-3"> | ||||
|             <div class="mb-3"> | ||||
|               <file-pool-select v-model="filePool" @update:pool="currentFilePool = $event" /> | ||||
|             </div> | ||||
|             <upload-area | ||||
|               :filePool="filePool" | ||||
|               :pools="pools as SnFilePool[]" | ||||
|               :modeAdvanced="modeAdvanced" | ||||
|               :bundleId="currentBundleId!" | ||||
|             /> | ||||
|           </div> | ||||
|         </n-tab-pane> | ||||
|       </n-tabs> | ||||
|     </n-card> | ||||
|  | ||||
|     <p class="mt-4 opacity-75 text-xs"> | ||||
|       <span v-if="version == null">Loading...</span> | ||||
| @@ -64,95 +74,91 @@ | ||||
|         {{ version.updatedAt }} | ||||
|       </span> | ||||
|     </p> | ||||
|     </n-card> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { | ||||
|   NCard, | ||||
|   NUpload, | ||||
|   NUploadDragger, | ||||
|   NIcon, | ||||
|   NText, | ||||
|   NP, | ||||
|   NInput, | ||||
|   NSwitch, | ||||
|   type UploadCustomRequestOptions, | ||||
|   type UploadSettledFileInfo, | ||||
| } from 'naive-ui' | ||||
| import { onMounted, ref } from 'vue' | ||||
| import { UploadOutlined } from '@vicons/material' | ||||
| import { NCard, NSwitch, NTabs, NTabPane, NButton, NModal } from 'naive-ui' | ||||
| import { computed, onMounted, ref } from 'vue' | ||||
| import { useUserStore } from '@/stores/user' | ||||
|  | ||||
| import * as tus from 'tus-js-client' | ||||
| import type { SnFilePool } from '@/types/pool' | ||||
| import FilePoolSelect from '@/components/FilePoolSelect.vue' | ||||
| import UploadArea from '@/components/UploadArea.vue' | ||||
| import BundleSelect from '@/components/BundleSelect.vue' | ||||
| import BundleForm from '@/components/form/BundleForm.vue' | ||||
|  | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| const version = ref<any>(null) | ||||
|  | ||||
| async function fetchVersion() { | ||||
|   const resp = await fetch('/api/version') | ||||
|   version.value = await resp.json() | ||||
| } | ||||
|  | ||||
| onMounted(() => fetchVersion()) | ||||
|  | ||||
| type SnFilePoolOption = SnFilePool & any | ||||
|  | ||||
| const pools = ref<SnFilePoolOption[] | undefined>() | ||||
| async function fetchPools() { | ||||
|   const resp = await fetch('/api/pools') | ||||
|   pools.value = await resp.json() | ||||
| } | ||||
| onMounted(() => fetchPools()) | ||||
|  | ||||
| const modeAdvanced = ref(false) | ||||
|  | ||||
| const filePass = ref<string>('') | ||||
| const filePool = ref<string | null>(null) | ||||
|  | ||||
| function customRequest({ | ||||
|   file, | ||||
|   data, | ||||
|   headers, | ||||
|   withCredentials, | ||||
|   action, | ||||
|   onFinish, | ||||
|   onError, | ||||
|   onProgress, | ||||
| }: UploadCustomRequestOptions) { | ||||
|   const upload = new tus.Upload(file.file, { | ||||
|     endpoint: '/api/tus', | ||||
|     retryDelays: [0, 3000, 5000, 10000, 20000], | ||||
|     metadata: { | ||||
|       filename: file.name, | ||||
|       filetype: file.type ?? 'application/octet-stream', | ||||
|     }, | ||||
| const currentFilePool = computed(() => { | ||||
|   if (!filePool.value) return null | ||||
|   return pools.value?.find((pool) => pool.id === filePool.value) ?? null | ||||
| }) | ||||
|  | ||||
| const bundles = ref<any>([]) | ||||
| const selectedBundleId = ref<string | null>(null) | ||||
| const showCreateBundleModal = ref(false) | ||||
| const newBundle = ref<any>({}) | ||||
| const bundleFormRef = ref<any>(null) | ||||
| const bundleUploadMode = ref(false) | ||||
| const currentBundleId = ref<string | null>(null) | ||||
| const newBundleId = ref<string | null>(null) | ||||
| const isBundleMode = ref(false) | ||||
|  | ||||
| async function createBundle() { | ||||
|   try { | ||||
|     await bundleFormRef.value?.formRef?.validate() | ||||
|     const resp = await fetch('/api/bundles', { | ||||
|       method: 'POST', | ||||
|       headers: { | ||||
|       'X-FilePass': filePass.value, | ||||
|       ...headers, | ||||
|     }, | ||||
|     onError: function (error) { | ||||
|       console.error('[DRIVE] Upload failed:', error) | ||||
|       onError() | ||||
|     }, | ||||
|     onProgress: function (bytesUploaded, bytesTotal) { | ||||
|       onProgress({ percent: (bytesUploaded / bytesTotal) * 100 }) | ||||
|     }, | ||||
|     onSuccess: function (payload) { | ||||
|       const rawInfo = payload.lastResponse.getHeader('x-fileinfo') | ||||
|       const jsonInfo = JSON.parse(rawInfo as string) | ||||
|       console.log('[DRIVE] Upload successful: ', jsonInfo) | ||||
|       file.url = `/api/files/${jsonInfo.id}` | ||||
|       file.type = jsonInfo.mime_type | ||||
|       onFinish() | ||||
|     }, | ||||
|     onBeforeRequest: function (req) { | ||||
|       const xhr = req.getUnderlyingObject() | ||||
|       xhr.withCredentials = withCredentials | ||||
|         'Content-Type': 'application/json', | ||||
|       }, | ||||
|       body: JSON.stringify(newBundle.value), | ||||
|     }) | ||||
|   upload.findPreviousUploads().then(function (previousUploads) { | ||||
|     if (previousUploads.length) { | ||||
|       upload.resumeFromPreviousUpload(previousUploads[0]) | ||||
|     if (!resp.ok) { | ||||
|       throw new Error('Failed to create bundle') | ||||
|     } | ||||
|     const createdBundle = await resp.json() | ||||
|     bundles.value.push(createdBundle) | ||||
|     selectedBundleId.value = createdBundle.id | ||||
|     newBundleId.value = createdBundle.id | ||||
|     showCreateBundleModal.value = false | ||||
|     newBundle.value = {} | ||||
|   } catch (error) { | ||||
|     console.error('Failed to create bundle:', error) | ||||
|   } | ||||
|     upload.start() | ||||
|   }) | ||||
| } | ||||
|  | ||||
| function createThumbnailUrl(_file: File | null, fileInfo: UploadSettledFileInfo): string | undefined { | ||||
|   if (!fileInfo) return undefined | ||||
|   return fileInfo.url ?? undefined | ||||
| function proceedToBundleUpload() { | ||||
|   currentBundleId.value = selectedBundleId.value || newBundleId.value | ||||
|   bundleUploadMode.value = true | ||||
|   isBundleMode.value = true | ||||
| } | ||||
|  | ||||
| function cancelBundleUpload() { | ||||
|   bundleUploadMode.value = false | ||||
|   isBundleMode.value = false | ||||
|   currentBundleId.value = null | ||||
|   selectedBundleId.value = null | ||||
|   newBundleId.value = null | ||||
| } | ||||
| </script> | ||||
|   | ||||
							
								
								
									
										16
									
								
								DysonNetwork.Drive/Client/src/views/not-found.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								DysonNetwork.Drive/Client/src/views/not-found.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| <template> | ||||
|   <section class="h-full flex items-center justify-center"> | ||||
|     <n-result status="404" title="404" description="Page not found"> | ||||
|       <template #footer> | ||||
|         <n-button @click="router.push('/')">Go to Home</n-button> | ||||
|       </template> | ||||
|     </n-result> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { NResult, NButton } from 'naive-ui' | ||||
| import { useRouter } from 'vue-router'; | ||||
|  | ||||
| const router = useRouter() | ||||
| </script> | ||||
| @@ -1,92 +1,94 @@ | ||||
| export async function downloadAndDecryptFile( | ||||
|   url: string, | ||||
|   password: string, | ||||
|   onProgress?: (progress: number) => void | ||||
|   fileName: string, | ||||
|   onProgress?: (progress: number) => void, | ||||
| ): Promise<void> { | ||||
|   const response = await fetch(url); | ||||
|   if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`); | ||||
|   const response = await fetch(url) | ||||
|   if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`) | ||||
|  | ||||
|   const contentLength = +(response.headers.get('Content-Length') || 0); | ||||
|   const reader = response.body!.getReader(); | ||||
|   const chunks: Uint8Array[] = []; | ||||
|   let received = 0; | ||||
|   const contentLength = +(response.headers.get('Content-Length') || 0) | ||||
|   const reader = response.body!.getReader() | ||||
|   const chunks: Uint8Array[] = [] | ||||
|   let received = 0 | ||||
|  | ||||
|   while (true) { | ||||
|     const { done, value } = await reader.read(); | ||||
|     if (done) break; | ||||
|     const { done, value } = await reader.read() | ||||
|     if (done) break | ||||
|     if (value) { | ||||
|       chunks.push(value); | ||||
|       received += value.length; | ||||
|       chunks.push(value) | ||||
|       received += value.length | ||||
|       if (contentLength && onProgress) { | ||||
|         onProgress(received / contentLength); | ||||
|         onProgress(received / contentLength) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const fullBuffer = new Uint8Array(received); | ||||
|   let offset = 0; | ||||
|   const fullBuffer = new Uint8Array(received) | ||||
|   let offset = 0 | ||||
|   for (const chunk of chunks) { | ||||
|     fullBuffer.set(chunk, offset); | ||||
|     offset += chunk.length; | ||||
|     fullBuffer.set(chunk, offset) | ||||
|     offset += chunk.length | ||||
|   } | ||||
|  | ||||
|   const decryptedBytes = await decryptFile(fullBuffer, password); | ||||
|   const decryptedBytes = await decryptFile(fullBuffer, password) | ||||
|  | ||||
|   // Create a blob and trigger a download | ||||
|   const blob = new Blob([decryptedBytes]); | ||||
|   const downloadUrl = URL.createObjectURL(blob); | ||||
|   const a = document.createElement('a'); | ||||
|   a.href = downloadUrl; | ||||
|   a.download = 'decrypted_file'; // You may allow customization | ||||
|   document.body.appendChild(a); | ||||
|   a.click(); | ||||
|   a.remove(); | ||||
|   URL.revokeObjectURL(downloadUrl); | ||||
|   const blob = new Blob([decryptedBytes]) | ||||
|   const downloadUrl = URL.createObjectURL(blob) | ||||
|   const a = document.createElement('a') | ||||
|   a.href = downloadUrl | ||||
|   a.download = fileName | ||||
|   document.body.appendChild(a) | ||||
|   a.click() | ||||
|   a.remove() | ||||
|   URL.revokeObjectURL(downloadUrl) | ||||
| } | ||||
|  | ||||
| export async function decryptFile( | ||||
|   fileBuffer: Uint8Array, | ||||
|   password: string | ||||
| ): Promise<Uint8Array> { | ||||
|   const salt = fileBuffer.slice(0, 16); | ||||
|   const nonce = fileBuffer.slice(16, 28); | ||||
|   const tag = fileBuffer.slice(28, 44); | ||||
|   const ciphertext = fileBuffer.slice(44); | ||||
| export async function decryptFile(fileBuffer: Uint8Array, password: string): Promise<Uint8Array> { | ||||
|   const salt = fileBuffer.slice(0, 16) | ||||
|   const nonce = fileBuffer.slice(16, 28) | ||||
|   const tag = fileBuffer.slice(28, 44) | ||||
|   const ciphertext = fileBuffer.slice(44) | ||||
|  | ||||
|   const enc = new TextEncoder(); | ||||
|   const enc = new TextEncoder() | ||||
|   const keyMaterial = await crypto.subtle.importKey( | ||||
|     'raw', enc.encode(password), { name: 'PBKDF2' }, false, ['deriveKey'] | ||||
|   ); | ||||
|     'raw', | ||||
|     enc.encode(password), | ||||
|     { name: 'PBKDF2' }, | ||||
|     false, | ||||
|     ['deriveKey'], | ||||
|   ) | ||||
|   const key = await crypto.subtle.deriveKey( | ||||
|     { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' }, | ||||
|     keyMaterial, | ||||
|     { name: 'AES-GCM', length: 256 }, | ||||
|     false, | ||||
|     ['decrypt'] | ||||
|   ); | ||||
|     ['decrypt'], | ||||
|   ) | ||||
|  | ||||
|   const fullCiphertext = new Uint8Array(ciphertext.length + tag.length); | ||||
|   fullCiphertext.set(ciphertext); | ||||
|   fullCiphertext.set(tag, ciphertext.length); | ||||
|   const fullCiphertext = new Uint8Array(ciphertext.length + tag.length) | ||||
|   fullCiphertext.set(ciphertext) | ||||
|   fullCiphertext.set(tag, ciphertext.length) | ||||
|  | ||||
|   let decrypted: ArrayBuffer; | ||||
|   let decrypted: ArrayBuffer | ||||
|   try { | ||||
|     decrypted = await crypto.subtle.decrypt( | ||||
|       { name: 'AES-GCM', iv: nonce, tagLength: 128 }, | ||||
|       key, | ||||
|       fullCiphertext | ||||
|     ); | ||||
|       fullCiphertext, | ||||
|     ) | ||||
|   } catch { | ||||
|     throw new Error("Incorrect password or corrupted file."); | ||||
|     throw new Error('Incorrect password or corrupted file.') | ||||
|   } | ||||
|  | ||||
|   const magic = new TextEncoder().encode("DYSON1"); | ||||
|   const decryptedBytes = new Uint8Array(decrypted); | ||||
|   const magic = new TextEncoder().encode('DYSON1') | ||||
|   const decryptedBytes = new Uint8Array(decrypted) | ||||
|   for (let i = 0; i < magic.length; i++) { | ||||
|     if (decryptedBytes[i] !== magic[i]) { | ||||
|       throw new Error("Incorrect password or corrupted file."); | ||||
|       throw new Error('Incorrect password or corrupted file.') | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return decryptedBytes.slice(magic.length); | ||||
|   return decryptedBytes.slice(magic.length) | ||||
| } | ||||
|   | ||||
| @@ -17,15 +17,6 @@ export default defineConfig({ | ||||
|       '@': fileURLToPath(new URL('./src', import.meta.url)), | ||||
|     }, | ||||
|   }, | ||||
|   build: { | ||||
|     rollupOptions: { | ||||
|       output: { | ||||
|         entryFileNames: `assets/[name].js`, | ||||
|         chunkFileNames: `assets/[name].js`, | ||||
|         assetFileNames: `assets/[name].[ext]`, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   server: { | ||||
|     proxy: { | ||||
|       '/api': { | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> | ||||
|         <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" /> | ||||
|         <PackageReference Include="FFMpegCore" Version="5.2.0" /> | ||||
|         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" /> | ||||
| @@ -34,7 +35,6 @@ | ||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" /> | ||||
|         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" /> | ||||
| @@ -66,6 +66,7 @@ | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" /> | ||||
|       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|   | ||||
| @@ -1,194 +0,0 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| 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("20250725051034_UpdateCloudFileThumbnail")] | ||||
|     partial class UpdateCloudFileThumbnail | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "9.0.7") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             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<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<Dictionary<string, object>>("FileMeta") | ||||
|                         .IsRequired() | ||||
|                         .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>("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<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<string>("UploadedTo") | ||||
|                         .HasMaxLength(128) | ||||
|                         .HasColumnType("character varying(128)") | ||||
|                         .HasColumnName("uploaded_to"); | ||||
|  | ||||
|                     b.Property<Dictionary<string, object>>("UserMeta") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("user_meta"); | ||||
|  | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_files"); | ||||
|  | ||||
|                     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.CloudFileReference", b => | ||||
|                 { | ||||
|                     b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("FileId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_file_references_files_file_id"); | ||||
|  | ||||
|                     b.Navigation("File"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,73 +0,0 @@ | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class EnrichCloudPoolConfigure : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AddColumn<bool>( | ||||
|                 name: "allow_anonymous", | ||||
|                 table: "pools", | ||||
|                 type: "boolean", | ||||
|                 nullable: false, | ||||
|                 defaultValue: false); | ||||
|  | ||||
|             migrationBuilder.AddColumn<bool>( | ||||
|                 name: "allow_encryption", | ||||
|                 table: "pools", | ||||
|                 type: "boolean", | ||||
|                 nullable: false, | ||||
|                 defaultValue: false); | ||||
|  | ||||
|             migrationBuilder.AddColumn<bool>( | ||||
|                 name: "no_metadata", | ||||
|                 table: "pools", | ||||
|                 type: "boolean", | ||||
|                 nullable: false, | ||||
|                 defaultValue: false); | ||||
|  | ||||
|             migrationBuilder.AddColumn<bool>( | ||||
|                 name: "no_optimization", | ||||
|                 table: "pools", | ||||
|                 type: "boolean", | ||||
|                 nullable: false, | ||||
|                 defaultValue: false); | ||||
|  | ||||
|             migrationBuilder.AddColumn<int>( | ||||
|                 name: "require_privilege", | ||||
|                 table: "pools", | ||||
|                 type: "integer", | ||||
|                 nullable: false, | ||||
|                 defaultValue: 0); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "allow_anonymous", | ||||
|                 table: "pools"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "allow_encryption", | ||||
|                 table: "pools"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "no_metadata", | ||||
|                 table: "pools"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "no_optimization", | ||||
|                 table: "pools"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "require_privilege", | ||||
|                 table: "pools"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,36 +0,0 @@ | ||||
| using System.Collections.Generic; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class NullableFileMeta : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AlterColumn<Dictionary<string, object>>( | ||||
|                 name: "file_meta", | ||||
|                 table: "files", | ||||
|                 type: "jsonb", | ||||
|                 nullable: true, | ||||
|                 oldClrType: typeof(Dictionary<string, object>), | ||||
|                 oldType: "jsonb"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AlterColumn<Dictionary<string, object>>( | ||||
|                 name: "file_meta", | ||||
|                 table: "files", | ||||
|                 type: "jsonb", | ||||
|                 nullable: false, | ||||
|                 oldClrType: typeof(Dictionary<string, object>), | ||||
|                 oldType: "jsonb", | ||||
|                 oldNullable: true); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -16,8 +16,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250725170254_AddCloudFileEncrypt")] | ||||
|     partial class AddCloudFileEncrypt | ||||
|     [Migration("20250726103203_AddCloudFilePool")] | ||||
|     partial class AddCloudFilePool | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
| @@ -55,7 +55,6 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnName("description"); | ||||
| 
 | ||||
|                     b.Property<Dictionary<string, object>>("FileMeta") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("file_meta"); | ||||
| 
 | ||||
| @@ -196,6 +195,10 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<Guid?>("AccountId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
| 
 | ||||
|                     b.Property<BillingConfig>("BillingConfig") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
| @@ -215,6 +218,11 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
| 
 | ||||
|                     b.Property<PolicyConfig>("PolicyConfig") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("policy_config"); | ||||
| 
 | ||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
| @@ -13,6 +14,28 @@ namespace DysonNetwork.Drive.Migrations | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AlterColumn<Dictionary<string, object>>( | ||||
|                 name: "file_meta", | ||||
|                 table: "files", | ||||
|                 type: "jsonb", | ||||
|                 nullable: true, | ||||
|                 oldClrType: typeof(Dictionary<string, object>), | ||||
|                 oldType: "jsonb"); | ||||
| 
 | ||||
|             migrationBuilder.AddColumn<bool>( | ||||
|                 name: "has_thumbnail", | ||||
|                 table: "files", | ||||
|                 type: "boolean", | ||||
|                 nullable: false, | ||||
|                 defaultValue: false); | ||||
| 
 | ||||
|             migrationBuilder.AddColumn<bool>( | ||||
|                 name: "is_encrypted", | ||||
|                 table: "files", | ||||
|                 type: "boolean", | ||||
|                 nullable: false, | ||||
|                 defaultValue: false); | ||||
| 
 | ||||
|             migrationBuilder.AddColumn<Guid>( | ||||
|                 name: "pool_id", | ||||
|                 table: "files", | ||||
| @@ -27,6 +50,8 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||
|                     storage_config = table.Column<RemoteStorageConfig>(type: "jsonb", nullable: false), | ||||
|                     billing_config = table.Column<BillingConfig>(type: "jsonb", nullable: false), | ||||
|                     policy_config = table.Column<PolicyConfig>(type: "jsonb", nullable: false), | ||||
|                     account_id = table.Column<Guid>(type: "uuid", nullable: true), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
| @@ -63,9 +88,26 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                 name: "ix_files_pool_id", | ||||
|                 table: "files"); | ||||
| 
 | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "has_thumbnail", | ||||
|                 table: "files"); | ||||
| 
 | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "is_encrypted", | ||||
|                 table: "files"); | ||||
| 
 | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "pool_id", | ||||
|                 table: "files"); | ||||
| 
 | ||||
|             migrationBuilder.AlterColumn<Dictionary<string, object>>( | ||||
|                 name: "file_meta", | ||||
|                 table: "files", | ||||
|                 type: "jsonb", | ||||
|                 nullable: false, | ||||
|                 oldClrType: typeof(Dictionary<string, object>), | ||||
|                 oldType: "jsonb", | ||||
|                 oldNullable: true); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -16,8 +16,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250725163615_AddCloudFilePool")] | ||||
|     partial class AddCloudFilePool | ||||
|     [Migration("20250726120323_AddFilePoolDescription")] | ||||
|     partial class AddFilePoolDescription | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
| @@ -55,7 +55,6 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnName("description"); | ||||
| 
 | ||||
|                     b.Property<Dictionary<string, object>>("FileMeta") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("file_meta"); | ||||
| 
 | ||||
| @@ -72,6 +71,10 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .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"); | ||||
| @@ -192,6 +195,10 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<Guid?>("AccountId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
| 
 | ||||
|                     b.Property<BillingConfig>("BillingConfig") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
| @@ -205,12 +212,23 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
| 
 | ||||
|                     b.Property<string>("Description") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(8192) | ||||
|                         .HasColumnType("character varying(8192)") | ||||
|                         .HasColumnName("description"); | ||||
| 
 | ||||
|                     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") | ||||
| @@ -0,0 +1,30 @@ | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddFilePoolDescription : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AddColumn<string>( | ||||
|                 name: "description", | ||||
|                 table: "pools", | ||||
|                 type: "character varying(8192)", | ||||
|                 maxLength: 8192, | ||||
|                 nullable: false, | ||||
|                 defaultValue: ""); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "description", | ||||
|                 table: "pools"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -16,8 +16,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250725184107_NullableFileMeta")] | ||||
|     partial class NullableFileMeta | ||||
|     [Migration("20250726172039_AddCloudFileExpiration")] | ||||
|     partial class AddCloudFileExpiration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
| @@ -54,6 +54,10 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .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"); | ||||
| @@ -195,13 +199,9 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<bool>("AllowAnonymous") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("allow_anonymous"); | ||||
| 
 | ||||
|                     b.Property<bool>("AllowEncryption") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("allow_encryption"); | ||||
|                     b.Property<Guid?>("AccountId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
| 
 | ||||
|                     b.Property<BillingConfig>("BillingConfig") | ||||
|                         .IsRequired() | ||||
| @@ -216,23 +216,22 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
| 
 | ||||
|                     b.Property<string>("Description") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(8192) | ||||
|                         .HasColumnType("character varying(8192)") | ||||
|                         .HasColumnName("description"); | ||||
| 
 | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
| 
 | ||||
|                     b.Property<bool>("NoMetadata") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("no_metadata"); | ||||
| 
 | ||||
|                     b.Property<bool>("NoOptimization") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("no_optimization"); | ||||
| 
 | ||||
|                     b.Property<int>("RequirePrivilege") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("require_privilege"); | ||||
|                     b.Property<PolicyConfig>("PolicyConfig") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("policy_config"); | ||||
| 
 | ||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") | ||||
|                         .IsRequired() | ||||
| @@ -0,0 +1,29 @@ | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddCloudFileExpiration : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AddColumn<Instant>( | ||||
|                 name: "expired_at", | ||||
|                 table: "files", | ||||
|                 type: "timestamp with time zone", | ||||
|                 nullable: true); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "expired_at", | ||||
|                 table: "files"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -16,8 +16,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     [DbContext(typeof(AppDatabase))] | ||||
|     [Migration("20250725183846_EnrichCloudPoolConfigure")] | ||||
|     partial class EnrichCloudPoolConfigure | ||||
|     [Migration("20250727092028_AddQuotaRecord")] | ||||
|     partial class AddQuotaRecord | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
| @@ -30,6 +30,53 @@ namespace DysonNetwork.Drive.Migrations | ||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||
|             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") | ||||
| @@ -54,8 +101,11 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
| 
 | ||||
|                     b.Property<Instant?>("ExpiredAt") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expired_at"); | ||||
| 
 | ||||
|                     b.Property<Dictionary<string, object>>("FileMeta") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("file_meta"); | ||||
| 
 | ||||
| @@ -196,13 +246,9 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<bool>("AllowAnonymous") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("allow_anonymous"); | ||||
| 
 | ||||
|                     b.Property<bool>("AllowEncryption") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("allow_encryption"); | ||||
|                     b.Property<Guid?>("AccountId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
| 
 | ||||
|                     b.Property<BillingConfig>("BillingConfig") | ||||
|                         .IsRequired() | ||||
| @@ -217,23 +263,22 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("deleted_at"); | ||||
| 
 | ||||
|                     b.Property<string>("Description") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(8192) | ||||
|                         .HasColumnType("character varying(8192)") | ||||
|                         .HasColumnName("description"); | ||||
| 
 | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasMaxLength(1024) | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
| 
 | ||||
|                     b.Property<bool>("NoMetadata") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("no_metadata"); | ||||
| 
 | ||||
|                     b.Property<bool>("NoOptimization") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("no_optimization"); | ||||
| 
 | ||||
|                     b.Property<int>("RequirePrivilege") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("require_privilege"); | ||||
|                     b.Property<PolicyConfig>("PolicyConfig") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("policy_config"); | ||||
| 
 | ||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") | ||||
|                         .IsRequired() | ||||
| @@ -0,0 +1,42 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddQuotaRecord : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "quota_records", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     account_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     name = table.Column<string>(type: "text", nullable: false), | ||||
|                     description = table.Column<string>(type: "text", nullable: false), | ||||
|                     quota = table.Column<long>(type: "bigint", nullable: false), | ||||
|                     expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_quota_records", x => x.id); | ||||
|                 }); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "quota_records"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										400
									
								
								DysonNetwork.Drive/Migrations/20250727130951_AddFileBundle.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										400
									
								
								DysonNetwork.Drive/Migrations/20250727130951_AddFileBundle.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,400 @@ | ||||
| // <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("20250727130951_AddFileBundle")] | ||||
|     partial class AddFileBundle | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "9.0.7") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||
|             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<string>("UploadedTo") | ||||
|                         .HasMaxLength(128) | ||||
|                         .HasColumnType("character varying(128)") | ||||
|                         .HasColumnName("uploaded_to"); | ||||
|  | ||||
|                     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<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() | ||||
|                         .HasForeignKey("FileId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_file_references_files_file_id"); | ||||
|  | ||||
|                     b.Navigation("File"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => | ||||
|                 { | ||||
|                     b.Navigation("Files"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,79 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddFileBundle : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AddColumn<Guid>( | ||||
|                 name: "bundle_id", | ||||
|                 table: "files", | ||||
|                 type: "uuid", | ||||
|                 nullable: true); | ||||
|  | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "bundles", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||
|                     name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||
|                     description = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: true), | ||||
|                     passcode = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), | ||||
|                     expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||
|                     account_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_bundles", x => x.id); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_files_bundle_id", | ||||
|                 table: "files", | ||||
|                 column: "bundle_id"); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_bundles_slug", | ||||
|                 table: "bundles", | ||||
|                 column: "slug", | ||||
|                 unique: true); | ||||
|  | ||||
|             migrationBuilder.AddForeignKey( | ||||
|                 name: "fk_files_bundles_bundle_id", | ||||
|                 table: "files", | ||||
|                 column: "bundle_id", | ||||
|                 principalTable: "bundles", | ||||
|                 principalColumn: "id"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropForeignKey( | ||||
|                 name: "fk_files_bundles_bundle_id", | ||||
|                 table: "files"); | ||||
|  | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "bundles"); | ||||
|  | ||||
|             migrationBuilder.DropIndex( | ||||
|                 name: "ix_files_bundle_id", | ||||
|                 table: "files"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "bundle_id", | ||||
|                 table: "files"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										404
									
								
								DysonNetwork.Drive/Migrations/20250808170904_AddHiddenPool.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										404
									
								
								DysonNetwork.Drive/Migrations/20250808170904_AddHiddenPool.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,404 @@ | ||||
| // <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("20250808170904_AddHiddenPool")] | ||||
|     partial class AddHiddenPool | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "9.0.7") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||
|             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<string>("UploadedTo") | ||||
|                         .HasMaxLength(128) | ||||
|                         .HasColumnType("character varying(128)") | ||||
|                         .HasColumnName("uploaded_to"); | ||||
|  | ||||
|                     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() | ||||
|                         .HasForeignKey("FileId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_file_references_files_file_id"); | ||||
|  | ||||
|                     b.Navigation("File"); | ||||
|                 }); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b => | ||||
|                 { | ||||
|                     b.Navigation("Files"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -5,14 +5,14 @@ | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddCloudFileEncrypt : Migration | ||||
|     public partial class AddHiddenPool : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AddColumn<bool>( | ||||
|                 name: "is_encrypted", | ||||
|                 table: "files", | ||||
|                 name: "is_hidden", | ||||
|                 table: "pools", | ||||
|                 type: "boolean", | ||||
|                 nullable: false, | ||||
|                 defaultValue: false); | ||||
| @@ -22,8 +22,8 @@ namespace DysonNetwork.Drive.Migrations | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "is_encrypted", | ||||
|                 table: "files"); | ||||
|                 name: "is_hidden", | ||||
|                 table: "pools"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										404
									
								
								DysonNetwork.Drive/Migrations/20250819164302_RemoveUploadedTo.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										404
									
								
								DysonNetwork.Drive/Migrations/20250819164302_RemoveUploadedTo.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,404 @@ | ||||
| // <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("20250819164302_RemoveUploadedTo")] | ||||
|     partial class RemoveUploadedTo | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "9.0.7") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||
|             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,29 @@ | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Drive.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class RemoveUploadedTo : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "uploaded_to", | ||||
|                 table: "files"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AddColumn<string>( | ||||
|                 name: "uploaded_to", | ||||
|                 table: "files", | ||||
|                 type: "character varying(128)", | ||||
|                 maxLength: 128, | ||||
|                 nullable: true); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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,14 +24,13 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                 .HasAnnotation("ProductVersion", "9.0.7") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b => | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => | ||||
|                 { | ||||
|                     b.Property<string>("Id") | ||||
|                         .HasMaxLength(32) | ||||
|                         .HasColumnType("character varying(32)") | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .ValueGeneratedOnAdd() | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<Guid>("AccountId") | ||||
| @@ -46,11 +45,66 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .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"); | ||||
| @@ -117,11 +171,6 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("uploaded_at"); | ||||
|  | ||||
|                     b.Property<string>("UploadedTo") | ||||
|                         .HasMaxLength(128) | ||||
|                         .HasColumnType("character varying(128)") | ||||
|                         .HasColumnName("uploaded_to"); | ||||
|  | ||||
|                     b.Property<Dictionary<string, object>>("UserMeta") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("user_meta"); | ||||
| @@ -129,6 +178,9 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_files"); | ||||
|  | ||||
|                     b.HasIndex("BundleId") | ||||
|                         .HasDatabaseName("ix_files_bundle_id"); | ||||
|  | ||||
|                     b.HasIndex("PoolId") | ||||
|                         .HasDatabaseName("ix_files_pool_id"); | ||||
|  | ||||
| @@ -185,6 +237,65 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                     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") | ||||
| @@ -192,13 +303,9 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<bool>("AllowAnonymous") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("allow_anonymous"); | ||||
|  | ||||
|                     b.Property<bool>("AllowEncryption") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("allow_encryption"); | ||||
|                     b.Property<Guid?>("AccountId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
|  | ||||
|                     b.Property<BillingConfig>("BillingConfig") | ||||
|                         .IsRequired() | ||||
| @@ -213,23 +320,26 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                         .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<bool>("NoMetadata") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("no_metadata"); | ||||
|  | ||||
|                     b.Property<bool>("NoOptimization") | ||||
|                         .HasColumnType("boolean") | ||||
|                         .HasColumnName("no_optimization"); | ||||
|  | ||||
|                     b.Property<int>("RequirePrivilege") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("require_privilege"); | ||||
|                     b.Property<PolicyConfig>("PolicyConfig") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("policy_config"); | ||||
|  | ||||
|                     b.Property<RemoteStorageConfig>("StorageConfig") | ||||
|                         .IsRequired() | ||||
| @@ -248,18 +358,25 @@ namespace DysonNetwork.Drive.Migrations | ||||
|  | ||||
|             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() | ||||
|                         .WithMany("References") | ||||
|                         .HasForeignKey("FileId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
| @@ -267,6 +384,16 @@ namespace DysonNetwork.Drive.Migrations | ||||
|  | ||||
|                     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 | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -10,16 +10,19 @@ using tusdotnet.Stores; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| builder.AddServiceDefaults(); | ||||
|  | ||||
| // Configure Kestrel and server options | ||||
| builder.ConfigureAppKestrel(builder.Configuration); | ||||
| builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue); | ||||
|  | ||||
| // Add application services | ||||
| builder.Services.AddRegistryService(builder.Configuration); | ||||
|  | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
| builder.Services.AddAppRateLimiting(); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| builder.Services.AddAppSwagger(); | ||||
| builder.Services.AddDysonAuth(); | ||||
| builder.Services.AddAccountService(); | ||||
|  | ||||
| builder.Services.AddAppFileStorage(builder.Configuration); | ||||
|  | ||||
| @@ -36,6 +39,8 @@ builder.Services.AddTransient<IPageDataProvider, VersionPageData>(); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| app.MapDefaultEndpoints(); | ||||
|  | ||||
| // Run database migrations | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
| @@ -46,9 +51,7 @@ using (var scope = app.Services.CreateScope()) | ||||
| var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>(); | ||||
|  | ||||
| // Configure application middleware pipeline | ||||
| app.ConfigureAppMiddleware(tusDiskStore); | ||||
|  | ||||
| app.MapGatewayProxy(); | ||||
| app.ConfigureAppMiddleware(tusDiskStore, builder.Environment.ContentRootPath); | ||||
|  | ||||
| app.MapPages(Path.Combine(app.Environment.WebRootPath, "dist", "index.html")); | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using Microsoft.Extensions.FileProviders; | ||||
| using tusdotnet; | ||||
| using tusdotnet.Interfaces; | ||||
|  | ||||
| @@ -6,7 +7,7 @@ namespace DysonNetwork.Drive.Startup; | ||||
|  | ||||
| public static class ApplicationBuilderExtensions | ||||
| { | ||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app, ITusStore tusStore) | ||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app, ITusStore tusStore, string contentRoot) | ||||
|     { | ||||
|         // Configure the HTTP request pipeline. | ||||
|         if (app.Environment.IsDevelopment()) | ||||
| @@ -18,6 +19,21 @@ public static class ApplicationBuilderExtensions | ||||
|         app.UseAuthorization(); | ||||
|         app.MapControllers(); | ||||
|          | ||||
|         app.UseCors(opts => | ||||
|             opts.SetIsOriginAllowed(_ => true) | ||||
|                 .WithExposedHeaders("*") | ||||
|                 .WithHeaders("*") | ||||
|                 .AllowCredentials() | ||||
|                 .AllowAnyHeader() | ||||
|                 .AllowAnyMethod() | ||||
|         ); | ||||
|          | ||||
|         app.UseDefaultFiles(); | ||||
|         app.UseStaticFiles(new StaticFileOptions | ||||
|         { | ||||
|             FileProvider = new PhysicalFileProvider(Path.Combine(contentRoot, "wwwroot", "dist")) | ||||
|         }); | ||||
|          | ||||
|         app.MapTus("/api/tus", _ => Task.FromResult(TusService.BuildConfiguration(tusStore, app.Configuration))); | ||||
|  | ||||
|         return app; | ||||
|   | ||||
							
								
								
									
										72
									
								
								DysonNetwork.Drive/Startup/BroadcastEventHandler.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								DysonNetwork.Drive/Startup/BroadcastEventHandler.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NATS.Client.Core; | ||||
| using NATS.Client.JetStream.Models; | ||||
| using NATS.Net; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Startup; | ||||
|  | ||||
| public class BroadcastEventHandler( | ||||
|     INatsConnection nats, | ||||
|     ILogger<BroadcastEventHandler> logger, | ||||
|     IServiceProvider serviceProvider | ||||
| ) : BackgroundService | ||||
| { | ||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|     { | ||||
|         var js = nats.CreateJetStreamContext(); | ||||
|  | ||||
|         await js.EnsureStreamCreated("account_events", [AccountDeletedEvent.Type]); | ||||
|          | ||||
|         var consumer = await js.CreateOrUpdateConsumerAsync("account_events", | ||||
|             new ConsumerConfig("drive_account_deleted_handler"), cancellationToken: stoppingToken); | ||||
|  | ||||
|         await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken)) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var evt = JsonSerializer.Deserialize<AccountDeletedEvent>(msg.Data); | ||||
|                 if (evt == null) | ||||
|                 { | ||||
|                     await msg.AckAsync(cancellationToken: stoppingToken); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 logger.LogInformation("Account deleted: {AccountId}", evt.AccountId); | ||||
|  | ||||
|                 using var scope = serviceProvider.CreateScope(); | ||||
|                 var fs = scope.ServiceProvider.GetRequiredService<FileService>(); | ||||
|                 var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||
|  | ||||
|                 await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken: stoppingToken); | ||||
|                 try | ||||
|                 { | ||||
|                     var files = await db.Files | ||||
|                         .Where(p => p.AccountId == evt.AccountId) | ||||
|                         .ToListAsync(cancellationToken: stoppingToken); | ||||
|  | ||||
|                     await fs.DeleteFileDataBatchAsync(files); | ||||
|                     await db.Files | ||||
|                         .Where(p => p.AccountId == evt.AccountId) | ||||
|                         .ExecuteDeleteAsync(cancellationToken: stoppingToken); | ||||
|  | ||||
|                     await transaction.CommitAsync(cancellationToken: stoppingToken); | ||||
|                 } | ||||
|                 catch (Exception) | ||||
|                 { | ||||
|                     await transaction.RollbackAsync(cancellationToken: stoppingToken); | ||||
|                     throw; | ||||
|                 } | ||||
|  | ||||
|                 await msg.AckAsync(cancellationToken: stoppingToken); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 logger.LogError(ex, "Error processing AccountDeleted"); | ||||
|                 await msg.NakAsync(cancellationToken: stoppingToken); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,3 +1,4 @@ | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using Quartz; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Startup; | ||||
| @@ -14,6 +15,13 @@ public static class ScheduledJobsConfiguration | ||||
|                 .ForJob(appDatabaseRecyclingJob) | ||||
|                 .WithIdentity("AppDatabaseRecyclingTrigger") | ||||
|                 .WithCronSchedule("0 0 0 * * ?")); | ||||
|              | ||||
|             var cloudFileUnusedRecyclingJob = new JobKey("CloudFileUnusedRecycling"); | ||||
|             q.AddJob<CloudFileUnusedRecyclingJob>(opts => opts.WithIdentity(cloudFileUnusedRecyclingJob)); | ||||
|             q.AddTrigger(opts => opts | ||||
|                 .ForJob(cloudFileUnusedRecyclingJob) | ||||
|                 .WithIdentity("CloudFileUnusedRecyclingTrigger") | ||||
|                 .WithCronSchedule("0 0 0 * * ?")); | ||||
|         }); | ||||
|         services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using System.Threading.RateLimiting; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.AspNetCore.RateLimiting; | ||||
| @@ -16,11 +17,6 @@ public static class ServiceCollectionExtensions | ||||
|     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddDbContext<AppDatabase>(); // Assuming you'll have an AppDatabase | ||||
|         services.AddSingleton<IConnectionMultiplexer>(_ => | ||||
|         { | ||||
|             var connection = configuration.GetConnectionString("FastRetrieve")!; | ||||
|             return ConnectionMultiplexer.Connect(connection); | ||||
|         }); | ||||
|         services.AddSingleton<IClock>(SystemClock.Instance); | ||||
|         services.AddHttpContextAccessor(); | ||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); // Uncomment if you have CacheServiceRedis | ||||
| @@ -40,6 +36,7 @@ public static class ServiceCollectionExtensions | ||||
|  | ||||
|         services.AddControllers().AddJsonOptions(options => | ||||
|         { | ||||
|             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|  | ||||
| @@ -138,6 +135,10 @@ public static class ServiceCollectionExtensions | ||||
|     { | ||||
|         services.AddScoped<Storage.FileService>(); | ||||
|         services.AddScoped<Storage.FileReferenceService>(); | ||||
|         services.AddScoped<Billing.UsageService>(); | ||||
|         services.AddScoped<Billing.QuotaService>(); | ||||
|  | ||||
|         services.AddHostedService<BroadcastEventHandler>(); | ||||
|          | ||||
|         return services; | ||||
|     } | ||||
|   | ||||
							
								
								
									
										153
									
								
								DysonNetwork.Drive/Storage/BundleController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								DysonNetwork.Drive/Storage/BundleController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Storage; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/bundles")] | ||||
| public class BundleController(AppDatabase db) : ControllerBase | ||||
| { | ||||
|     public class BundleRequest | ||||
|     { | ||||
|         [MaxLength(1024)] public string? Slug { get; set; } | ||||
|         [MaxLength(1024)] public string? Name { get; set; } | ||||
|         [MaxLength(8192)] public string? Description { get; set; } | ||||
|         [MaxLength(256)] public string? Passcode { get; set; } | ||||
|  | ||||
|         public Instant? ExpiredAt { get; set; } | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{id:guid}")] | ||||
|     public async Task<ActionResult<FileBundle>> GetBundle([FromRoute] Guid id, [FromQuery] string? passcode) | ||||
|     { | ||||
|         var bundle = await db.Bundles | ||||
|             .Where(e => e.Id == id) | ||||
|             .Include(e => e.Files) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (bundle is null) return NotFound(); | ||||
|         if (!bundle.VerifyPasscode(passcode)) return Forbid(); | ||||
|  | ||||
|         return Ok(bundle); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("me")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<FileBundle>>> ListBundles( | ||||
|         [FromQuery] string? term, | ||||
|         [FromQuery] int offset = 0, | ||||
|         [FromQuery] int take = 20 | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|  | ||||
|         var query = db.Bundles | ||||
|             .Where(e => e.AccountId == accountId) | ||||
|             .OrderByDescending(e => e.CreatedAt) | ||||
|             .AsQueryable(); | ||||
|         if (!string.IsNullOrEmpty(term)) | ||||
|             query = query.Where(e => EF.Functions.ILike(e.Name, $"%{term}%")); | ||||
|  | ||||
|         var total = await query.CountAsync(); | ||||
|         Response.Headers.Append("X-Total", total.ToString()); | ||||
|  | ||||
|         var bundles = await query | ||||
|             .Skip(offset) | ||||
|             .Take(take) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         return Ok(bundles); | ||||
|     } | ||||
|  | ||||
|     [HttpPost] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<FileBundle>> CreateBundle([FromBody] BundleRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|  | ||||
|         if (currentUser.PerkSubscription is null && !string.IsNullOrEmpty(request.Slug)) | ||||
|             return StatusCode(403, "You must have a subscription to create a bundle with a custom slug"); | ||||
|         if (string.IsNullOrEmpty(request.Slug)) | ||||
|             request.Slug = Guid.NewGuid().ToString("N")[..6]; | ||||
|         if (string.IsNullOrEmpty(request.Name)) | ||||
|             request.Name = "Unnamed Bundle"; | ||||
|  | ||||
|         var bundle = new FileBundle | ||||
|         { | ||||
|             Slug = request.Slug, | ||||
|             Name = request.Name, | ||||
|             Description = request.Description, | ||||
|             Passcode = request.Passcode, | ||||
|             ExpiredAt = request.ExpiredAt, | ||||
|             AccountId = accountId | ||||
|         }.HashPasscode(); | ||||
|  | ||||
|         db.Bundles.Add(bundle); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         return Ok(bundle); | ||||
|     } | ||||
|  | ||||
|     [HttpPut("{id:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<FileBundle>> UpdateBundle([FromRoute] Guid id, [FromBody] BundleRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|  | ||||
|         var bundle = await db.Bundles | ||||
|             .Where(e => e.Id == id) | ||||
|             .Where(e => e.AccountId == accountId) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (bundle is null) return NotFound(); | ||||
|  | ||||
|         if (request.Slug != null && request.Slug != bundle.Slug) | ||||
|         { | ||||
|             if (currentUser.PerkSubscription is null) | ||||
|                 return StatusCode(403, "You must have a subscription to change the slug of a bundle"); | ||||
|             bundle.Slug = request.Slug; | ||||
|         } | ||||
|  | ||||
|         if (request.Name != null) bundle.Name = request.Name; | ||||
|         if (request.Description != null) bundle.Description = request.Description; | ||||
|         if (request.ExpiredAt != null) bundle.ExpiredAt = request.ExpiredAt; | ||||
|  | ||||
|         if (request.Passcode != null) | ||||
|         { | ||||
|             bundle.Passcode = request.Passcode; | ||||
|             bundle = bundle.HashPasscode(); | ||||
|         } | ||||
|  | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         return Ok(bundle); | ||||
|     } | ||||
|  | ||||
|     [HttpDelete("{id:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> DeleteBundle([FromRoute] Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|  | ||||
|         var bundle = await db.Bundles | ||||
|             .Where(e => e.Id == id) | ||||
|             .Where(e => e.AccountId == accountId) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (bundle is null) return NotFound(); | ||||
|  | ||||
|         db.Bundles.Remove(bundle); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         await db.Files | ||||
|             .Where(e => e.BundleId == id) | ||||
|             .ExecuteUpdateAsync(s => s.SetProperty(e => e.IsMarkedRecycle, true)); | ||||
|  | ||||
|         return NoContent(); | ||||
|     } | ||||
| } | ||||
| @@ -1,35 +1,18 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Google.Protobuf; | ||||
| using Newtonsoft.Json; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Storage; | ||||
|  | ||||
| /// <summary> | ||||
| /// The class that used in jsonb columns which referenced the cloud file. | ||||
| /// The aim of this class is to store some properties that won't change to a file to reduce the database load. | ||||
| /// </summary> | ||||
| public class CloudFileReferenceObject : ModelBase, ICloudFile | ||||
| { | ||||
|     public string Id { get; set; } = null!; | ||||
|     public string Name { get; set; } = string.Empty; | ||||
|     public Dictionary<string, object?> FileMeta { get; set; } = null!; | ||||
|     public Dictionary<string, object>? UserMeta { get; set; } = null!; | ||||
|     public string? MimeType { get; set; } | ||||
|     public string? Hash { get; set; } | ||||
|     public long Size { get; set; } | ||||
|     public bool HasCompression { get; set; } = false; | ||||
| } | ||||
|  | ||||
| public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | ||||
| { | ||||
|     /// The id generated by TuS, basically just UUID remove the dash lines | ||||
|     [MaxLength(32)] | ||||
|     public string Id { get; set; } = Guid.NewGuid().ToString(); | ||||
|     public string Id { get; set; } = Guid.NewGuid().ToString().Replace("-", string.Empty); | ||||
|  | ||||
|     [MaxLength(1024)] public string Name { get; set; } = string.Empty; | ||||
|     [MaxLength(4096)] public string? Description { get; set; } | ||||
| @@ -38,18 +21,17 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | ||||
|     [Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = []; | ||||
|     [MaxLength(256)] public string? MimeType { get; set; } | ||||
|     [MaxLength(256)] public string? Hash { get; set; } | ||||
|     public Instant? ExpiredAt { get; set; } | ||||
|     public long Size { get; set; } | ||||
|     public Instant? UploadedAt { get; set; } | ||||
|     public bool HasCompression { get; set; } = false; | ||||
|     public bool HasThumbnail { get; set; } = false; | ||||
|     public bool IsEncrypted { get; set; } = false; | ||||
|  | ||||
|     [JsonIgnore] public FilePool? Pool { get; set; } | ||||
|     public FilePool? Pool { get; set; } | ||||
|     public Guid? PoolId { get; set; } | ||||
|  | ||||
|     [Obsolete("Deprecated, use PoolId instead. For database migration only.")] | ||||
|     [MaxLength(128)] | ||||
|     public string? UploadedTo { get; set; } | ||||
|     [JsonIgnore] public FileBundle? Bundle { get; set; } | ||||
|     public Guid? BundleId { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// The field is set to true if the recycling job plans to delete the file. | ||||
| @@ -71,6 +53,12 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | ||||
|     [MaxLength(4096)] | ||||
|     public string? StorageUrl { get; set; } | ||||
|  | ||||
|     [NotMapped] | ||||
|     [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||||
|     public string? FastUploadLink { get; set; } | ||||
|      | ||||
|     public ICollection<CloudFileReference> References { get; set; } = new List<CloudFileReference>(); | ||||
|  | ||||
|     public Guid AccountId { get; set; } | ||||
|  | ||||
|     public CloudFileReferenceObject ToReferenceObject() | ||||
| @@ -82,8 +70,9 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | ||||
|             DeletedAt = DeletedAt, | ||||
|             Id = Id, | ||||
|             Name = Name, | ||||
|             FileMeta = FileMeta, | ||||
|             UserMeta = UserMeta, | ||||
|             FileMeta = FileMeta ?? [], | ||||
|             UserMeta = UserMeta ?? [], | ||||
|             SensitiveMarks = SensitiveMarks, | ||||
|             MimeType = MimeType, | ||||
|             Hash = Hash, | ||||
|             Size = Size, | ||||
| @@ -102,7 +91,7 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | ||||
|         var proto = new Shared.Proto.CloudFile | ||||
|         { | ||||
|             Id = Id, | ||||
|             Name = Name ?? string.Empty, | ||||
|             Name = Name, | ||||
|             MimeType = MimeType ?? string.Empty, | ||||
|             Hash = Hash ?? string.Empty, | ||||
|             Size = Size, | ||||
| @@ -111,13 +100,10 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | ||||
|             ContentType = MimeType ?? string.Empty, | ||||
|             UploadedAt = UploadedAt?.ToTimestamp(), | ||||
|             // Convert file metadata | ||||
|             FileMeta = ByteString.CopyFromUtf8( | ||||
|                 System.Text.Json.JsonSerializer.Serialize(FileMeta, GrpcTypeHelper.SerializerOptions) | ||||
|             ), | ||||
|             FileMeta = GrpcTypeHelper.ConvertObjectToByteString(FileMeta), | ||||
|             // Convert user metadata | ||||
|             UserMeta = ByteString.CopyFromUtf8( | ||||
|                 System.Text.Json.JsonSerializer.Serialize(UserMeta, GrpcTypeHelper.SerializerOptions) | ||||
|             ) | ||||
|             UserMeta = GrpcTypeHelper.ConvertObjectToByteString(UserMeta), | ||||
|             SensitiveMarks = GrpcTypeHelper.ConvertObjectToByteString(SensitiveMarks) | ||||
|         }; | ||||
|  | ||||
|         return proto; | ||||
|   | ||||
| @@ -7,19 +7,42 @@ namespace DysonNetwork.Drive.Storage; | ||||
| public class CloudFileUnusedRecyclingJob( | ||||
|     AppDatabase db, | ||||
|     FileReferenceService fileRefService, | ||||
|     ILogger<CloudFileUnusedRecyclingJob> logger | ||||
|     ILogger<CloudFileUnusedRecyclingJob> logger, | ||||
|     IConfiguration configuration | ||||
| ) | ||||
|     : IJob | ||||
| { | ||||
|     public async Task Execute(IJobExecutionContext context) | ||||
|     { | ||||
|         logger.LogInformation("Cleaning tus cloud files..."); | ||||
|         var storePath = configuration["Tus:StorePath"]; | ||||
|         if (Directory.Exists(storePath)) | ||||
|         { | ||||
|             var oneHourAgo = SystemClock.Instance.GetCurrentInstant() - Duration.FromHours(1); | ||||
|             var files = Directory.GetFiles(storePath); | ||||
|             foreach (var file in files) | ||||
|             { | ||||
|                 var creationTime = File.GetCreationTime(file).ToUniversalTime(); | ||||
|                 if (creationTime < oneHourAgo.ToDateTimeUtc()) | ||||
|                     File.Delete(file); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         logger.LogInformation("Marking unused cloud files..."); | ||||
|  | ||||
|         var recyclablePools = await db.Pools | ||||
|             .Where(p => p.PolicyConfig.EnableRecycle) | ||||
|             .Select(p => p.Id) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         const int batchSize = 1000; // Process larger batches for efficiency | ||||
|         var processedCount = 0; | ||||
|         var markedCount = 0; | ||||
|         var totalFiles = await db.Files.Where(f => !f.IsMarkedRecycle).CountAsync(); | ||||
|         var totalFiles = await db.Files | ||||
|             .Where(f => f.PoolId.HasValue && recyclablePools.Contains(f.PoolId.Value)) | ||||
|             .Where(f => !f.IsMarkedRecycle) | ||||
|             .CountAsync(); | ||||
|  | ||||
|         logger.LogInformation("Found {TotalFiles} files to check for unused status", totalFiles); | ||||
|  | ||||
| @@ -35,13 +58,12 @@ public class CloudFileUnusedRecyclingJob( | ||||
|         { | ||||
|             // Query for the next batch of files using keyset pagination | ||||
|             var filesQuery = db.Files | ||||
|                 .Where(f => f.PoolId.HasValue && recyclablePools.Contains(f.PoolId.Value)) | ||||
|                 .Where(f => !f.IsMarkedRecycle) | ||||
|                 .Where(f => f.CreatedAt <= ageThreshold); // Only process older files first | ||||
|  | ||||
|             if (lastProcessedId != null) | ||||
|             { | ||||
|                 filesQuery = filesQuery.Where(f => string.Compare(f.Id, lastProcessedId) > 0); | ||||
|             } | ||||
|  | ||||
|             var fileBatch = await filesQuery | ||||
|                 .OrderBy(f => f.Id) // Ensure consistent ordering for pagination | ||||
| @@ -84,10 +106,18 @@ public class CloudFileUnusedRecyclingJob( | ||||
|             { | ||||
|                 logger.LogInformation( | ||||
|                     "Progress: processed {ProcessedCount}/{TotalFiles} files, marked {MarkedCount} for recycling", | ||||
|                     processedCount, totalFiles, markedCount); | ||||
|                     processedCount, | ||||
|                     totalFiles, | ||||
|                     markedCount | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         var expiredCount = await db.Files | ||||
|             .Where(f => f.ExpiredAt.HasValue && f.ExpiredAt.Value <= now) | ||||
|             .ExecuteUpdateAsync(s => s.SetProperty(f => f.IsMarkedRecycle, true)); | ||||
|         markedCount += expiredCount; | ||||
|  | ||||
|         logger.LogInformation("Completed marking {MarkedCount} files for recycling", markedCount); | ||||
|     } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user