Compare commits
	
		
			207 Commits
		
	
	
		
			0117fdf084
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8f1047ff5d | |||
| 43e50a00ce | |||
| 50133684c7 | |||
| befde25266 | |||
| 437f49fb20 | |||
| c3b6358f33 | |||
| 4347281fcd | |||
| 92cd6b5f7e | |||
| cf6e534d02 | |||
| 29c5971554 | |||
| cdfc3f6571 | |||
| f65a7360e2 | |||
| 85e706335a | |||
| fe74060df9 | |||
| e8d5f22395 | |||
| 83fa2568aa | |||
| bf1c8e0a85 | |||
| 323fa8ee15 | |||
| e7a46e96ed | |||
| 3a0dee11a6 | |||
| 43be47d526 | |||
| 48067af034 | |||
| 7e7e90ad24 | |||
| 3af4069581 | |||
| 609b130b4e | |||
| 93f7dfd379 | |||
| 40325c6df5 | |||
| bbcaa27ac5 | |||
| 19d833a522 | |||
| a94102e136 | |||
| fc693793fe | |||
| 8cfdabbae4 | |||
| 985ff41c72 | |||
| a79ea4ac49 | |||
| 7385caff9a | |||
| 15954dbfe2 | |||
| 4ba6206c9d | |||
| 266b9e36e2 | |||
| e6aa61b03b | |||
| 0c09ef25ec | |||
| dd5929c691 | |||
| cf87fdfb49 | |||
| ff03584518 | |||
| d6c37784e1 | |||
| 46ebd92dc1 | |||
| 7f8521bb40 | |||
| f01226d91a | |||
| 6cb6dee6be | |||
| 0e9caf67ff | |||
| ca70bb5487 | |||
| 59ed135f20 | |||
| 6077f91529 | |||
| 5c485bb1c3 | |||
| 27d979d77b | |||
| 15687a0c32 | |||
| 37ea882ef7 | |||
| e624c2bb3e | |||
| 9631cd3edd | |||
| f4a659fce5 | |||
| 1ded811b36 | |||
| 32977d9580 | |||
| aaf29e7228 | |||
| 658ef3bddf | |||
| fc0bc936ce | |||
| 3850ae6a8e | |||
| 21c99567b4 | |||
| 1315c7f4d4 | |||
| 630a532d98 | |||
| b9bb180113 | |||
| 04d74d0d70 | |||
| 6a8a0ed491 | |||
| 0f835845bf | |||
| c5d8a8d07f | |||
| 95e2ba1136 | |||
| 1176fde8b4 | |||
| e634968e00 | |||
| 282a1dbddc | |||
| c64adace24 | |||
| 8ac0b28c66 | |||
| 8f71d7f9e5 | |||
| c435e63917 | |||
| 243159e4cc | |||
| 42dad7095a | |||
| d1efcdede8 | |||
| 47680475b3 | |||
| 6632d43f32 | |||
| 29c4dcd71c | |||
| e7aa887715 | |||
| 0f05633996 | |||
| 966af08a33 | |||
| b25b90a074 | |||
| dcbefeaaab | |||
| eb83a0392a | |||
| 85fefcf724 | |||
| d17c26a228 | |||
| 2e5ef8ff94 | |||
| 7a5f410e36 | |||
| 0b4e8a9777 | |||
| 30fd912281 | |||
| 5bf58f0194 | |||
| 8e3e3f09df | |||
| fa24f14c05 | |||
| a93b633e84 | |||
| 97a7b876db | |||
| 909fe173c2 | |||
| 58a44e8af4 | |||
| 1075177511 | |||
| 78f8a9e638 | |||
| 9ce31c4dd8 | |||
| e70d8371f8 | |||
| 51b6f7309e | |||
| d75876a772 | |||
| 4910c3296b | |||
| 7b924fa075 | |||
| d69c9f9623 | |||
| a88d828e21 | |||
| 14c93d372e | |||
| adf371a72e | |||
| c03f2472fa | |||
| 50efe62bac | |||
| 7bc94a9646 | |||
| d9fe1273b5 | |||
| ff9d490869 | |||
| 266312e97e | |||
| 7087736e31 | |||
| 82bf1608fd | |||
| 3b3287db0b | |||
| 4573d9395f | |||
| a8c99b3128 | |||
| fdd7bd3c9d | |||
| b785d0098b | |||
| 5b31357fe9 | |||
| d5a5721402 | |||
| 204640a759 | |||
| e3657386cd | |||
| f81e3dc9f4 | |||
| b2a0d25ffa | |||
| e1459951c4 | |||
| a88843a4c2 | |||
| 4d83c2de31 | |||
| f63c934cee | |||
| 001da9ae40 | |||
| 4efbfa948a | |||
| 3458e85a8b | |||
| 3710169f8c | |||
| 9e4a58a8a0 | |||
| dc93991de2 | |||
| b0154e1a63 | |||
| 66e14ffedb | |||
| b152edb848 | |||
| 2ace444dbb | |||
| 634958ffc5 | |||
| 1e374a73c7 | |||
| cc59e046bd | |||
| f3dcff2e4a | |||
| 1a5723c880 | |||
| 96559a2c26 | |||
| 366edfc14f | |||
| 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 | 
							
								
								
									
										4
									
								
								.aspire/settings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.aspire/settings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "appHostPath": "../DysonNetwork.Control/DysonNetwork.Control.csproj" | ||||
| } | ||||
|  | ||||
							
								
								
									
										5
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| root = true | ||||
|  | ||||
| [*] | ||||
| indent_style = space | ||||
| indent_size = 4 | ||||
							
								
								
									
										38
									
								
								.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								.env
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| # 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 | ||||
|  | ||||
| # Container image name for gateway | ||||
| GATEWAY_IMAGE=gateway:latest | ||||
							
								
								
									
										292
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										292
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,189 +1,103 @@ | ||||
|  name: Build and Push Microservices | ||||
|   | ||||
|  on: | ||||
|    push: | ||||
|      branches: | ||||
|        - master | ||||
|    workflow_dispatch: | ||||
|   | ||||
|  jobs: | ||||
|    build-sphere: | ||||
|      runs-on: ubuntu-latest | ||||
|      permissions: | ||||
|        contents: read | ||||
|        packages: write | ||||
|      steps: | ||||
|        - name: Checkout repository | ||||
|          uses: actions/checkout@v3 | ||||
|          with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|        - name: Log in to GitHub Container Registry | ||||
|          uses: docker/login-action@v3 | ||||
|          with: | ||||
|            registry: ghcr.io | ||||
|            username: ${{ github.actor }} | ||||
|            password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Sphere Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Sphere/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-sphere:latest | ||||
|            platforms: linux/amd64 | ||||
|   | ||||
|    build-pass: | ||||
|      runs-on: ubuntu-latest | ||||
|      permissions: | ||||
|        contents: read | ||||
|        packages: write | ||||
|      steps: | ||||
|        - name: Checkout repository | ||||
|          uses: actions/checkout@v3 | ||||
|          with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|        - name: Log in to GitHub Container Registry | ||||
|          uses: docker/login-action@v3 | ||||
|          with: | ||||
|            registry: ghcr.io | ||||
|            username: ${{ github.actor }} | ||||
|            password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Pass Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Pass/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pass:latest | ||||
|            platforms: linux/amd64 | ||||
|   | ||||
|    build-pusher: | ||||
|      runs-on: ubuntu-latest | ||||
|      permissions: | ||||
|        contents: read | ||||
|        packages: write | ||||
|      steps: | ||||
|        - name: Checkout repository | ||||
|          uses: actions/checkout@v3 | ||||
|          with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|        - name: Log in to GitHub Container Registry | ||||
|          uses: docker/login-action@v3 | ||||
|          with: | ||||
|            registry: ghcr.io | ||||
|            username: ${{ github.actor }} | ||||
|            password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Pusher Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Pusher/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pusher:latest | ||||
|            platforms: linux/amd64 | ||||
|   | ||||
|    build-drive: | ||||
|      runs-on: ubuntu-latest | ||||
|      permissions: | ||||
|        contents: read | ||||
|        packages: write | ||||
|      steps: | ||||
|        - name: Checkout repository | ||||
|          uses: actions/checkout@v3 | ||||
|          with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|        - name: Log in to GitHub Container Registry | ||||
|          uses: docker/login-action@v3 | ||||
|          with: | ||||
|            registry: ghcr.io | ||||
|            username: ${{ github.actor }} | ||||
|            password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Drive Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Drive/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-drive:latest | ||||
|            platforms: linux/amd64 | ||||
|   | ||||
|    build-gateway: | ||||
|      runs-on: ubuntu-latest | ||||
|      permissions: | ||||
|        contents: read | ||||
|        packages: write | ||||
|      steps: | ||||
|        - name: Checkout repository | ||||
|          uses: actions/checkout@v3 | ||||
|          with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|        - name: Log in to GitHub Container Registry | ||||
|          uses: docker/login-action@v3 | ||||
|          with: | ||||
|            registry: ghcr.io | ||||
|            username: ${{ github.actor }} | ||||
|            password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Gateway Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Gateway/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-gateway:latest | ||||
|            platforms: linux/amd64 | ||||
|   | ||||
|    build-develop: | ||||
|      runs-on: ubuntu-latest | ||||
|      permissions: | ||||
|        contents: read | ||||
|        packages: write | ||||
|      steps: | ||||
|        - name: Checkout repository | ||||
|          uses: actions/checkout@v3 | ||||
|          with: | ||||
|            fetch-depth: 0 | ||||
|        - name: Setup NBGV | ||||
|          uses: dotnet/nbgv@master | ||||
|          id: nbgv | ||||
|        - name: Set up Docker Buildx | ||||
|          uses: docker/setup-buildx-action@v3 | ||||
|        - name: Log in to GitHub Container Registry | ||||
|          uses: docker/login-action@v3 | ||||
|          with: | ||||
|            registry: ghcr.io | ||||
|            username: ${{ github.actor }} | ||||
|            password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        - name: Build and push DysonNetwork.Develop Docker image | ||||
|          uses: docker/build-push-action@v6 | ||||
|          with: | ||||
|            file: DysonNetwork.Develop/Dockerfile | ||||
|            context: . | ||||
|            push: true | ||||
|            tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-develop:latest | ||||
|            platforms: linux/amd64 | ||||
|   | ||||
| name: Build and Push Microservices | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
|   workflow_dispatch: | ||||
|  | ||||
| jobs: | ||||
|   determine-changes: | ||||
|     runs-on: ubuntu-latest | ||||
|     outputs: | ||||
|       matrix: ${{ steps.changes.outputs.matrix }} | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v4 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|  | ||||
|       - name: Get changed files | ||||
|         id: changed-files | ||||
|         run: | | ||||
|           echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | xargs)" >> $GITHUB_OUTPUT | ||||
|  | ||||
|       - name: Determine changed services | ||||
|         id: changes | ||||
|         run: | | ||||
|           files="${{ steps.changed-files.outputs.files }}" | ||||
|           matrix="{\"include\":[]}" | ||||
|           services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight") | ||||
|           images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight") | ||||
|           changed_services=() | ||||
|  | ||||
|           for file in $files; do | ||||
|             if [[ "$file" == DysonNetwork.Shared/* ]]; then | ||||
|               changed_services=("${services[@]}") | ||||
|               break | ||||
|             fi | ||||
|             for i in "${!services[@]}"; do | ||||
|               if [[ "$file" == DysonNetwork.${services[$i]}/* ]]; then | ||||
|                 # check if service is already in changed_services | ||||
|                 if [[ ! " ${changed_services[@]} " =~ " ${services[$i]} " ]]; then | ||||
|                   changed_services+=("${services[$i]}") | ||||
|                 fi | ||||
|               fi | ||||
|             done | ||||
|           done | ||||
|  | ||||
|           if [ ${#changed_services[@]} -gt 0 ]; then | ||||
|             json_objects="" | ||||
|             for service in "${changed_services[@]}"; do | ||||
|               for i in "${!services[@]}"; do | ||||
|                 if [[ "${services[$i]}" == "$service" ]]; then | ||||
|                   image="${images[$i]}" | ||||
|                   break | ||||
|                 fi | ||||
|               done | ||||
|               json_objects+="{\"service\":\"$service\",\"image\":\"$image\"}," | ||||
|             done | ||||
|             matrix="{\"include\":[${json_objects%,}]}" | ||||
|           fi | ||||
|           echo "matrix=$matrix" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   build-and-push: | ||||
|     needs: determine-changes | ||||
|     if: ${{ needs.determine-changes.outputs.matrix != '{"include":[]}' }} | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       contents: read | ||||
|       packages: write | ||||
|     strategy: | ||||
|       matrix: ${{ fromJson(needs.determine-changes.outputs.matrix) }} | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v4 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|  | ||||
|       - name: Setup NBGV | ||||
|         uses: dotnet/nbgv@master | ||||
|         id: nbgv | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Log in to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Build and push Docker image for ${{ matrix.service }} | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           context: . | ||||
|           file: DysonNetwork.${{ matrix.service }}/Dockerfile | ||||
|           push: true | ||||
|           tags: | | ||||
|             ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:${{ steps.nbgv.outputs.SimpleVersion }} | ||||
|             ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:latest | ||||
|           platforms: linux/amd64 | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -6,3 +6,4 @@ riderModule.iml | ||||
| /_ReSharper.Caches/ | ||||
| .idea | ||||
| .DS_Store | ||||
| /Keys/ | ||||
|   | ||||
							
								
								
									
										613
									
								
								API_WALLET_FUNDS.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										613
									
								
								API_WALLET_FUNDS.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,613 @@ | ||||
| # Wallet Funds API Documentation | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| The Wallet Funds API provides red packet functionality for the DysonNetwork platform, allowing users to create and distribute funds among multiple recipients with expiration and claiming mechanisms. | ||||
|  | ||||
| ## Authentication | ||||
|  | ||||
| All endpoints require Bearer token authentication: | ||||
|  | ||||
| ``` | ||||
| Authorization: Bearer {jwt_token} | ||||
| ``` | ||||
|  | ||||
| ## Data Types | ||||
|  | ||||
| ### Enums | ||||
|  | ||||
| #### FundSplitType | ||||
| ```typescript | ||||
| enum FundSplitType { | ||||
|   Even = 0,    // Equal distribution | ||||
|   Random = 1   // Lucky draw distribution | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### FundStatus | ||||
| ```typescript | ||||
| enum FundStatus { | ||||
|   Created = 0,           // Fund created, waiting for claims | ||||
|   PartiallyReceived = 1, // Some recipients claimed | ||||
|   FullyReceived = 2,     // All recipients claimed | ||||
|   Expired = 3,           // Fund expired, unclaimed amounts refunded | ||||
|   Refunded = 4           // Legacy status | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Request/Response Models | ||||
|  | ||||
| #### CreateFundRequest | ||||
| ```typescript | ||||
| interface CreateFundRequest { | ||||
|   recipientAccountIds: string[];  // UUIDs of recipients | ||||
|   currency: string;               // e.g., "points", "golds" | ||||
|   totalAmount: number;            // Total amount to distribute | ||||
|   splitType: FundSplitType;       // Even or Random | ||||
|   message?: string;               // Optional message | ||||
|   expirationHours?: number;       // Optional: hours until expiration (default: 24) | ||||
|   pinCode: string;                // Required: 6-digit PIN code for security | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### SnWalletFund | ||||
| ```typescript | ||||
| interface SnWalletFund { | ||||
|   id: string;                     // UUID | ||||
|   currency: string; | ||||
|   totalAmount: number; | ||||
|   splitType: FundSplitType; | ||||
|   status: FundStatus; | ||||
|   message?: string; | ||||
|   creatorAccountId: string;       // UUID | ||||
|   creatorAccount: SnAccount;      // Creator account details (includes profile) | ||||
|   recipients: SnWalletFundRecipient[]; | ||||
|   expiredAt: string;              // ISO 8601 timestamp | ||||
|   createdAt: string;              // ISO 8601 timestamp | ||||
|   updatedAt: string;              // ISO 8601 timestamp | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### SnWalletFundRecipient | ||||
| ```typescript | ||||
| interface SnWalletFundRecipient { | ||||
|   id: string;                     // UUID | ||||
|   fundId: string;                 // UUID | ||||
|   recipientAccountId: string;     // UUID | ||||
|   recipientAccount: SnAccount;    // Recipient account details (includes profile) | ||||
|   amount: number;                 // Allocated amount | ||||
|   isReceived: boolean; | ||||
|   receivedAt?: string;            // ISO 8601 timestamp (if claimed) | ||||
|   createdAt: string;              // ISO 8601 timestamp | ||||
|   updatedAt: string;              // ISO 8601 timestamp | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### SnWalletTransaction | ||||
| ```typescript | ||||
| interface SnWalletTransaction { | ||||
|   id: string;                     // UUID | ||||
|   payerWalletId?: string;         // UUID (null for system transfers) | ||||
|   payeeWalletId?: string;         // UUID (null for system transfers) | ||||
|   currency: string; | ||||
|   amount: number; | ||||
|   remarks?: string; | ||||
|   type: TransactionType; | ||||
|   createdAt: string;              // ISO 8601 timestamp | ||||
|   updatedAt: string;              // ISO 8601 timestamp | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Error Response | ||||
| ```typescript | ||||
| interface ErrorResponse { | ||||
|   type: string;                   // Error type | ||||
|   title: string;                  // Error title | ||||
|   status: number;                 // HTTP status code | ||||
|   detail: string;                 // Error details | ||||
|   instance?: string;              // Request instance | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## API Endpoints | ||||
|  | ||||
| ### 1. Create Fund | ||||
|  | ||||
| Creates a new fund (red packet) for distribution among recipients. | ||||
|  | ||||
| **Endpoint:** `POST /api/wallets/funds` | ||||
|  | ||||
| **Request Body:** `CreateFundRequest` | ||||
|  | ||||
| **Response:** `SnWalletFund` (201 Created) | ||||
|  | ||||
| **Example Request:** | ||||
| ```bash | ||||
| curl -X POST "/api/wallets/funds" \ | ||||
|   -H "Authorization: Bearer {token}" \ | ||||
|   -H "Content-Type: application/json" \ | ||||
|   -d '{ | ||||
|     "recipientAccountIds": [ | ||||
|       "550e8400-e29b-41d4-a716-446655440000", | ||||
|       "550e8400-e29b-41d4-a716-446655440001", | ||||
|       "550e8400-e29b-41d4-a716-446655440002" | ||||
|     ], | ||||
|     "currency": "points", | ||||
|     "totalAmount": 100.00, | ||||
|     "splitType": "Even", | ||||
|     "message": "Happy New Year! 🎉", | ||||
|     "expirationHours": 48, | ||||
|     "pinCode": "123456" | ||||
|   }' | ||||
| ``` | ||||
|  | ||||
| **Example Response:** | ||||
| ```json | ||||
| { | ||||
|   "id": "550e8400-e29b-41d4-a716-446655440003", | ||||
|   "currency": "points", | ||||
|   "totalAmount": 100.00, | ||||
|   "splitType": 0, | ||||
|   "status": 0, | ||||
|   "message": "Happy New Year! 🎉", | ||||
|   "creatorAccountId": "550e8400-e29b-41d4-a716-446655440004", | ||||
|   "creatorAccount": { | ||||
|     "id": "550e8400-e29b-41d4-a716-446655440004", | ||||
|     "username": "creator_user" | ||||
|   }, | ||||
|   "recipients": [ | ||||
|     { | ||||
|       "id": "550e8400-e29b-41d4-a716-446655440005", | ||||
|       "fundId": "550e8400-e29b-41d4-a716-446655440003", | ||||
|       "recipientAccountId": "550e8400-e29b-41d4-a716-446655440000", | ||||
|       "amount": 33.34, | ||||
|       "isReceived": false, | ||||
|       "createdAt": "2025-10-03T22:00:00Z", | ||||
|       "updatedAt": "2025-10-03T22:00:00Z" | ||||
|     }, | ||||
|     { | ||||
|       "id": "550e8400-e29b-41d4-a716-446655440006", | ||||
|       "fundId": "550e8400-e29b-41d4-a716-446655440003", | ||||
|       "recipientAccountId": "550e8400-e29b-41d4-a716-446655440001", | ||||
|       "amount": 33.33, | ||||
|       "isReceived": false, | ||||
|       "createdAt": "2025-10-03T22:00:00Z", | ||||
|       "updatedAt": "2025-10-03T22:00:00Z" | ||||
|     }, | ||||
|     { | ||||
|       "id": "550e8400-e29b-41d4-a716-446655440007", | ||||
|       "fundId": "550e8400-e29b-41d4-a716-446655440003", | ||||
|       "recipientAccountId": "550e8400-e29b-41d4-a716-446655440002", | ||||
|       "amount": 33.33, | ||||
|       "isReceived": false, | ||||
|       "createdAt": "2025-10-03T22:00:00Z", | ||||
|       "updatedAt": "2025-10-03T22:00:00Z" | ||||
|     } | ||||
|   ], | ||||
|   "expiredAt": "2025-10-05T22:00:00Z", | ||||
|   "createdAt": "2025-10-03T22:00:00Z", | ||||
|   "updatedAt": "2025-10-03T22:00:00Z" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| **Error Responses:** | ||||
| - `400 Bad Request`: Invalid parameters, insufficient funds, invalid recipients | ||||
| - `401 Unauthorized`: Missing or invalid authentication | ||||
| - `403 Forbidden`: Invalid PIN code | ||||
| - `422 Unprocessable Entity`: Business logic violations | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 2. Get Funds | ||||
|  | ||||
| Retrieves funds that the authenticated user is involved in (as creator or recipient). | ||||
|  | ||||
| **Endpoint:** `GET /api/wallets/funds` | ||||
|  | ||||
| **Query Parameters:** | ||||
| - `offset` (number, optional): Pagination offset (default: 0) | ||||
| - `take` (number, optional): Number of items to return (default: 20, max: 100) | ||||
| - `status` (FundStatus, optional): Filter by fund status | ||||
|  | ||||
| **Response:** `SnWalletFund[]` (200 OK) | ||||
|  | ||||
| **Headers:** | ||||
| - `X-Total`: Total number of funds matching the criteria | ||||
|  | ||||
| **Example Request:** | ||||
| ```bash | ||||
| curl -X GET "/api/wallets/funds?offset=0&take=10&status=0" \ | ||||
|   -H "Authorization: Bearer {token}" | ||||
| ``` | ||||
|  | ||||
| **Example Response:** | ||||
| ```json | ||||
| [ | ||||
|   { | ||||
|     "id": "550e8400-e29b-41d4-a716-446655440003", | ||||
|     "currency": "points", | ||||
|     "totalAmount": 100.00, | ||||
|     "splitType": 0, | ||||
|     "status": 0, | ||||
|     "message": "Happy New Year! 🎉", | ||||
|     "creatorAccountId": "550e8400-e29b-41d4-a716-446655440004", | ||||
|     "creatorAccount": { | ||||
|       "id": "550e8400-e29b-41d4-a716-446655440004", | ||||
|       "username": "creator_user" | ||||
|     }, | ||||
|     "recipients": [ | ||||
|       { | ||||
|         "id": "550e8400-e29b-41d4-a716-446655440005", | ||||
|         "fundId": "550e8400-e29b-41d4-a716-446655440003", | ||||
|         "recipientAccountId": "550e8400-e29b-41d4-a716-446655440000", | ||||
|         "amount": 33.34, | ||||
|         "isReceived": false | ||||
|       } | ||||
|     ], | ||||
|     "expiredAt": "2025-10-05T22:00:00Z", | ||||
|     "createdAt": "2025-10-03T22:00:00Z", | ||||
|     "updatedAt": "2025-10-03T22:00:00Z" | ||||
|   } | ||||
| ] | ||||
| ``` | ||||
|  | ||||
| **Error Responses:** | ||||
| - `401 Unauthorized`: Missing or invalid authentication | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 3. Get Fund | ||||
|  | ||||
| Retrieves details of a specific fund. | ||||
|  | ||||
| **Endpoint:** `GET /api/wallets/funds/{id}` | ||||
|  | ||||
| **Path Parameters:** | ||||
| - `id` (string): Fund UUID | ||||
|  | ||||
| **Response:** `SnWalletFund` (200 OK) | ||||
|  | ||||
| **Example Request:** | ||||
| ```bash | ||||
| curl -X GET "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003" \ | ||||
|   -H "Authorization: Bearer {token}" | ||||
| ``` | ||||
|  | ||||
| **Example Response:** (Same as create fund response) | ||||
|  | ||||
| **Error Responses:** | ||||
| - `401 Unauthorized`: Missing or invalid authentication | ||||
| - `403 Forbidden`: User doesn't have permission to view this fund | ||||
| - `404 Not Found`: Fund not found | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 4. Receive Fund | ||||
|  | ||||
| Claims the authenticated user's portion of a fund. | ||||
|  | ||||
| **Endpoint:** `POST /api/wallets/funds/{id}/receive` | ||||
|  | ||||
| **Path Parameters:** | ||||
| - `id` (string): Fund UUID | ||||
|  | ||||
| **Response:** `SnWalletTransaction` (200 OK) | ||||
|  | ||||
| **Example Request:** | ||||
| ```bash | ||||
| curl -X POST "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive" \ | ||||
|   -H "Authorization: Bearer {token}" | ||||
| ``` | ||||
|  | ||||
| **Example Response:** | ||||
| ```json | ||||
| { | ||||
|   "id": "550e8400-e29b-41d4-a716-446655440008", | ||||
|   "payerWalletId": null, | ||||
|   "payeeWalletId": "550e8400-e29b-41d4-a716-446655440009", | ||||
|   "currency": "points", | ||||
|   "amount": 33.34, | ||||
|   "remarks": "Received fund portion from 550e8400-e29b-41d4-a716-446655440004", | ||||
|   "type": 1, | ||||
|   "createdAt": "2025-10-03T22:05:00Z", | ||||
|   "updatedAt": "2025-10-03T22:05:00Z" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| **Error Responses:** | ||||
| - `400 Bad Request`: Fund expired, already claimed, not a recipient | ||||
| - `401 Unauthorized`: Missing or invalid authentication | ||||
| - `404 Not Found`: Fund not found | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 5. Get Wallet Overview | ||||
|  | ||||
| Retrieves a summarized overview of wallet transactions grouped by type for graphing/charting purposes. | ||||
|  | ||||
| **Endpoint:** `GET /api/wallets/overview` | ||||
|  | ||||
| **Query Parameters:** | ||||
| - `startDate` (string, optional): Start date in ISO 8601 format (e.g., "2025-01-01T00:00:00Z") | ||||
| - `endDate` (string, optional): End date in ISO 8601 format (e.g., "2025-12-31T23:59:59Z") | ||||
|  | ||||
| **Response:** `WalletOverview` (200 OK) | ||||
|  | ||||
| **Example Request:** | ||||
| ```bash | ||||
| curl -X GET "/api/wallets/overview?startDate=2025-01-01T00:00:00Z&endDate=2025-12-31T23:59:59Z" \ | ||||
|   -H "Authorization: Bearer {token}" | ||||
| ``` | ||||
|  | ||||
| **Example Response:** | ||||
| ```json | ||||
| { | ||||
|   "accountId": "550e8400-e29b-41d4-a716-446655440000", | ||||
|   "startDate": "2025-01-01T00:00:00.0000000Z", | ||||
|   "endDate": "2025-12-31T23:59:59.0000000Z", | ||||
|   "summary": { | ||||
|     "System": { | ||||
|       "type": "System", | ||||
|       "currencies": { | ||||
|         "points": { | ||||
|           "currency": "points", | ||||
|           "income": 150.00, | ||||
|           "spending": 0.00, | ||||
|           "net": 150.00 | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "Transfer": { | ||||
|       "type": "Transfer", | ||||
|       "currencies": { | ||||
|         "points": { | ||||
|           "currency": "points", | ||||
|           "income": 25.00, | ||||
|           "spending": 75.00, | ||||
|           "net": -50.00 | ||||
|         }, | ||||
|         "golds": { | ||||
|           "currency": "golds", | ||||
|           "income": 0.00, | ||||
|           "spending": 10.00, | ||||
|           "net": -10.00 | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "Order": { | ||||
|       "type": "Order", | ||||
|       "currencies": { | ||||
|         "points": { | ||||
|           "currency": "points", | ||||
|           "income": 0.00, | ||||
|           "spending": 200.00, | ||||
|           "net": -200.00 | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "totalIncome": 175.00, | ||||
|   "totalSpending": 285.00, | ||||
|   "netTotal": -110.00 | ||||
| } | ||||
| ``` | ||||
|  | ||||
| **Response Fields:** | ||||
| - `accountId`: User's account UUID | ||||
| - `startDate`/`endDate`: Date range applied (ISO 8601 format) | ||||
| - `summary`: Object keyed by transaction type | ||||
|   - `type`: Transaction type name | ||||
|   - `currencies`: Object keyed by currency code | ||||
|     - `currency`: Currency name | ||||
|     - `income`: Total money received | ||||
|     - `spending`: Total money spent | ||||
|     - `net`: Income minus spending | ||||
| - `totalIncome`: Sum of all income across all types/currencies | ||||
| - `totalSpending`: Sum of all spending across all types/currencies | ||||
| - `netTotal`: Overall net (totalIncome - totalSpending) | ||||
|  | ||||
| **Error Responses:** | ||||
| - `401 Unauthorized`: Missing or invalid authentication | ||||
|  | ||||
| ## Error Codes | ||||
|  | ||||
| ### Common Error Types | ||||
|  | ||||
| #### Validation Errors | ||||
| ```json | ||||
| { | ||||
|   "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", | ||||
|   "title": "Bad Request", | ||||
|   "status": 400, | ||||
|   "detail": "At least one recipient is required", | ||||
|   "instance": "/api/wallets/funds" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Insufficient Funds | ||||
| ```json | ||||
| { | ||||
|   "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", | ||||
|   "title": "Bad Request", | ||||
|   "status": 400, | ||||
|   "detail": "Insufficient funds", | ||||
|   "instance": "/api/wallets/funds" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Fund Not Available | ||||
| ```json | ||||
| { | ||||
|   "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", | ||||
|   "title": "Bad Request", | ||||
|   "status": 400, | ||||
|   "detail": "Fund is no longer available", | ||||
|   "instance": "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Already Claimed | ||||
| ```json | ||||
| { | ||||
|   "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", | ||||
|   "title": "Bad Request", | ||||
|   "status": 400, | ||||
|   "detail": "You have already received this fund", | ||||
|   "instance": "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Rate Limiting | ||||
|  | ||||
| - **Create Fund**: 10 requests per minute per user | ||||
| - **Get Funds**: 60 requests per minute per user | ||||
| - **Get Fund**: 60 requests per minute per user | ||||
| - **Receive Fund**: 30 requests per minute per user | ||||
|  | ||||
| ## Webhooks/Notifications | ||||
|  | ||||
| The system integrates with the platform's notification system: | ||||
|  | ||||
| - **Fund Created**: Creator receives confirmation | ||||
| - **Fund Claimed**: Creator receives notification when someone claims | ||||
| - **Fund Expired**: Creator receives refund notification | ||||
|  | ||||
| ## SDK Examples | ||||
|  | ||||
| ### JavaScript/TypeScript | ||||
|  | ||||
| ```typescript | ||||
| // Create a fund | ||||
| const createFund = async (fundData: CreateFundRequest): Promise<SnWalletFund> => { | ||||
|   const response = await fetch('/api/wallets/funds', { | ||||
|     method: 'POST', | ||||
|     headers: { | ||||
|       'Authorization': `Bearer ${token}`, | ||||
|       'Content-Type': 'application/json' | ||||
|     }, | ||||
|     body: JSON.stringify(fundData) | ||||
|   }); | ||||
|  | ||||
|   if (!response.ok) { | ||||
|     throw new Error(`HTTP error! status: ${response.status}`); | ||||
|   } | ||||
|  | ||||
|   return response.json(); | ||||
| }; | ||||
|  | ||||
| // Get user's funds | ||||
| const getFunds = async (params?: { | ||||
|   offset?: number; | ||||
|   take?: number; | ||||
|   status?: FundStatus; | ||||
| }): Promise<SnWalletFund[]> => { | ||||
|   const queryParams = new URLSearchParams(); | ||||
|   if (params?.offset) queryParams.set('offset', params.offset.toString()); | ||||
|   if (params?.take) queryParams.set('take', params.take.toString()); | ||||
|   if (params?.status !== undefined) queryParams.set('status', params.status.toString()); | ||||
|  | ||||
|   const response = await fetch(`/api/wallets/funds?${queryParams}`, { | ||||
|     headers: { | ||||
|       'Authorization': `Bearer ${token}` | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   if (!response.ok) { | ||||
|     throw new Error(`HTTP error! status: ${response.status}`); | ||||
|   } | ||||
|  | ||||
|   return response.json(); | ||||
| }; | ||||
|  | ||||
| // Claim a fund | ||||
| const receiveFund = async (fundId: string): Promise<SnWalletTransaction> => { | ||||
|   const response = await fetch(`/api/wallets/funds/${fundId}/receive`, { | ||||
|     method: 'POST', | ||||
|     headers: { | ||||
|       'Authorization': `Bearer ${token}` | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   if (!response.ok) { | ||||
|     throw new Error(`HTTP error! status: ${response.status}`); | ||||
|   } | ||||
|  | ||||
|   return response.json(); | ||||
| }; | ||||
| ``` | ||||
|  | ||||
| ### Python | ||||
|  | ||||
| ```python | ||||
| import requests | ||||
| from typing import List, Optional | ||||
| from enum import Enum | ||||
|  | ||||
| class FundSplitType(Enum): | ||||
|     EVEN = 0 | ||||
|     RANDOM = 1 | ||||
|  | ||||
| class FundStatus(Enum): | ||||
|     CREATED = 0 | ||||
|     PARTIALLY_RECEIVED = 1 | ||||
|     FULLY_RECEIVED = 2 | ||||
|     EXPIRED = 3 | ||||
|     REFUNDED = 4 | ||||
|  | ||||
| def create_fund(token: str, fund_data: dict) -> dict: | ||||
|     """Create a new fund""" | ||||
|     response = requests.post( | ||||
|         '/api/wallets/funds', | ||||
|         json=fund_data, | ||||
|         headers={ | ||||
|             'Authorization': f'Bearer {token}', | ||||
|             'Content-Type': 'application/json' | ||||
|         } | ||||
|     ) | ||||
|     response.raise_for_status() | ||||
|     return response.json() | ||||
|  | ||||
| def get_funds( | ||||
|     token: str, | ||||
|     offset: int = 0, | ||||
|     take: int = 20, | ||||
|     status: Optional[FundStatus] = None | ||||
| ) -> List[dict]: | ||||
|     """Get user's funds""" | ||||
|     params = {'offset': offset, 'take': take} | ||||
|     if status is not None: | ||||
|         params['status'] = status.value | ||||
|  | ||||
|     response = requests.get( | ||||
|         '/api/wallets/funds', | ||||
|         params=params, | ||||
|         headers={'Authorization': f'Bearer {token}'} | ||||
|     ) | ||||
|     response.raise_for_status() | ||||
|     return response.json() | ||||
|  | ||||
| def receive_fund(token: str, fund_id: str) -> dict: | ||||
|     """Claim a fund portion""" | ||||
|     response = requests.post( | ||||
|         f'/api/wallets/funds/{fund_id}/receive', | ||||
|         headers={'Authorization': f'Bearer {token}'} | ||||
|     ) | ||||
|     response.raise_for_status() | ||||
|     return response.json() | ||||
| ``` | ||||
|  | ||||
| ## Changelog | ||||
|  | ||||
| ### Version 1.0.0 | ||||
| - Initial release with basic red packet functionality | ||||
| - Support for even and random split types | ||||
| - 24-hour expiration with automatic refunds | ||||
| - RESTful API endpoints | ||||
| - Comprehensive error handling | ||||
|  | ||||
| ## Support | ||||
|  | ||||
| For API support or questions: | ||||
| - Check the main documentation at `README_WALLET_FUNDS.md` | ||||
| - Review error messages for specific guidance | ||||
| - Contact the development team for technical issues | ||||
							
								
								
									
										71
									
								
								DysonNetwork.Control/AppHost.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								DysonNetwork.Control/AppHost.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| using Microsoft.Extensions.Hosting; | ||||
|  | ||||
| var builder = DistributedApplication.CreateBuilder(args); | ||||
|  | ||||
| var isDev = builder.Environment.IsDevelopment(); | ||||
|  | ||||
| var cache = builder.AddRedis("cache"); | ||||
| var queue = builder.AddNats("queue").WithJetStream(); | ||||
|  | ||||
| var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring"); | ||||
| var passService = builder.AddProject<Projects.DysonNetwork_Pass>("pass") | ||||
|     .WithReference(ringService); | ||||
| var driveService = builder.AddProject<Projects.DysonNetwork_Drive>("drive") | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService); | ||||
| var sphereService = builder.AddProject<Projects.DysonNetwork_Sphere>("sphere") | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService) | ||||
|     .WithReference(driveService); | ||||
| var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop") | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService) | ||||
|     .WithReference(sphereService); | ||||
| var insightService = builder.AddProject<Projects.DysonNetwork_Insight>("insight") | ||||
|     .WithReference(passService) | ||||
|     .WithReference(ringService) | ||||
|     .WithReference(sphereService) | ||||
|     .WithReference(developService); | ||||
|  | ||||
| passService.WithReference(developService).WithReference(driveService); | ||||
|  | ||||
| List<IResourceBuilder<ProjectResource>> services = | ||||
|     [ringService, passService, driveService, sphereService, developService, insightService]; | ||||
|  | ||||
| for (var idx = 0; idx < services.Count; idx++) | ||||
| { | ||||
|     var service = services[idx]; | ||||
|  | ||||
|     service.WithReference(cache).WithReference(queue); | ||||
|  | ||||
|     var grpcPort = 7002 + idx; | ||||
|  | ||||
|     if (isDev) | ||||
|     { | ||||
|         service.WithEnvironment("GRPC_PORT", grpcPort.ToString()); | ||||
|  | ||||
|         var httpPort = 8001 + idx; | ||||
|         service.WithEnvironment("HTTP_PORTS", httpPort.ToString()); | ||||
|         service.WithHttpEndpoint(httpPort, targetPort: null, isProxied: false, name: "http"); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         service.WithHttpEndpoint(8080, targetPort: null, isProxied: false, name: "http"); | ||||
|     } | ||||
|  | ||||
|     service.WithEndpoint(isDev ? grpcPort : 7001, isDev ? null : 7001, "https", name: "grpc", isProxied: false); | ||||
| } | ||||
|  | ||||
| // Extra double-ended references | ||||
| ringService.WithReference(passService); | ||||
|  | ||||
| var gateway = builder.AddProject<Projects.DysonNetwork_Gateway>("gateway") | ||||
|     .WithEnvironment("HTTP_PORTS", "5001") | ||||
|     .WithHttpEndpoint(port: 5001, targetPort: null, isProxied: false, name: "http"); | ||||
|  | ||||
| foreach (var service in services) | ||||
|     gateway.WithReference(service); | ||||
|  | ||||
| builder.AddDockerComposeEnvironment("docker-compose"); | ||||
|  | ||||
| builder.Build().Run(); | ||||
							
								
								
									
										28
									
								
								DysonNetwork.Control/DysonNetwork.Control.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								DysonNetwork.Control/DysonNetwork.Control.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|   <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.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.5.2" /> | ||||
|     <PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" /> | ||||
|     <PackageReference Include="Aspire.Hosting.Nats" Version="9.5.2" /> | ||||
|     <PackageReference Include="Aspire.Hosting.Redis" Version="9.5.2" /> | ||||
|   </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" /> | ||||
|     <ProjectReference Include="..\DysonNetwork.Gateway\DysonNetwork.Gateway.csproj" /> | ||||
|     <ProjectReference Include="..\DysonNetwork.Insight\DysonNetwork.Insight.csproj" /> | ||||
|   </ItemGroup> | ||||
| </Project> | ||||
							
								
								
									
										32
									
								
								DysonNetwork.Control/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								DysonNetwork.Control/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| { | ||||
|   "$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", | ||||
|         "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21260", | ||||
|         "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22052" | ||||
|       } | ||||
|     }, | ||||
|     "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", | ||||
|         "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:22108" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										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" | ||||
|   } | ||||
| } | ||||
| @@ -1,8 +1,7 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Develop; | ||||
|  | ||||
| @@ -11,13 +10,13 @@ public class AppDatabase( | ||||
|     IConfiguration configuration | ||||
| ) : DbContext(options) | ||||
| { | ||||
|     public DbSet<Developer> Developers { get; set; } = null!; | ||||
|     public DbSet<SnDeveloper> Developers { get; set; } = null!; | ||||
|  | ||||
|     public DbSet<DevProject> DevProjects { get; set; } = null!; | ||||
|     public DbSet<SnDevProject> 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!; | ||||
|     public DbSet<SnCustomApp> CustomApps { get; set; } = null!; | ||||
|     public DbSet<SnCustomAppSecret> CustomAppSecrets { get; set; } = null!; | ||||
|     public DbSet<SnBotAccount> BotAccounts { get; set; } = null!; | ||||
|  | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|     { | ||||
| @@ -31,6 +30,35 @@ public class AppDatabase( | ||||
|  | ||||
|         base.OnConfiguring(optionsBuilder); | ||||
|     } | ||||
|      | ||||
|     public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|  | ||||
|         foreach (var entry in ChangeTracker.Entries<ModelBase>()) | ||||
|         { | ||||
|             switch (entry.State) | ||||
|             { | ||||
|                 case EntityState.Added: | ||||
|                     entry.Entity.CreatedAt = now; | ||||
|                     entry.Entity.UpdatedAt = now; | ||||
|                     break; | ||||
|                 case EntityState.Modified: | ||||
|                     entry.Entity.UpdatedAt = now; | ||||
|                     break; | ||||
|                 case EntityState.Deleted: | ||||
|                     entry.State = EntityState.Modified; | ||||
|                     entry.Entity.DeletedAt = now; | ||||
|                     break; | ||||
|                 case EntityState.Detached: | ||||
|                 case EntityState.Unchanged: | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return await base.SaveChangesAsync(cancellationToken); | ||||
|     } | ||||
|  | ||||
|     protected override void OnModelCreating(ModelBuilder modelBuilder) | ||||
|     { | ||||
|   | ||||
| @@ -9,16 +9,15 @@ | ||||
|  | ||||
|     <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"> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" /> | ||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10"> | ||||
|             <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="Swashbuckle.AspNetCore" Version="9.0.6" /> | ||||
|         <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"/> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Grpc.Core; | ||||
| @@ -16,10 +16,10 @@ namespace DysonNetwork.Develop.Identity; | ||||
| [Authorize] | ||||
| public class BotAccountController( | ||||
|     BotAccountService botService, | ||||
|     DeveloperService developerService, | ||||
|     DeveloperService ds, | ||||
|     DevProjectService projectService, | ||||
|     ILogger<BotAccountController> logger, | ||||
|     AccountClientHelper accounts, | ||||
|     RemoteAccountService remoteAccounts, | ||||
|     BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver | ||||
| ) | ||||
|     : ControllerBase | ||||
| @@ -50,9 +50,9 @@ public class BotAccountController( | ||||
|         ] | ||||
|         public string Name { get; set; } = string.Empty; | ||||
|  | ||||
|         [Required] [MaxLength(256)] public string Nick { get; set; } = string.Empty; | ||||
|         [Required][MaxLength(256)] public string Nick { get; set; } = string.Empty; | ||||
|  | ||||
|         [Required] [MaxLength(1024)] public string Slug { get; set; } = string.Empty; | ||||
|         [Required][MaxLength(1024)] public string Slug { get; set; } = string.Empty; | ||||
|  | ||||
|         [MaxLength(128)] public string Language { get; set; } = "en-us"; | ||||
|     } | ||||
| @@ -68,7 +68,7 @@ public class BotAccountController( | ||||
|  | ||||
|         [MaxLength(256)] public string? Nick { get; set; } = string.Empty; | ||||
|  | ||||
|         [Required] [MaxLength(1024)] public string? Slug { get; set; } = string.Empty; | ||||
|         [Required][MaxLength(1024)] public string? Slug { get; set; } = string.Empty; | ||||
|  | ||||
|         [MaxLength(128)] public string? Language { get; set; } | ||||
|  | ||||
| @@ -83,12 +83,12 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Viewer)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 Shared.Proto.PublisherMemberRole.Viewer)) | ||||
|             return StatusCode(403, "You must be an viewer of the developer to list bots"); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
| @@ -108,12 +108,12 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Viewer)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 Shared.Proto.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); | ||||
| @@ -137,12 +137,12 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Editor)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 Shared.Proto.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); | ||||
| @@ -206,12 +206,12 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Editor)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 Shared.Proto.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); | ||||
| @@ -222,7 +222,7 @@ public class BotAccountController( | ||||
|         if (bot is null || bot.ProjectId != projectId) | ||||
|             return NotFound("Bot not found"); | ||||
|  | ||||
|         var botAccount = await accounts.GetBotAccount(bot.Id); | ||||
|         var botAccount = await remoteAccounts.GetBotAccount(bot.Id); | ||||
|  | ||||
|         if (request.Name is not null) botAccount.Name = request.Name; | ||||
|         if (request.Nick is not null) botAccount.Nick = request.Nick; | ||||
| @@ -267,12 +267,12 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 PublisherMemberRole.Editor)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), | ||||
|                 Shared.Proto.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); | ||||
| @@ -296,7 +296,7 @@ public class BotAccountController( | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{botId:guid}/keys")] | ||||
|     public async Task<ActionResult<List<ApiKeyReference>>> ListBotKeys( | ||||
|     public async Task<ActionResult<List<SnApiKey>>> ListBotKeys( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId | ||||
| @@ -305,7 +305,7 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer); | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.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"); | ||||
| @@ -314,13 +314,13 @@ public class BotAccountController( | ||||
|         { | ||||
|             AutomatedId = bot.Id.ToString() | ||||
|         }); | ||||
|         var data = keys.Data.Select(ApiKeyReference.FromProtoValue).ToList(); | ||||
|         var data = keys.Data.Select(SnApiKey.FromProtoValue).ToList(); | ||||
|  | ||||
|         return Ok(data); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{botId:guid}/keys/{keyId:guid}")] | ||||
|     public async Task<ActionResult<ApiKeyReference>> GetBotKey( | ||||
|     public async Task<ActionResult<SnApiKey>> GetBotKey( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId, | ||||
| @@ -329,7 +329,7 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer); | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.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"); | ||||
| @@ -338,7 +338,7 @@ public class BotAccountController( | ||||
|         { | ||||
|             var key = await accountsReceiver.GetApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() }); | ||||
|             if (key == null) return NotFound("API key not found"); | ||||
|             return Ok(ApiKeyReference.FromProtoValue(key)); | ||||
|             return Ok(SnApiKey.FromProtoValue(key)); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) | ||||
|         { | ||||
| @@ -353,7 +353,7 @@ public class BotAccountController( | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{botId:guid}/keys")] | ||||
|     public async Task<ActionResult<ApiKeyReference>> CreateBotKey( | ||||
|     public async Task<ActionResult<SnApiKey>> CreateBotKey( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId, | ||||
| @@ -362,7 +362,7 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor); | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.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"); | ||||
| @@ -374,9 +374,9 @@ public class BotAccountController( | ||||
|                 AccountId = bot.Id.ToString(), | ||||
|                 Label = request.Label | ||||
|             }; | ||||
|              | ||||
|  | ||||
|             var createdKey = await accountsReceiver.CreateApiKeyAsync(newKey); | ||||
|             return Ok(ApiKeyReference.FromProtoValue(createdKey)); | ||||
|             return Ok(SnApiKey.FromProtoValue(createdKey)); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument) | ||||
|         { | ||||
| @@ -385,7 +385,7 @@ public class BotAccountController( | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{botId:guid}/keys/{keyId:guid}/rotate")] | ||||
|     public async Task<ActionResult<ApiKeyReference>> RotateBotKey( | ||||
|     public async Task<ActionResult<SnApiKey>> RotateBotKey( | ||||
|         [FromRoute] string pubName, | ||||
|         [FromRoute] Guid projectId, | ||||
|         [FromRoute] Guid botId, | ||||
| @@ -394,7 +394,7 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor); | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.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"); | ||||
| @@ -402,7 +402,7 @@ public class BotAccountController( | ||||
|         try | ||||
|         { | ||||
|             var rotatedKey = await accountsReceiver.RotateApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() }); | ||||
|             return Ok(ApiKeyReference.FromProtoValue(rotatedKey)); | ||||
|             return Ok(SnApiKey.FromProtoValue(rotatedKey)); | ||||
|         } | ||||
|         catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) | ||||
|         { | ||||
| @@ -420,7 +420,7 @@ public class BotAccountController( | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor); | ||||
|         var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.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"); | ||||
| @@ -436,17 +436,17 @@ public class BotAccountController( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private async Task<(Developer?, DevProject?, BotAccount?)> ValidateBotAccess( | ||||
|     private async Task<(SnDeveloper?, SnDevProject?, SnBotAccount?)> ValidateBotAccess( | ||||
|         string pubName, | ||||
|         Guid projectId, | ||||
|         Guid botId, | ||||
|         Account currentUser, | ||||
|         PublisherMemberRole requiredRole) | ||||
|         Shared.Proto.PublisherMemberRole requiredRole) | ||||
|     { | ||||
|         var developer = await developerService.GetDeveloperByName(pubName); | ||||
|         var developer = await ds.GetDeveloperByName(pubName); | ||||
|         if (developer == null) return (null, null, null); | ||||
|  | ||||
|         if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), requiredRole)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), requiredRole)) | ||||
|             return (null, null, null); | ||||
|  | ||||
|         var project = await projectService.GetProjectAsync(projectId, developer.Id); | ||||
| @@ -457,4 +457,4 @@ public class BotAccountController( | ||||
|  | ||||
|         return (developer, project, bot); | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Identity; | ||||
| @@ -7,7 +8,7 @@ namespace DysonNetwork.Develop.Identity; | ||||
| public class BotAccountPublicController(BotAccountService botService, DeveloperService developerService) : ControllerBase | ||||
| { | ||||
|     [HttpGet("{botId:guid}")] | ||||
|     public async Task<ActionResult<BotAccount>> GetBotTransparentInfo([FromRoute] Guid botId) | ||||
|     public async Task<ActionResult<SnBotAccount>> GetBotTransparentInfo([FromRoute] Guid botId) | ||||
|     { | ||||
|         var bot = await botService.GetBotByIdAsync(botId); | ||||
|         if (bot is null) return NotFound("Bot not found"); | ||||
| @@ -21,7 +22,7 @@ public class BotAccountPublicController(BotAccountService botService, DeveloperS | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{botId:guid}/developer")] | ||||
|     public async Task<ActionResult<Developer>> GetBotDeveloper([FromRoute] Guid botId) | ||||
|     public async Task<ActionResult<SnDeveloper>> GetBotDeveloper([FromRoute] Guid botId) | ||||
|     { | ||||
|         var bot = await botService.GetBotByIdAsync(botId); | ||||
|         if (bot is null) return NotFound("Bot not found"); | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Grpc.Core; | ||||
| @@ -11,25 +10,25 @@ namespace DysonNetwork.Develop.Identity; | ||||
| public class BotAccountService( | ||||
|     AppDatabase db, | ||||
|     BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver, | ||||
|     AccountClientHelper accounts | ||||
|     RemoteAccountService remoteAccounts | ||||
| ) | ||||
| { | ||||
|     public async Task<BotAccount?> GetBotByIdAsync(Guid id) | ||||
|     public async Task<SnBotAccount?> GetBotByIdAsync(Guid id) | ||||
|     { | ||||
|         return await db.BotAccounts | ||||
|             .Include(b => b.Project) | ||||
|             .FirstOrDefaultAsync(b => b.Id == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<IEnumerable<BotAccount>> GetBotsByProjectAsync(Guid projectId) | ||||
|     public async Task<List<SnBotAccount>> GetBotsByProjectAsync(Guid projectId) | ||||
|     { | ||||
|         return await db.BotAccounts | ||||
|             .Where(b => b.ProjectId == projectId) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<BotAccount> CreateBotAsync( | ||||
|         DevProject project, | ||||
|     public async Task<SnBotAccount> CreateBotAsync( | ||||
|         SnDevProject project, | ||||
|         string slug, | ||||
|         Account account, | ||||
|         string? pictureId, | ||||
| @@ -58,7 +57,7 @@ public class BotAccountService( | ||||
|             var botAccount = createResponse.Bot; | ||||
|  | ||||
|             // Then create the local bot account | ||||
|             var bot = new BotAccount | ||||
|             var bot = new SnBotAccount | ||||
|             { | ||||
|                 Id = automatedId, | ||||
|                 Slug = slug, | ||||
| @@ -89,8 +88,8 @@ public class BotAccountService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task<BotAccount> UpdateBotAsync( | ||||
|         BotAccount bot, | ||||
|     public async Task<SnBotAccount> UpdateBotAsync( | ||||
|         SnBotAccount bot, | ||||
|         Account account, | ||||
|         string? pictureId, | ||||
|         string? backgroundId | ||||
| @@ -98,7 +97,7 @@ public class BotAccountService( | ||||
|     { | ||||
|         db.Update(bot); | ||||
|         await db.SaveChangesAsync(); | ||||
|          | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             // Update the bot account in the Pass service | ||||
| @@ -130,7 +129,7 @@ public class BotAccountService( | ||||
|         return bot; | ||||
|     } | ||||
|  | ||||
|     public async Task DeleteBotAsync(BotAccount bot) | ||||
|     public async Task DeleteBotAsync(SnBotAccount bot) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
| @@ -153,22 +152,21 @@ public class BotAccountService( | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<BotAccount?> LoadBotAccountAsync(BotAccount bot) => | ||||
|     public async Task<SnBotAccount?> LoadBotAccountAsync(SnBotAccount bot) => | ||||
|         (await LoadBotsAccountAsync([bot])).FirstOrDefault(); | ||||
|  | ||||
|     public async Task<List<BotAccount>> LoadBotsAccountAsync(IEnumerable<BotAccount> bots) | ||||
|     public async Task<List<SnBotAccount>> LoadBotsAccountAsync(List<SnBotAccount> bots) | ||||
|     { | ||||
|         bots = bots.ToList(); | ||||
|         var automatedIds = bots.Select(b => b.Id).ToList(); | ||||
|         var data = await accounts.GetBotAccountBatch(automatedIds); | ||||
|         var data = await remoteAccounts.GetBotAccountBatch(automatedIds); | ||||
|  | ||||
|         foreach (var bot in bots) | ||||
|         { | ||||
|             bot.Account = data | ||||
|                 .Select(AccountReference.FromProtoValue) | ||||
|                 .Select(SnAccount.FromProtoValue) | ||||
|                 .FirstOrDefault(e => e.AutomatedId == bot.Id); | ||||
|         } | ||||
|  | ||||
|         return bots as List<BotAccount> ?? []; | ||||
|         return bots; | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| @@ -18,9 +19,9 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         [MaxLength(4096)] string? Description, | ||||
|         string? PictureId, | ||||
|         string? BackgroundId, | ||||
|         CustomAppStatus? Status, | ||||
|         CustomAppLinks? Links, | ||||
|         CustomAppOauthConfig? OauthConfig | ||||
|         Shared.Models.CustomAppStatus? Status, | ||||
|         SnCustomAppLinks? Links, | ||||
|         SnCustomAppOauthConfig? OauthConfig | ||||
|     ); | ||||
|  | ||||
|     public record CreateSecretRequest( | ||||
| @@ -50,7 +51,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) return NotFound(); | ||||
|  | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Viewer)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.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); | ||||
| @@ -72,7 +73,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) return NotFound(); | ||||
|          | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Viewer)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.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); | ||||
| @@ -99,7 +100,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.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); | ||||
| @@ -143,7 +144,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.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); | ||||
| @@ -180,7 +181,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.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); | ||||
| @@ -212,7 +213,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.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); | ||||
| @@ -250,7 +251,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.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); | ||||
| @@ -263,7 +264,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var secret = await customApps.CreateAppSecretAsync(new CustomAppSecret | ||||
|             var secret = await customApps.CreateAppSecretAsync(new SnCustomAppSecret | ||||
|             { | ||||
|                 AppId = appId, | ||||
|                 Description = request.Description, | ||||
| @@ -309,7 +310,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.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); | ||||
| @@ -350,7 +351,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.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); | ||||
| @@ -388,7 +389,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|         if (developer is null) | ||||
|             return NotFound("Developer not found"); | ||||
|  | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor)) | ||||
|         if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.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); | ||||
| @@ -401,7 +402,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var secret = await customApps.RotateAppSecretAsync(new CustomAppSecret | ||||
|             var secret = await customApps.RotateAppSecretAsync(new SnCustomAppSecret | ||||
|             { | ||||
|                 Id = secretId, | ||||
|                 AppId = appId, | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using DysonNetwork.Develop.Project; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using System.Security.Cryptography; | ||||
| @@ -13,7 +12,7 @@ public class CustomAppService( | ||||
|     FileService.FileServiceClient files | ||||
| ) | ||||
| { | ||||
|     public async Task<CustomApp?> CreateAppAsync( | ||||
|     public async Task<SnCustomApp?> CreateAppAsync( | ||||
|         Guid projectId, | ||||
|         CustomAppController.CustomAppRequest request | ||||
|     ) | ||||
| @@ -25,12 +24,12 @@ public class CustomAppService( | ||||
|         if (project == null) | ||||
|             return null; | ||||
|              | ||||
|         var app = new CustomApp | ||||
|         var app = new SnCustomApp | ||||
|         { | ||||
|             Slug = request.Slug!, | ||||
|             Name = request.Name!, | ||||
|             Description = request.Description, | ||||
|             Status = request.Status ?? CustomAppStatus.Developing, | ||||
|             Status = request.Status ?? Shared.Models.CustomAppStatus.Developing, | ||||
|             Links = request.Links, | ||||
|             OauthConfig = request.OauthConfig, | ||||
|             ProjectId = projectId | ||||
| @@ -46,7 +45,7 @@ public class CustomAppService( | ||||
|             ); | ||||
|             if (picture is null) | ||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); | ||||
|             app.Picture = CloudFileReferenceObject.FromProtoValue(picture); | ||||
|             app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture); | ||||
|  | ||||
|             // Create a new reference | ||||
|             await fileRefs.CreateReferenceAsync( | ||||
| @@ -65,7 +64,7 @@ public class CustomAppService( | ||||
|             ); | ||||
|             if (background is null) | ||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); | ||||
|             app.Background = CloudFileReferenceObject.FromProtoValue(background); | ||||
|             app.Background = SnCloudFileReferenceObject.FromProtoValue(background); | ||||
|  | ||||
|             // Create a new reference | ||||
|             await fileRefs.CreateReferenceAsync( | ||||
| @@ -84,7 +83,7 @@ public class CustomAppService( | ||||
|         return app; | ||||
|     } | ||||
|  | ||||
|     public async Task<CustomApp?> GetAppAsync(Guid id, Guid? projectId = null) | ||||
|     public async Task<SnCustomApp?> GetAppAsync(Guid id, Guid? projectId = null) | ||||
|     { | ||||
|         var query = db.CustomApps.AsQueryable(); | ||||
|          | ||||
| @@ -96,7 +95,7 @@ public class CustomAppService( | ||||
|         return await query.FirstOrDefaultAsync(a => a.Id == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<List<CustomAppSecret>> GetAppSecretsAsync(Guid appId) | ||||
|     public async Task<List<SnCustomAppSecret>> GetAppSecretsAsync(Guid appId) | ||||
|     { | ||||
|         return await db.CustomAppSecrets | ||||
|             .Where(s => s.AppId == appId) | ||||
| @@ -104,13 +103,13 @@ public class CustomAppService( | ||||
|             .ToListAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<CustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId) | ||||
|     public async Task<SnCustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId) | ||||
|     { | ||||
|         return await db.CustomAppSecrets | ||||
|             .FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId); | ||||
|     } | ||||
|  | ||||
|     public async Task<CustomAppSecret> CreateAppSecretAsync(CustomAppSecret secret) | ||||
|     public async Task<SnCustomAppSecret> CreateAppSecretAsync(SnCustomAppSecret secret) | ||||
|     { | ||||
|         if (string.IsNullOrWhiteSpace(secret.Secret)) | ||||
|         { | ||||
| @@ -141,7 +140,7 @@ public class CustomAppService( | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public async Task<CustomAppSecret> RotateAppSecretAsync(CustomAppSecret secretUpdate) | ||||
|     public async Task<SnCustomAppSecret> RotateAppSecretAsync(SnCustomAppSecret secretUpdate) | ||||
|     { | ||||
|         var existingSecret = await db.CustomAppSecrets | ||||
|             .FirstOrDefaultAsync(s => s.Id == secretUpdate.Id && s.AppId == secretUpdate.AppId); | ||||
| @@ -177,14 +176,14 @@ public class CustomAppService( | ||||
|         return res.ToString(); | ||||
|     } | ||||
|  | ||||
|     public async Task<List<CustomApp>> GetAppsByProjectAsync(Guid projectId) | ||||
|     public async Task<List<SnCustomApp>> GetAppsByProjectAsync(Guid projectId) | ||||
|     { | ||||
|         return await db.CustomApps | ||||
|             .Where(a => a.ProjectId == projectId) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<CustomApp?> UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request) | ||||
|     public async Task<SnCustomApp?> UpdateAppAsync(SnCustomApp app, CustomAppController.CustomAppRequest request) | ||||
|     { | ||||
|         if (request.Slug is not null) | ||||
|             app.Slug = request.Slug; | ||||
| @@ -209,7 +208,7 @@ public class CustomAppService( | ||||
|             ); | ||||
|             if (picture is null) | ||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); | ||||
|             app.Picture = CloudFileReferenceObject.FromProtoValue(picture); | ||||
|             app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture); | ||||
|  | ||||
|             // Create a new reference | ||||
|             await fileRefs.CreateReferenceAsync( | ||||
| @@ -228,7 +227,7 @@ public class CustomAppService( | ||||
|             ); | ||||
|             if (background is null) | ||||
|                 throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); | ||||
|             app.Background = CloudFileReferenceObject.FromProtoValue(background); | ||||
|             app.Background = SnCloudFileReferenceObject.FromProtoValue(background); | ||||
|  | ||||
|             // Create a new reference | ||||
|             await fileRefs.CreateReferenceAsync( | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -37,7 +38,7 @@ public class CustomAppServiceGrpc(AppDatabase db) : Shared.Proto.CustomAppServic | ||||
|         if (string.IsNullOrEmpty(request.Secret)) | ||||
|             throw new RpcException(new Status(StatusCode.InvalidArgument, "secret required")); | ||||
|  | ||||
|         IQueryable<CustomAppSecret> q = db.CustomAppSecrets; | ||||
|         IQueryable<SnCustomAppSecret> q = db.CustomAppSecrets; | ||||
|         switch (request.SecretIdentifierCase) | ||||
|         { | ||||
|             case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.SecretId: | ||||
|   | ||||
| @@ -1,79 +0,0 @@ | ||||
| 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; | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| @@ -18,7 +19,7 @@ public class DeveloperController( | ||||
|     : ControllerBase | ||||
| { | ||||
|     [HttpGet("{name}")] | ||||
|     public async Task<ActionResult<Developer>> GetDeveloper(string name) | ||||
|     public async Task<ActionResult<SnDeveloper>> GetDeveloper(string name) | ||||
|     { | ||||
|         var developer = await ds.GetDeveloperByName(name); | ||||
|         if (developer is null) return NotFound(); | ||||
| @@ -47,10 +48,9 @@ public class DeveloperController( | ||||
|  | ||||
|     [HttpGet] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<Developer>>> ListJoinedDevelopers() | ||||
|     public async Task<ActionResult<List<SnDeveloper>>> 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(); | ||||
| @@ -70,16 +70,16 @@ public class DeveloperController( | ||||
|     [HttpPost("{name}/enroll")] | ||||
|     [Authorize] | ||||
|     [RequiredPermission("global", "developers.create")] | ||||
|     public async Task<ActionResult<Developer>> EnrollDeveloperProgram(string name) | ||||
|     public async Task<ActionResult<SnDeveloper>> EnrollDeveloperProgram(string name) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|  | ||||
|         PublisherInfo? pub; | ||||
|         SnPublisher? pub; | ||||
|         try | ||||
|         { | ||||
|             var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name }); | ||||
|             pub = PublisherInfo.FromProto(pubResponse.Publisher); | ||||
|             pub = SnPublisher.FromProtoValue(pubResponse.Publisher); | ||||
|         } catch (RpcException ex) | ||||
|         { | ||||
|             return NotFound(ex.Status.Detail); | ||||
| @@ -90,14 +90,14 @@ public class DeveloperController( | ||||
|         { | ||||
|             PublisherId = pub.Id.ToString(), | ||||
|             AccountId = currentUser.Id, | ||||
|             Role = PublisherMemberRole.Owner | ||||
|             Role = Shared.Proto.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 | ||||
|         var developer = new SnDeveloper | ||||
|         { | ||||
|             Id = Guid.NewGuid(), | ||||
|             PublisherId = pub.Id | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| using DysonNetwork.Shared.Models; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -9,22 +10,22 @@ public class DeveloperService( | ||||
|     PublisherService.PublisherServiceClient ps, | ||||
|     ILogger<DeveloperService> logger) | ||||
| { | ||||
|     public async Task<Developer> LoadDeveloperPublisher(Developer developer) | ||||
|     public async Task<SnDeveloper> LoadDeveloperPublisher(SnDeveloper developer) | ||||
|     { | ||||
|         var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() }); | ||||
|         developer.Publisher = PublisherInfo.FromProto(pubResponse.Publisher); | ||||
|         developer.Publisher = SnPublisher.FromProtoValue(pubResponse.Publisher); | ||||
|         return developer; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public async Task<IEnumerable<Developer>> LoadDeveloperPublisher(IEnumerable<Developer> developers) | ||||
|     public async Task<IEnumerable<SnDeveloper>> LoadDeveloperPublisher(IEnumerable<SnDeveloper> 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); | ||||
|         var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), SnPublisher.FromProtoValue); | ||||
|  | ||||
|         return enumerable.Select(d => | ||||
|         { | ||||
| @@ -33,7 +34,7 @@ public class DeveloperService( | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public async Task<Developer?> GetDeveloperByName(string name) | ||||
|     public async Task<SnDeveloper?> GetDeveloperByName(string name) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
| @@ -50,12 +51,12 @@ public class DeveloperService( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public async Task<Developer?> GetDeveloperById(Guid id) | ||||
|     public async Task<SnDeveloper?> GetDeveloperById(Guid id) | ||||
|     { | ||||
|         return await db.Developers.FirstOrDefaultAsync(d => d.Id == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, PublisherMemberRole role) | ||||
|     public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, Shared.Proto.PublisherMemberRole role) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| @@ -35,7 +34,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
| @@ -56,7 +55,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("developer_id"); | ||||
|  | ||||
|                     b.Property<CustomAppLinks>("Links") | ||||
|                     b.Property<SnCustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
| @@ -66,11 +65,11 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
| @@ -88,7 +87,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                     b.Property<SnVerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| using System; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| @@ -35,11 +33,11 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                     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), | ||||
|                     picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||
|                     background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), | ||||
|                     verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true), | ||||
|                     oauth_config = table.Column<SnCustomAppOauthConfig>(type: "jsonb", nullable: true), | ||||
|                     links = table.Column<SnCustomAppLinks>(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), | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| @@ -35,7 +34,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
| @@ -52,7 +51,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<CustomAppLinks>("Links") | ||||
|                     b.Property<SnCustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
| @@ -62,11 +61,11 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
| @@ -88,7 +87,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                     b.Property<SnVerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| @@ -77,7 +76,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
| @@ -94,7 +93,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<CustomAppLinks>("Links") | ||||
|                     b.Property<SnCustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
| @@ -104,11 +103,11 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
| @@ -130,7 +129,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                     b.Property<SnVerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| @@ -74,7 +73,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Background") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Background") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("background"); | ||||
|  | ||||
| @@ -91,7 +90,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(4096)") | ||||
|                         .HasColumnName("description"); | ||||
|  | ||||
|                     b.Property<CustomAppLinks>("Links") | ||||
|                     b.Property<SnCustomAppLinks>("Links") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("links"); | ||||
|  | ||||
| @@ -101,11 +100,11 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("character varying(1024)") | ||||
|                         .HasColumnName("name"); | ||||
|  | ||||
|                     b.Property<CustomAppOauthConfig>("OauthConfig") | ||||
|                     b.Property<SnCustomAppOauthConfig>("OauthConfig") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("oauth_config"); | ||||
|  | ||||
|                     b.Property<CloudFileReferenceObject>("Picture") | ||||
|                     b.Property<SnCloudFileReferenceObject>("Picture") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("picture"); | ||||
|  | ||||
| @@ -127,7 +126,7 @@ namespace DysonNetwork.Develop.Migrations | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("updated_at"); | ||||
|  | ||||
|                     b.Property<VerificationMark>("Verification") | ||||
|                     b.Property<SnVerificationMark>("Verification") | ||||
|                         .HasColumnType("jsonb") | ||||
|                         .HasColumnName("verification"); | ||||
|  | ||||
|   | ||||
| @@ -1,27 +1,32 @@ | ||||
| using DysonNetwork.Develop; | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using DysonNetwork.Develop.Startup; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| builder.AddServiceDefaults(); | ||||
|  | ||||
| builder.ConfigureAppKestrel(builder.Configuration); | ||||
|  | ||||
| builder.Services.AddRegistryService(builder.Configuration); | ||||
| builder.Services.AddStreamConnection(builder.Configuration); | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| builder.Services.AddAppSwagger(); | ||||
| builder.Services.AddDysonAuth(); | ||||
| builder.Services.AddPublisherService(); | ||||
| builder.Services.AddSphereService(); | ||||
| builder.Services.AddAccountService(); | ||||
| builder.Services.AddDriveService(); | ||||
|  | ||||
| builder.AddSwaggerManifest( | ||||
|     "DysonNetwork.Develop", | ||||
|     "The developer portal in the Solar Network." | ||||
| ); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| app.MapDefaultEndpoints(); | ||||
|  | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
|     var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||
| @@ -30,4 +35,6 @@ using (var scope = app.Services.CreateScope()) | ||||
|  | ||||
| app.ConfigureAppMiddleware(builder.Configuration); | ||||
|  | ||||
| app.UseSwaggerManifest("DysonNetwork.Develop"); | ||||
|  | ||||
| app.Run(); | ||||
| @@ -1,21 +1,17 @@ | ||||
| using DysonNetwork.Develop.Identity; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Models; | ||||
|  | ||||
| namespace DysonNetwork.Develop.Project; | ||||
|  | ||||
| public class DevProjectService( | ||||
|     AppDatabase db, | ||||
|     FileReferenceService.FileReferenceServiceClient fileRefs, | ||||
|     FileService.FileServiceClient files | ||||
| ) | ||||
| public class DevProjectService(AppDatabase db ) | ||||
| { | ||||
|     public async Task<DevProject> CreateProjectAsync( | ||||
|         Developer developer, | ||||
|     public async Task<SnDevProject> CreateProjectAsync( | ||||
|         SnDeveloper developer, | ||||
|         DevProjectController.DevProjectRequest request | ||||
|     ) | ||||
|     { | ||||
|         var project = new DevProject | ||||
|         var project = new SnDevProject | ||||
|         { | ||||
|             Slug = request.Slug!, | ||||
|             Name = request.Name!, | ||||
| @@ -25,14 +21,14 @@ public class DevProjectService( | ||||
|  | ||||
|         db.DevProjects.Add(project); | ||||
|         await db.SaveChangesAsync(); | ||||
|          | ||||
|  | ||||
|         return project; | ||||
|     } | ||||
|  | ||||
|     public async Task<DevProject?> GetProjectAsync(Guid id, Guid? developerId = null) | ||||
|     public async Task<SnDevProject?> GetProjectAsync(Guid id, Guid? developerId = null) | ||||
|     { | ||||
|         var query = db.DevProjects.AsQueryable(); | ||||
|          | ||||
|  | ||||
|         if (developerId.HasValue) | ||||
|         { | ||||
|             query = query.Where(p => p.DeveloperId == developerId.Value); | ||||
| @@ -41,14 +37,14 @@ public class DevProjectService( | ||||
|         return await query.FirstOrDefaultAsync(p => p.Id == id); | ||||
|     } | ||||
|  | ||||
|     public async Task<List<DevProject>> GetProjectsByDeveloperAsync(Guid developerId) | ||||
|     public async Task<List<SnDevProject>> GetProjectsByDeveloperAsync(Guid developerId) | ||||
|     { | ||||
|         return await db.DevProjects | ||||
|             .Where(p => p.DeveloperId == developerId) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task<DevProject?> UpdateProjectAsync( | ||||
|     public async Task<SnDevProject?> UpdateProjectAsync( | ||||
|         Guid id, | ||||
|         Guid developerId, | ||||
|         DevProjectController.DevProjectRequest request | ||||
| @@ -74,4 +70,4 @@ public class DevProjectService( | ||||
|         await db.SaveChangesAsync(); | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
| } | ||||
| @@ -5,7 +5,6 @@ | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "http://localhost:5156", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
| @@ -14,7 +13,6 @@ | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "https://localhost:7192;http://localhost:5156", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
|   | ||||
| @@ -1,9 +1,6 @@ | ||||
| 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; | ||||
|  | ||||
| @@ -11,12 +8,8 @@ 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); | ||||
| @@ -28,6 +21,7 @@ public static class ApplicationConfiguration | ||||
|         app.MapControllers(); | ||||
|          | ||||
|         app.MapGrpcService<CustomAppServiceGrpc>(); | ||||
|         app.MapGrpcReflectionService(); | ||||
|  | ||||
|         return app; | ||||
|     } | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| 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; | ||||
|  | ||||
| @@ -19,23 +18,21 @@ public static class ServiceCollectionExtensions | ||||
|         services.AddDbContext<AppDatabase>(); | ||||
|         services.AddSingleton<IClock>(SystemClock.Instance); | ||||
|         services.AddHttpContextAccessor(); | ||||
|         services.AddSingleton<IConnectionMultiplexer>(_ => | ||||
|         { | ||||
|             var connection = configuration.GetConnectionString("FastRetrieve")!; | ||||
|             return ConnectionMultiplexer.Connect(connection); | ||||
|         }); | ||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); | ||||
|  | ||||
|         services.AddHttpClient(); | ||||
|  | ||||
|         services.AddControllers().AddJsonOptions(options => | ||||
|         { | ||||
|             options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals; | ||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|              | ||||
|             options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||
|         }); | ||||
|  | ||||
|         services.AddGrpc(options => { options.EnableDetailedErrors = true; }); | ||||
|         services.AddGrpcReflection(); | ||||
|  | ||||
|         services.Configure<RequestLocalizationOptions>(options => | ||||
|         { | ||||
| @@ -59,23 +56,7 @@ public static class ServiceCollectionExtensions | ||||
|  | ||||
|     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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -10,22 +10,13 @@ | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60", | ||||
|     "FastRetrieve": "localhost:6379", | ||||
|     "Etcd": "etcd.orb.local:2379", | ||||
|     "Stream": "nats.orb.local:4222" | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_develop;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" | ||||
|   }, | ||||
|   "KnownProxies": ["127.0.0.1", "::1"], | ||||
|   "Swagger": { | ||||
|     "PublicBasePath": "/develop" | ||||
|   }, | ||||
|   "KnownProxies": [ | ||||
|     "127.0.0.1", | ||||
|     "::1" | ||||
|   ], | ||||
|   "Etcd": { | ||||
|     "Insecure": true | ||||
|   }, | ||||
|   "Service": { | ||||
|     "Name": "DysonNetwork.Develop", | ||||
|     "Url": "https://localhost:7192", | ||||
|     "ClientCert": "../Certificates/client.crt", | ||||
|     "ClientKey": "../Certificates/client.key" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| using System.Linq.Expressions; | ||||
| using System.Reflection; | ||||
| using DysonNetwork.Drive.Billing; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| using Microsoft.EntityFrameworkCore.Query; | ||||
| @@ -17,11 +16,11 @@ public class AppDatabase( | ||||
| ) : DbContext(options) | ||||
| { | ||||
|     public DbSet<FilePool> Pools { get; set; } = null!; | ||||
|     public DbSet<FileBundle> Bundles { get; set; } = null!; | ||||
|     public DbSet<SnFileBundle> Bundles { get; set; } = null!; | ||||
|      | ||||
|     public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!; | ||||
|      | ||||
|     public DbSet<CloudFile> Files { get; set; } = null!; | ||||
|     public DbSet<SnCloudFile> Files { get; set; } = null!; | ||||
|     public DbSet<CloudFileReference> FileReferences { get; set; } = null!; | ||||
|      | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
| @@ -31,7 +30,6 @@ public class AppDatabase( | ||||
|             opt => opt | ||||
|                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) | ||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) | ||||
|                 .UseNetTopologySuite() | ||||
|                 .UseNodaTime() | ||||
|         ).UseSnakeCaseNamingConvention(); | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Billing; | ||||
|   | ||||
| @@ -30,7 +30,7 @@ public class QuotaService( | ||||
|          | ||||
|         var (based, extra) = await GetQuotaVerbose(accountId); | ||||
|         var quota = based + extra; | ||||
|         await cache.SetAsync(cacheKey, quota); | ||||
|         await cache.SetAsync(cacheKey, quota, expiry: TimeSpan.FromMinutes(30)); | ||||
|         return quota; | ||||
|     } | ||||
|      | ||||
|   | ||||
| @@ -1,9 +0,0 @@ | ||||
| [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] | ||||
| charset = utf-8 | ||||
| indent_size = 2 | ||||
| indent_style = space | ||||
| insert_final_newline = true | ||||
| trim_trailing_whitespace = true | ||||
|  | ||||
| end_of_line = lf | ||||
| max_line_length = 100 | ||||
							
								
								
									
										1
									
								
								DysonNetwork.Drive/Client/.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								DysonNetwork.Drive/Client/.gitattributes
									
									
									
									
										vendored
									
									
								
							| @@ -1 +0,0 @@ | ||||
| * text=auto eol=lf | ||||
							
								
								
									
										31
									
								
								DysonNetwork.Drive/Client/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										31
									
								
								DysonNetwork.Drive/Client/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,31 +0,0 @@ | ||||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
|  | ||||
| node_modules | ||||
| **/node_modules/highlight.js/ | ||||
| .DS_Store | ||||
| dist | ||||
| dist-ssr | ||||
| coverage | ||||
| *.local | ||||
|  | ||||
| /cypress/videos/ | ||||
| /cypress/screenshots/ | ||||
|  | ||||
| # Editor directories and files | ||||
| .vscode/* | ||||
| !.vscode/extensions.json | ||||
| .idea | ||||
| *.suo | ||||
| *.ntvs* | ||||
| *.njsproj | ||||
| *.sln | ||||
| *.sw? | ||||
|  | ||||
| *.tsbuildinfo | ||||
| @@ -1,6 +0,0 @@ | ||||
| { | ||||
|   "$schema": "https://json.schemastore.org/prettierrc", | ||||
|   "semi": false, | ||||
|   "singleQuote": true, | ||||
|   "printWidth": 100 | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| { | ||||
|   "recommendations": [ | ||||
|     "Vue.volar", | ||||
|     "dbaeumer.vscode-eslint", | ||||
|     "EditorConfig.EditorConfig", | ||||
|     "oxc.oxc-vscode", | ||||
|     "esbenp.prettier-vscode" | ||||
|   ] | ||||
| } | ||||
| @@ -1,955 +0,0 @@ | ||||
| { | ||||
|   "lockfileVersion": 1, | ||||
|   "workspaces": { | ||||
|     "": { | ||||
|       "name": "@solar-network/pass", | ||||
|       "dependencies": { | ||||
|         "@fingerprintjs/fingerprintjs": "^4.6.2", | ||||
|         "@fontsource-variable/nunito": "^5.2.6", | ||||
|         "@hcaptcha/vue3-hcaptcha": "^1.3.0", | ||||
|         "@tailwindcss/vite": "^4.1.11", | ||||
|         "@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": { | ||||
|         "@tsconfig/node22": "^22.0.2", | ||||
|         "@types/node": "^22.16.4", | ||||
|         "@vicons/material": "^0.13.0", | ||||
|         "@vitejs/plugin-vue": "^6.0.0", | ||||
|         "@vitejs/plugin-vue-jsx": "^5.0.1", | ||||
|         "@vue/eslint-config-prettier": "^10.2.0", | ||||
|         "@vue/eslint-config-typescript": "^14.6.0", | ||||
|         "@vue/tsconfig": "^0.7.0", | ||||
|         "eslint": "^9.31.0", | ||||
|         "eslint-plugin-oxlint": "~1.1.0", | ||||
|         "eslint-plugin-vue": "~10.2.0", | ||||
|         "jiti": "^2.4.2", | ||||
|         "naive-ui": "^2.42.0", | ||||
|         "npm-run-all2": "^8.0.4", | ||||
|         "oxlint": "~1.1.0", | ||||
|         "prettier": "3.5.3", | ||||
|         "typescript": "~5.8.3", | ||||
|         "vite": "npm:rolldown-vite@latest", | ||||
|         "vite-plugin-vue-devtools": "^7.7.7", | ||||
|         "vue-tsc": "^2.2.12", | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   "packages": { | ||||
|     "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], | ||||
|  | ||||
|     "@antfu/utils": ["@antfu/utils@0.7.10", "", {}, "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww=="], | ||||
|  | ||||
|     "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], | ||||
|  | ||||
|     "@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="], | ||||
|  | ||||
|     "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], | ||||
|  | ||||
|     "@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="], | ||||
|  | ||||
|     "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], | ||||
|  | ||||
|     "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], | ||||
|  | ||||
|     "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A=="], | ||||
|  | ||||
|     "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], | ||||
|  | ||||
|     "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA=="], | ||||
|  | ||||
|     "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], | ||||
|  | ||||
|     "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="], | ||||
|  | ||||
|     "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], | ||||
|  | ||||
|     "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], | ||||
|  | ||||
|     "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], | ||||
|  | ||||
|     "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], | ||||
|  | ||||
|     "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], | ||||
|  | ||||
|     "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], | ||||
|  | ||||
|     "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], | ||||
|  | ||||
|     "@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="], | ||||
|  | ||||
|     "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], | ||||
|  | ||||
|     "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.28.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-decorators": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg=="], | ||||
|  | ||||
|     "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A=="], | ||||
|  | ||||
|     "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="], | ||||
|  | ||||
|     "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], | ||||
|  | ||||
|     "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], | ||||
|  | ||||
|     "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], | ||||
|  | ||||
|     "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg=="], | ||||
|  | ||||
|     "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], | ||||
|  | ||||
|     "@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], | ||||
|  | ||||
|     "@babel/types": ["@babel/types@7.28.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ=="], | ||||
|  | ||||
|     "@css-render/plugin-bem": ["@css-render/plugin-bem@0.15.14", "", { "peerDependencies": { "css-render": "~0.15.14" } }, "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg=="], | ||||
|  | ||||
|     "@css-render/vue3-ssr": ["@css-render/vue3-ssr@0.15.14", "", { "peerDependencies": { "vue": "^3.0.11" } }, "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g=="], | ||||
|  | ||||
|     "@emnapi/core": ["@emnapi/core@1.4.4", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" } }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="], | ||||
|  | ||||
|     "@emnapi/runtime": ["@emnapi/runtime@1.4.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="], | ||||
|  | ||||
|     "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw=="], | ||||
|  | ||||
|     "@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="], | ||||
|  | ||||
|     "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], | ||||
|  | ||||
|     "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], | ||||
|  | ||||
|     "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], | ||||
|  | ||||
|     "@eslint/config-helpers": ["@eslint/config-helpers@0.3.0", "", {}, "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="], | ||||
|  | ||||
|     "@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="], | ||||
|  | ||||
|     "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], | ||||
|  | ||||
|     "@eslint/js": ["@eslint/js@9.31.0", "", {}, "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw=="], | ||||
|  | ||||
|     "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], | ||||
|  | ||||
|     "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.3", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag=="], | ||||
|  | ||||
|     "@fingerprintjs/fingerprintjs": ["@fingerprintjs/fingerprintjs@4.6.2", "", { "dependencies": { "tslib": "^2.4.1" } }, "sha512-g8mXuqcFKbgH2CZKwPfVtsUJDHyvcgIABQI7Y0tzWEFXpGxJaXuAuzlifT2oTakjDBLTK4Gaa9/5PERDhqUjtw=="], | ||||
|  | ||||
|     "@fontsource-variable/nunito": ["@fontsource-variable/nunito@5.2.6", "", {}, "sha512-dGYTQ0Hl94jjfMraYefrURHGH8fk/vL/1zYAZGofiPJVs6C0OkM8T87Te5Gwrbe6HG/XEMm5lib8AqasTN3ucw=="], | ||||
|  | ||||
|     "@hcaptcha/vue3-hcaptcha": ["@hcaptcha/vue3-hcaptcha@1.3.0", "", { "dependencies": { "vue": "^3.2.19" } }, "sha512-IEonS6JiYdU7uy6aeib8cYtMO4nj8utwStbA9bWHyYbOvOvhpkV+AW8vfSKh6SntYxqle/TRwhv+kU9p92CfsA=="], | ||||
|  | ||||
|     "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], | ||||
|  | ||||
|     "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], | ||||
|  | ||||
|     "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], | ||||
|  | ||||
|     "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], | ||||
|  | ||||
|     "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], | ||||
|  | ||||
|     "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="], | ||||
|  | ||||
|     "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], | ||||
|  | ||||
|     "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="], | ||||
|  | ||||
|     "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], | ||||
|  | ||||
|     "@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=="], | ||||
|  | ||||
|     "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], | ||||
|  | ||||
|     "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], | ||||
|  | ||||
|     "@oxc-project/runtime": ["@oxc-project/runtime@0.77.0", "", {}, "sha512-cMbHs/DaomWSjxeJ79G10GA5hzJW9A7CZ+/cO+KuPZ7Trf3Rr07qSLauC4Ns8ba4DKVDjd8VSC9nVLpw6jpoGQ=="], | ||||
|  | ||||
|     "@oxc-project/types": ["@oxc-project/types@0.77.0", "", {}, "sha512-iUQj185VvCPnSba+ltUV5tVDrPX6LeZVtQywnnoGbe4oJ1VKvDKisjGkD/AvVtdm98b/BdsVS35IlJV1m2mBBA=="], | ||||
|  | ||||
|     "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sSnR3SOxIU/QfaqXrcQ0UVUkzJO0bcInQ7dMhHa102gVAgWjp1fBeMVCM0adEY0UNmEXrRkgD/rQtQgn9YAU+w=="], | ||||
|  | ||||
|     "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jvd3fHnzY2OYbmsg9NSGPoBkGViDGHSFnBKyJQ9LOIw7lxAyQBG2Quxc3GYPFR/f9OYho9C3p4+dIaAJfKhnsw=="], | ||||
|  | ||||
|     "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MgW4iskOdXuoR+wDXIJUfbdnTg2eo2FnQRaD6ZqhnDTDa7LnV+06rp/Cg3aGj2X9jSEcKDv/bMbYQuot7WRs6Q=="], | ||||
|  | ||||
|     "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-a+pkEKmDRdrW+y0gtZ/m68ElVW2VZgATGbMxDgDYFpdiMx9Y0pUPwTMZ2EX/17Aslop4c1BiDSFDK7aEBxKR2g=="], | ||||
|  | ||||
|     "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wNBsXCKVZMvUTcFitrV1wTsdhUAv8l+XQxHxciZ2SO6dpNnWEb2YCxSAIOXeyzBLdO4pIODYcSy38CvGue7TwA=="], | ||||
|  | ||||
|     "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pZD0lt6A5j2Wp70fgIYk4GoPfKTZ8mHWamWIpKFT7aSkFkiOi6nhLWDFvMEIHWRTK3LgkWUNcnWPp4brvin4wQ=="], | ||||
|  | ||||
|     "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.1.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-rT6uXQvE80+B+L04HJf30uF26426FPI9i9DAY2AxBUhrpNwhqkDEhQdd9ilFWVC7SSbpHgAs50lo+ImSAAkHPQ=="], | ||||
|  | ||||
|     "@oxlint/win32-x64": ["@oxlint/win32-x64@1.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-x6r5yvM3wEty93Bx0NuNK+kutUyS/K55itkUrxdExoK6GcmVDboGGuhju9HyU2cM/IWLEWO8RHcXSyaxr9GR5g=="], | ||||
|  | ||||
|     "@pkgr/core": ["@pkgr/core@0.2.7", "", {}, "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg=="], | ||||
|  | ||||
|     "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], | ||||
|  | ||||
|     "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.27", "", { "os": "android", "cpu": "arm64" }, "sha512-IJL3efUJmvb5MfTEi7bGK4jq3ZFAzVbSy+vmul0DcdrglUd81Tfyy7Zzq2oM0tUgmACG32d8Jz/ykbpbf+3C5A=="], | ||||
|  | ||||
|     "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.27", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TXTiuHbtnHfb0c44vNfWfIyEFJ0BFUf63ip9Z4mj8T2zRcZXQYVger4OuAxnwGNGBgDyHo1VaNBG+Vxn2VrpqQ=="], | ||||
|  | ||||
|     "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.27", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jpjflgvbolh+fAaaEajPJQCOpZMawYMbNVzuZp3nidX1B7kMAP7NEKp9CWzthoL2Y8RfD7OApN6bx4+vFurTaw=="], | ||||
|  | ||||
|     "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.27", "", { "os": "freebsd", "cpu": "x64" }, "sha512-07ZNlXIunyS1jCTnene7aokkzCZNBUnmnJWu4Nz5X5XQvVHJNjsDhPFJTlNmneSDzA3vGkRNwdECKXiDTH/CqA=="], | ||||
|  | ||||
|     "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.27", "", { "os": "linux", "cpu": "arm" }, "sha512-z74ah00oyKnTUtaIbg34TaIU1PYM8tGE1bK6aUs8OLZ9sWW4g3Xo5A0nit2zyeanmYFvrAUxnt3Bpk+mTZCtlg=="], | ||||
|  | ||||
|     "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.27", "", { "os": "linux", "cpu": "arm64" }, "sha512-b9oKl/M5OIyAcosS73BmjOZOjvcONV97t2SnKpgwfDX/mjQO3dBgTYyvHMFA6hfhIDW1+2XVQR/k5uzBULFhoA=="], | ||||
|  | ||||
|     "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.27", "", { "os": "linux", "cpu": "arm64" }, "sha512-RmaNSkVmAH8u/r5Q+v4O0zL4HY8pLrvlM5wBoBrb/QHDQgksGKBqhecpg1ERER0Q7gMh/GJUz6JiiD55Q+9UOA=="], | ||||
|  | ||||
|     "@rolldown/binding-linux-arm64-ohos": ["@rolldown/binding-linux-arm64-ohos@1.0.0-beta.27", "", { "os": "none", "cpu": "arm64" }, "sha512-gq78fI/g0cp1UKFMk53kP/oZAgYOXbaqdadVMuCJc0CoSkDJcpO2YIasRs/QYlE91QWfcHD5RZl9zbf4ksTS/w=="], | ||||
|  | ||||
|     "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.27", "", { "os": "linux", "cpu": "x64" }, "sha512-yS/GreJ6BT44dHu1WLigc50S8jZA+pDzzsf8tqRptUTwi5YW7dX3NqcDlc/lXsZqu57aKynLljgClYAm90LEKw=="], | ||||
|  | ||||
|     "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.27", "", { "os": "linux", "cpu": "x64" }, "sha512-6FV9To1sXewGHY4NaCPeOE5p5o1qfuAjj+m75WVIPw9HEJVsQoC5QiTL5wWVNqSMch4X0eWnQ6WsQolU6sGMIA=="], | ||||
|  | ||||
|     "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.27", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.12" }, "cpu": "none" }, "sha512-VcxdhF0PQda9krFJHw4DqUkdAsHWYs/Uz/Kr/zhU8zMFDzmK6OdUgl9emGj9wTzXAEHYkAMDhk+OJBRJvp424g=="], | ||||
|  | ||||
|     "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.27", "", { "os": "win32", "cpu": "arm64" }, "sha512-3bXSARqSf8jLHrQ1/tw9pX1GwIR9jA6OEsqTgdC0DdpoZ+34sbJXE9Nse3dQ0foGLKBkh4PqDv/rm2Thu9oVBw=="], | ||||
|  | ||||
|     "@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.27", "", { "os": "win32", "cpu": "ia32" }, "sha512-xPGcKb+W8NIWAf5KApsUIrhiKH5NImTarICge5jQ2m0BBxD31crio4OXy/eYVq5CZkqkqszLQz2fWZcWNmbzlQ=="], | ||||
|  | ||||
|     "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.27", "", { "os": "win32", "cpu": "x64" }, "sha512-3y1G8ARpXBAcz4RJM5nzMU6isS/gXZl8SuX8lS2piFOnQMiOp6ajeelnciD+EgG4ej793zvNvr+WZtdnao2yrw=="], | ||||
|  | ||||
|     "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.19", "", {}, "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA=="], | ||||
|  | ||||
|     "@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="], | ||||
|  | ||||
|     "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], | ||||
|  | ||||
|     "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], | ||||
|  | ||||
|     "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="], | ||||
|  | ||||
|     "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="], | ||||
|  | ||||
|     "@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="], | ||||
|  | ||||
|     "@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="], | ||||
|  | ||||
|     "@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], | ||||
|  | ||||
|     "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], | ||||
|  | ||||
|     "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], | ||||
|  | ||||
|     "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="], | ||||
|  | ||||
|     "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], | ||||
|  | ||||
|     "@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="], | ||||
|  | ||||
|     "@types/node": ["@types/node@22.16.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g=="], | ||||
|  | ||||
|     "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="], | ||||
|  | ||||
|     "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.37.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/type-utils": "8.37.0", "@typescript-eslint/utils": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA=="], | ||||
|  | ||||
|     "@typescript-eslint/parser": ["@typescript-eslint/parser@8.37.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA=="], | ||||
|  | ||||
|     "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.37.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.37.0", "@typescript-eslint/types": "^8.37.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA=="], | ||||
|  | ||||
|     "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0" } }, "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA=="], | ||||
|  | ||||
|     "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.37.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg=="], | ||||
|  | ||||
|     "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/utils": "8.37.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow=="], | ||||
|  | ||||
|     "@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="], | ||||
|  | ||||
|     "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.37.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.37.0", "@typescript-eslint/tsconfig-utils": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg=="], | ||||
|  | ||||
|     "@typescript-eslint/utils": ["@typescript-eslint/utils@8.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A=="], | ||||
|  | ||||
|     "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w=="], | ||||
|  | ||||
|     "@vicons/material": ["@vicons/material@0.13.0", "", {}, "sha512-lKVxFNprM+CaBkUH3gt6VjIeiMsKQl2zARQMwTCZruQl2vRHzyeZiKeCflWS99CEfv2JzX/6y697smxlzyxcVw=="], | ||||
|  | ||||
|     "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.19" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "sha512-iAliE72WsdhjzTOp2DtvKThq1VBC4REhwRcaA+zPAAph6I+OQhUXv+Xu2KS7ElxYtb7Zc/3R30Hwv1DxEo7NXQ=="], | ||||
|  | ||||
|     "@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@5.0.1", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/plugin-transform-typescript": "^7.27.1", "@rolldown/pluginutils": "^1.0.0-beta.21", "@vue/babel-plugin-jsx": "^1.4.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.0.0" } }, "sha512-X7qmQMXbdDh+sfHUttXokPD0cjPkMFoae7SgbkF9vi3idGUKmxLcnU2Ug49FHwiKXebfzQRIm5yK3sfCJzNBbg=="], | ||||
|  | ||||
|     "@volar/language-core": ["@volar/language-core@2.4.15", "", { "dependencies": { "@volar/source-map": "2.4.15" } }, "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA=="], | ||||
|  | ||||
|     "@volar/source-map": ["@volar/source-map@2.4.15", "", {}, "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg=="], | ||||
|  | ||||
|     "@volar/typescript": ["@volar/typescript@2.4.15", "", { "dependencies": { "@volar/language-core": "2.4.15", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg=="], | ||||
|  | ||||
|     "@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.4.0", "", {}, "sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw=="], | ||||
|  | ||||
|     "@vue/babel-plugin-jsx": ["@vue/babel-plugin-jsx@1.4.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.9", "@babel/types": "^7.26.9", "@vue/babel-helper-vue-transform-on": "1.4.0", "@vue/babel-plugin-resolve-type": "1.4.0", "@vue/shared": "^3.5.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" }, "optionalPeers": ["@babel/core"] }, "sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA=="], | ||||
|  | ||||
|     "@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@1.4.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/parser": "^7.26.9", "@vue/compiler-sfc": "^3.5.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ=="], | ||||
|  | ||||
|     "@vue/compiler-core": ["@vue/compiler-core@3.5.17", "", { "dependencies": { "@babel/parser": "^7.27.5", "@vue/shared": "3.5.17", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA=="], | ||||
|  | ||||
|     "@vue/compiler-dom": ["@vue/compiler-dom@3.5.17", "", { "dependencies": { "@vue/compiler-core": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ=="], | ||||
|  | ||||
|     "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.17", "", { "dependencies": { "@babel/parser": "^7.27.5", "@vue/compiler-core": "3.5.17", "@vue/compiler-dom": "3.5.17", "@vue/compiler-ssr": "3.5.17", "@vue/shared": "3.5.17", "estree-walker": "^2.0.2", "magic-string": "^0.30.17", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww=="], | ||||
|  | ||||
|     "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.17", "", { "dependencies": { "@vue/compiler-dom": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ=="], | ||||
|  | ||||
|     "@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="], | ||||
|  | ||||
|     "@vue/devtools-api": ["@vue/devtools-api@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7" } }, "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg=="], | ||||
|  | ||||
|     "@vue/devtools-core": ["@vue/devtools-core@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7", "@vue/devtools-shared": "^7.7.7", "mitt": "^3.0.1", "nanoid": "^5.1.0", "pathe": "^2.0.3", "vite-hot-client": "^2.0.4" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ=="], | ||||
|  | ||||
|     "@vue/devtools-kit": ["@vue/devtools-kit@7.7.7", "", { "dependencies": { "@vue/devtools-shared": "^7.7.7", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA=="], | ||||
|  | ||||
|     "@vue/devtools-shared": ["@vue/devtools-shared@7.7.7", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw=="], | ||||
|  | ||||
|     "@vue/eslint-config-prettier": ["@vue/eslint-config-prettier@10.2.0", "", { "dependencies": { "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2" }, "peerDependencies": { "eslint": ">= 8.21.0", "prettier": ">= 3.0.0" } }, "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw=="], | ||||
|  | ||||
|     "@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.6.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.35.1", "fast-glob": "^3.3.3", "typescript-eslint": "^8.35.1", "vue-eslint-parser": "^10.2.0" }, "peerDependencies": { "eslint": "^9.10.0", "eslint-plugin-vue": "^9.28.0 || ^10.0.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-UpiRY/7go4Yps4mYCjkvlIbVWmn9YvPGQDxTAlcKLphyaD77LjIu3plH4Y9zNT0GB4f3K5tMmhhtRhPOgrQ/bQ=="], | ||||
|  | ||||
|     "@vue/language-core": ["@vue/language-core@2.2.12", "", { "dependencies": { "@volar/language-core": "2.4.15", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA=="], | ||||
|  | ||||
|     "@vue/reactivity": ["@vue/reactivity@3.5.17", "", { "dependencies": { "@vue/shared": "3.5.17" } }, "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw=="], | ||||
|  | ||||
|     "@vue/runtime-core": ["@vue/runtime-core@3.5.17", "", { "dependencies": { "@vue/reactivity": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q=="], | ||||
|  | ||||
|     "@vue/runtime-dom": ["@vue/runtime-dom@3.5.17", "", { "dependencies": { "@vue/reactivity": "3.5.17", "@vue/runtime-core": "3.5.17", "@vue/shared": "3.5.17", "csstype": "^3.1.3" } }, "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g=="], | ||||
|  | ||||
|     "@vue/server-renderer": ["@vue/server-renderer@3.5.17", "", { "dependencies": { "@vue/compiler-ssr": "3.5.17", "@vue/shared": "3.5.17" }, "peerDependencies": { "vue": "3.5.17" } }, "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA=="], | ||||
|  | ||||
|     "@vue/shared": ["@vue/shared@3.5.17", "", {}, "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg=="], | ||||
|  | ||||
|     "@vue/tsconfig": ["@vue/tsconfig@0.7.0", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg=="], | ||||
|  | ||||
|     "@vueuse/core": ["@vueuse/core@13.5.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.5.0", "@vueuse/shared": "13.5.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g=="], | ||||
|  | ||||
|     "@vueuse/metadata": ["@vueuse/metadata@13.5.0", "", {}, "sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw=="], | ||||
|  | ||||
|     "@vueuse/shared": ["@vueuse/shared@13.5.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g=="], | ||||
|  | ||||
|     "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], | ||||
|  | ||||
|     "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], | ||||
|  | ||||
|     "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], | ||||
|  | ||||
|     "alien-signals": ["alien-signals@1.0.13", "", {}, "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg=="], | ||||
|  | ||||
|     "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], | ||||
|  | ||||
|     "ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="], | ||||
|  | ||||
|     "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], | ||||
|  | ||||
|     "aspnet-prerendering": ["aspnet-prerendering@3.0.1", "", { "dependencies": { "domain-task": "^3.0.0" } }, "sha512-nfOQYVKW3sYQMZBXNM2KPrXU2MOBuLn/gszRZM0Y1Pj4EpzCw1KjXiO681eQo4ZR1TLLzJ8L2sQbq0qeC1zxVg=="], | ||||
|  | ||||
|     "async-validator": ["async-validator@4.2.5", "", {}, "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="], | ||||
|  | ||||
|     "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], | ||||
|  | ||||
|     "birpc": ["birpc@2.5.0", "", {}, "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ=="], | ||||
|  | ||||
|     "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], | ||||
|  | ||||
|     "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], | ||||
|  | ||||
|     "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], | ||||
|  | ||||
|     "browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="], | ||||
|  | ||||
|     "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], | ||||
|  | ||||
|     "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], | ||||
|  | ||||
|     "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], | ||||
|  | ||||
|     "caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="], | ||||
|  | ||||
|     "cfturnstile-vue3": ["cfturnstile-vue3@2.0.0", "", { "dependencies": { "vue": "^3.2.38" } }, "sha512-wamRC8ZoUAjvfOVoPAbJM14qqxc0gfjqfV6ESZh4rMs7G0yp+R4dpHNjxa7YAjdFTutaviMEZYCuK9tM4ZaGJQ=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], | ||||
|  | ||||
|     "combine-errors": ["combine-errors@3.0.3", "", { "dependencies": { "custom-error-instance": "2.1.1", "lodash.uniqby": "4.5.0" } }, "sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q=="], | ||||
|  | ||||
|     "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], | ||||
|  | ||||
|     "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], | ||||
|  | ||||
|     "copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="], | ||||
|  | ||||
|     "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], | ||||
|  | ||||
|     "css-render": ["css-render@0.15.14", "", { "dependencies": { "@emotion/hash": "~0.8.0", "csstype": "~3.0.5" } }, "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg=="], | ||||
|  | ||||
|     "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], | ||||
|  | ||||
|     "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], | ||||
|  | ||||
|     "custom-error-instance": ["custom-error-instance@2.1.1", "", {}, "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg=="], | ||||
|  | ||||
|     "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], | ||||
|  | ||||
|     "date-fns-tz": ["date-fns-tz@3.2.0", "", { "peerDependencies": { "date-fns": "^3.0.0 || ^4.0.0" } }, "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ=="], | ||||
|  | ||||
|     "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], | ||||
|  | ||||
|     "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], | ||||
|  | ||||
|     "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], | ||||
|  | ||||
|     "default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="], | ||||
|  | ||||
|     "default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="], | ||||
|  | ||||
|     "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], | ||||
|  | ||||
|     "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], | ||||
|  | ||||
|     "domain-context": ["domain-context@0.5.1", "", {}, "sha512-WyTWkXciNvYYaQzdnKJtjlVSXHivtt0E/vCv36Bkwh+Sk4NXkrQpHxZT5BHYmKRVgxWMol1wcdurZCzyTT6Euw=="], | ||||
|  | ||||
|     "domain-task": ["domain-task@3.0.3", "", { "dependencies": { "domain-context": "^0.5.1", "is-absolute-url": "^2.1.0", "isomorphic-fetch": "^2.2.1" } }, "sha512-7oAiY1AvjhVNVJbOwSHbrm6lEHczOSSCSqDkHp2ZO7vb/iOCGl7YNk/1cv4yKwSGhBMpBZ5mu+7cMorbWxWvOg=="], | ||||
|  | ||||
|     "electron-to-chromium": ["electron-to-chromium@1.5.183", "", {}, "sha512-vCrDBYjQCAEefWGjlK3EpoSKfKbT10pR4XXPdn65q7snuNOZnthoVpBfZPykmDapOKfoD+MMIPG8ZjKyyc9oHA=="], | ||||
|  | ||||
|     "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], | ||||
|  | ||||
|     "enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="], | ||||
|  | ||||
|     "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], | ||||
|  | ||||
|     "error-stack-parser-es": ["error-stack-parser-es@0.1.5", "", {}, "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg=="], | ||||
|  | ||||
|     "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], | ||||
|  | ||||
|     "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], | ||||
|  | ||||
|     "eslint": ["eslint@9.31.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ=="], | ||||
|  | ||||
|     "eslint-config-prettier": ["eslint-config-prettier@10.1.5", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw=="], | ||||
|  | ||||
|     "eslint-plugin-oxlint": ["eslint-plugin-oxlint@1.1.0", "", { "dependencies": { "jsonc-parser": "^3.3.1" } }, "sha512-spDWxcsAfoUDjSwxPrP2gfuOJ2Hrv8faqQ5Vkm90lURp4no5aWJQ09xRKmZroIPTuQCKYgG9nvnakdIbXGlijg=="], | ||||
|  | ||||
|     "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.1", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw=="], | ||||
|  | ||||
|     "eslint-plugin-vue": ["eslint-plugin-vue@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.15", "semver": "^7.6.3", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "vue-eslint-parser": "^10.0.0" } }, "sha512-tl9s+KN3z0hN2b8fV2xSs5ytGl7Esk1oSCxULLwFcdaElhZ8btYYZFrWxvh4En+czrSDtuLCeCOGa8HhEZuBdQ=="], | ||||
|  | ||||
|     "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], | ||||
|  | ||||
|     "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], | ||||
|  | ||||
|     "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], | ||||
|  | ||||
|     "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], | ||||
|  | ||||
|     "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], | ||||
|  | ||||
|     "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], | ||||
|  | ||||
|     "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], | ||||
|  | ||||
|     "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], | ||||
|  | ||||
|     "evtd": ["evtd@0.2.4", "", {}, "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw=="], | ||||
|  | ||||
|     "execa": ["execa@9.6.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw=="], | ||||
|  | ||||
|     "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], | ||||
|  | ||||
|     "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], | ||||
|  | ||||
|     "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], | ||||
|  | ||||
|     "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], | ||||
|  | ||||
|     "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], | ||||
|  | ||||
|     "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], | ||||
|  | ||||
|     "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], | ||||
|  | ||||
|     "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], | ||||
|  | ||||
|     "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], | ||||
|  | ||||
|     "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], | ||||
|  | ||||
|     "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], | ||||
|  | ||||
|     "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], | ||||
|  | ||||
|     "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], | ||||
|  | ||||
|     "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], | ||||
|  | ||||
|     "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | ||||
|  | ||||
|     "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], | ||||
|  | ||||
|     "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], | ||||
|  | ||||
|     "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], | ||||
|  | ||||
|     "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], | ||||
|  | ||||
|     "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], | ||||
|  | ||||
|     "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], | ||||
|  | ||||
|     "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], | ||||
|  | ||||
|     "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], | ||||
|  | ||||
|     "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], | ||||
|  | ||||
|     "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], | ||||
|  | ||||
|     "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], | ||||
|  | ||||
|     "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], | ||||
|  | ||||
|     "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], | ||||
|  | ||||
|     "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], | ||||
|  | ||||
|     "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], | ||||
|  | ||||
|     "is-absolute-url": ["is-absolute-url@2.1.0", "", {}, "sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg=="], | ||||
|  | ||||
|     "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], | ||||
|  | ||||
|     "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], | ||||
|  | ||||
|     "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], | ||||
|  | ||||
|     "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], | ||||
|  | ||||
|     "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], | ||||
|  | ||||
|     "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], | ||||
|  | ||||
|     "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], | ||||
|  | ||||
|     "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], | ||||
|  | ||||
|     "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], | ||||
|  | ||||
|     "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], | ||||
|  | ||||
|     "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], | ||||
|  | ||||
|     "isomorphic-fetch": ["isomorphic-fetch@2.2.1", "", { "dependencies": { "node-fetch": "^1.0.1", "whatwg-fetch": ">=0.10.0" } }, "sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA=="], | ||||
|  | ||||
|     "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], | ||||
|  | ||||
|     "js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="], | ||||
|  | ||||
|     "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], | ||||
|  | ||||
|     "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], | ||||
|  | ||||
|     "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], | ||||
|  | ||||
|     "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], | ||||
|  | ||||
|     "json-parse-even-better-errors": ["json-parse-even-better-errors@4.0.0", "", {}, "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA=="], | ||||
|  | ||||
|     "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], | ||||
|  | ||||
|     "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], | ||||
|  | ||||
|     "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], | ||||
|  | ||||
|     "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], | ||||
|  | ||||
|     "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], | ||||
|  | ||||
|     "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], | ||||
|  | ||||
|     "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], | ||||
|  | ||||
|     "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], | ||||
|  | ||||
|     "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], | ||||
|  | ||||
|     "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], | ||||
|  | ||||
|     "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], | ||||
|  | ||||
|     "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], | ||||
|  | ||||
|     "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], | ||||
|  | ||||
|     "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], | ||||
|  | ||||
|     "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], | ||||
|  | ||||
|     "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], | ||||
|  | ||||
|     "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], | ||||
|  | ||||
|     "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], | ||||
|  | ||||
|     "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], | ||||
|  | ||||
|     "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], | ||||
|  | ||||
|     "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], | ||||
|  | ||||
|     "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], | ||||
|  | ||||
|     "lodash._baseiteratee": ["lodash._baseiteratee@4.7.0", "", { "dependencies": { "lodash._stringtopath": "~4.8.0" } }, "sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ=="], | ||||
|  | ||||
|     "lodash._basetostring": ["lodash._basetostring@4.12.0", "", {}, "sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw=="], | ||||
|  | ||||
|     "lodash._baseuniq": ["lodash._baseuniq@4.6.0", "", { "dependencies": { "lodash._createset": "~4.0.0", "lodash._root": "~3.0.0" } }, "sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A=="], | ||||
|  | ||||
|     "lodash._createset": ["lodash._createset@4.0.3", "", {}, "sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA=="], | ||||
|  | ||||
|     "lodash._root": ["lodash._root@3.0.1", "", {}, "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ=="], | ||||
|  | ||||
|     "lodash._stringtopath": ["lodash._stringtopath@4.8.0", "", { "dependencies": { "lodash._basetostring": "~4.12.0" } }, "sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ=="], | ||||
|  | ||||
|     "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], | ||||
|  | ||||
|     "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="], | ||||
|  | ||||
|     "lodash.uniqby": ["lodash.uniqby@4.5.0", "", { "dependencies": { "lodash._baseiteratee": "~4.7.0", "lodash._baseuniq": "~4.6.0" } }, "sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ=="], | ||||
|  | ||||
|     "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], | ||||
|  | ||||
|     "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], | ||||
|  | ||||
|     "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="], | ||||
|  | ||||
|     "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], | ||||
|  | ||||
|     "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], | ||||
|  | ||||
|     "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], | ||||
|  | ||||
|     "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], | ||||
|  | ||||
|     "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], | ||||
|  | ||||
|     "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], | ||||
|  | ||||
|     "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], | ||||
|  | ||||
|     "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], | ||||
|  | ||||
|     "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], | ||||
|  | ||||
|     "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], | ||||
|  | ||||
|     "naive-ui": ["naive-ui@2.42.0", "", { "dependencies": { "@css-render/plugin-bem": "^0.15.14", "@css-render/vue3-ssr": "^0.15.14", "@types/katex": "^0.16.2", "@types/lodash": "^4.14.198", "@types/lodash-es": "^4.17.9", "async-validator": "^4.2.5", "css-render": "^0.15.14", "csstype": "^3.1.3", "date-fns": "^3.6.0", "date-fns-tz": "^3.1.3", "evtd": "^0.2.4", "highlight.js": "^11.8.0", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "seemly": "^0.3.8", "treemate": "^0.3.11", "vdirs": "^0.1.8", "vooks": "^0.2.12", "vueuc": "^0.4.63" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-c7cXR2YgOjgtBadXHwiWL4Y0tpGLAI5W5QzzHksOi22iuHXoSGMAzdkVTGVPE/PM0MSGQ/JtUIzCx2Y0hU0vTQ=="], | ||||
|  | ||||
|     "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], | ||||
|  | ||||
|     "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], | ||||
|  | ||||
|     "node-fetch": ["node-fetch@1.7.3", "", { "dependencies": { "encoding": "^0.1.11", "is-stream": "^1.0.1" } }, "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ=="], | ||||
|  | ||||
|     "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], | ||||
|  | ||||
|     "npm-normalize-package-bin": ["npm-normalize-package-bin@4.0.0", "", {}, "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w=="], | ||||
|  | ||||
|     "npm-run-all2": ["npm-run-all2@8.0.4", "", { "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "picomatch": "^4.0.2", "pidtree": "^0.6.0", "read-package-json-fast": "^4.0.0", "shell-quote": "^1.7.3", "which": "^5.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA=="], | ||||
|  | ||||
|     "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], | ||||
|  | ||||
|     "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], | ||||
|  | ||||
|     "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], | ||||
|  | ||||
|     "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], | ||||
|  | ||||
|     "oxlint": ["oxlint@1.1.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.1.0", "@oxlint/darwin-x64": "1.1.0", "@oxlint/linux-arm64-gnu": "1.1.0", "@oxlint/linux-arm64-musl": "1.1.0", "@oxlint/linux-x64-gnu": "1.1.0", "@oxlint/linux-x64-musl": "1.1.0", "@oxlint/win32-arm64": "1.1.0", "@oxlint/win32-x64": "1.1.0" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-OVNpaoaQCUHHhCv5sYMPJ7Ts5k7ziw0QteH1gBSwF3elf/8GAew2Uh/0S7HsU1iGtjhlFy80+A8nwIb3Tq6m1w=="], | ||||
|  | ||||
|     "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], | ||||
|  | ||||
|     "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], | ||||
|  | ||||
|     "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], | ||||
|  | ||||
|     "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], | ||||
|  | ||||
|     "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], | ||||
|  | ||||
|     "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], | ||||
|  | ||||
|     "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], | ||||
|  | ||||
|     "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], | ||||
|  | ||||
|     "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], | ||||
|  | ||||
|     "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], | ||||
|  | ||||
|     "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], | ||||
|  | ||||
|     "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], | ||||
|  | ||||
|     "pinia": ["pinia@3.0.3", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA=="], | ||||
|  | ||||
|     "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], | ||||
|  | ||||
|     "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], | ||||
|  | ||||
|     "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], | ||||
|  | ||||
|     "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], | ||||
|  | ||||
|     "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], | ||||
|  | ||||
|     "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], | ||||
|  | ||||
|     "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], | ||||
|  | ||||
|     "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], | ||||
|  | ||||
|     "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], | ||||
|  | ||||
|     "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], | ||||
|  | ||||
|     "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="], | ||||
|  | ||||
|     "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], | ||||
|  | ||||
|     "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], | ||||
|  | ||||
|     "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], | ||||
|  | ||||
|     "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], | ||||
|  | ||||
|     "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], | ||||
|  | ||||
|     "rolldown": ["rolldown@1.0.0-beta.27", "", { "dependencies": { "@oxc-project/runtime": "=0.77.0", "@oxc-project/types": "=0.77.0", "@rolldown/pluginutils": "1.0.0-beta.27", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.27", "@rolldown/binding-darwin-arm64": "1.0.0-beta.27", "@rolldown/binding-darwin-x64": "1.0.0-beta.27", "@rolldown/binding-freebsd-x64": "1.0.0-beta.27", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.27", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.27", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.27", "@rolldown/binding-linux-arm64-ohos": "1.0.0-beta.27", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.27", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.27", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.27", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.27", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.27", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.27" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-aYiJmzKoUHoaaEZLRegYVfZkXW7gzdgSbq+u5cXQ6iXc/y8tnQ3zGffQo44Pr1lTKeLluw3bDIDUCx/NAzqKeA=="], | ||||
|  | ||||
|     "run-applescript": ["run-applescript@7.0.0", "", {}, "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="], | ||||
|  | ||||
|     "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], | ||||
|  | ||||
|     "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], | ||||
|  | ||||
|     "seemly": ["seemly@0.3.10", "", {}, "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q=="], | ||||
|  | ||||
|     "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], | ||||
|  | ||||
|     "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], | ||||
|  | ||||
|     "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], | ||||
|  | ||||
|     "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], | ||||
|  | ||||
|     "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], | ||||
|  | ||||
|     "sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="], | ||||
|  | ||||
|     "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], | ||||
|  | ||||
|     "speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="], | ||||
|  | ||||
|     "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], | ||||
|  | ||||
|     "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], | ||||
|  | ||||
|     "superjson": ["superjson@2.2.2", "", { "dependencies": { "copy-anything": "^3.0.2" } }, "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q=="], | ||||
|  | ||||
|     "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], | ||||
|  | ||||
|     "synckit": ["synckit@0.11.8", "", { "dependencies": { "@pkgr/core": "^0.2.4" } }, "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A=="], | ||||
|  | ||||
|     "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], | ||||
|  | ||||
|     "tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="], | ||||
|  | ||||
|     "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], | ||||
|  | ||||
|     "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], | ||||
|  | ||||
|     "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], | ||||
|  | ||||
|     "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], | ||||
|  | ||||
|     "treemate": ["treemate@0.3.11", "", {}, "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg=="], | ||||
|  | ||||
|     "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], | ||||
|  | ||||
|     "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||
|  | ||||
|     "tus-js-client": ["tus-js-client@4.3.1", "", { "dependencies": { "buffer-from": "^1.1.2", "combine-errors": "^3.0.3", "is-stream": "^2.0.0", "js-base64": "^3.7.2", "lodash.throttle": "^4.1.1", "proper-lockfile": "^4.1.2", "url-parse": "^1.5.7" } }, "sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg=="], | ||||
|  | ||||
|     "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], | ||||
|  | ||||
|     "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], | ||||
|  | ||||
|     "typescript-eslint": ["typescript-eslint@8.37.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.37.0", "@typescript-eslint/parser": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/utils": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA=="], | ||||
|  | ||||
|     "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], | ||||
|  | ||||
|     "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], | ||||
|  | ||||
|     "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], | ||||
|  | ||||
|     "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], | ||||
|  | ||||
|     "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], | ||||
|  | ||||
|     "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], | ||||
|  | ||||
|     "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], | ||||
|  | ||||
|     "vdirs": ["vdirs@0.1.8", "", { "dependencies": { "evtd": "^0.2.2" }, "peerDependencies": { "vue": "^3.0.11" } }, "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw=="], | ||||
|  | ||||
|     "vite": ["rolldown-vite@7.0.9", "", { "dependencies": { "fdir": "^6.4.6", "lightningcss": "^1.30.1", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.27", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-RxVP6CY9CNCEM9UecdytqeADxOGSjgkfSE/eI986sM7I3/F09lQ9UfQo3y6W10ICBppKsEHe71NbCX/tirYDFg=="], | ||||
|  | ||||
|     "vite-hot-client": ["vite-hot-client@2.1.0", "", { "peerDependencies": { "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ=="], | ||||
|  | ||||
|     "vite-plugin-inspect": ["vite-plugin-inspect@0.8.9", "", { "dependencies": { "@antfu/utils": "^0.7.10", "@rollup/pluginutils": "^5.1.3", "debug": "^4.3.7", "error-stack-parser-es": "^0.1.5", "fs-extra": "^11.2.0", "open": "^10.1.0", "perfect-debounce": "^1.0.0", "picocolors": "^1.1.1", "sirv": "^3.0.0" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1" } }, "sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A=="], | ||||
|  | ||||
|     "vite-plugin-vue-devtools": ["vite-plugin-vue-devtools@7.7.7", "", { "dependencies": { "@vue/devtools-core": "^7.7.7", "@vue/devtools-kit": "^7.7.7", "@vue/devtools-shared": "^7.7.7", "execa": "^9.5.2", "sirv": "^3.0.1", "vite-plugin-inspect": "0.8.9", "vite-plugin-vue-inspector": "^5.3.1" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-d0fIh3wRcgSlr4Vz7bAk4va1MkdqhQgj9ANE/rBhsAjOnRfTLs2ocjFMvSUOsv6SRRXU9G+VM7yMgqDb6yI4iQ=="], | ||||
|  | ||||
|     "vite-plugin-vue-inspector": ["vite-plugin-vue-inspector@5.3.2", "", { "dependencies": { "@babel/core": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.0", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-transform-typescript": "^7.22.15", "@vue/babel-plugin-jsx": "^1.1.5", "@vue/compiler-dom": "^3.3.4", "kolorist": "^1.8.0", "magic-string": "^0.30.4" }, "peerDependencies": { "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q=="], | ||||
|  | ||||
|     "vooks": ["vooks@0.2.12", "", { "dependencies": { "evtd": "^0.2.2" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q=="], | ||||
|  | ||||
|     "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "vue-tsc": ["vue-tsc@2.2.12", "", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="], | ||||
|  | ||||
|     "vueuc": ["vueuc@0.4.64", "", { "dependencies": { "@css-render/vue3-ssr": "^0.15.10", "@juggle/resize-observer": "^3.3.1", "css-render": "^0.15.10", "evtd": "^0.2.4", "seemly": "^0.3.6", "vdirs": "^0.1.4", "vooks": "^0.2.4" }, "peerDependencies": { "vue": "^3.0.11" } }, "sha512-wlJQj7fIwKK2pOEoOq4Aro8JdPOGpX8aWQhV8YkTW9OgWD2uj2O8ANzvSsIGjx7LTOc7QbS7sXdxHi6XvRnHPA=="], | ||||
|  | ||||
|     "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], | ||||
|  | ||||
|     "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], | ||||
|  | ||||
|     "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], | ||||
|  | ||||
|     "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], | ||||
|  | ||||
|     "xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="], | ||||
|  | ||||
|     "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], | ||||
|  | ||||
|     "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], | ||||
|  | ||||
|     "yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="], | ||||
|  | ||||
|     "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], | ||||
|  | ||||
|     "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], | ||||
|  | ||||
|     "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], | ||||
|  | ||||
|     "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], | ||||
|  | ||||
|     "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.4", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/@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" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||
|  | ||||
|     "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], | ||||
|  | ||||
|     "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], | ||||
|  | ||||
|     "@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5", "", {}, "sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ=="], | ||||
|  | ||||
|     "@vue/devtools-core/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], | ||||
|  | ||||
|     "@vue/language-core/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], | ||||
|  | ||||
|     "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], | ||||
|  | ||||
|     "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], | ||||
|  | ||||
|     "css-render/csstype": ["csstype@3.0.11", "", {}, "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="], | ||||
|  | ||||
|     "execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], | ||||
|  | ||||
|     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], | ||||
|  | ||||
|     "get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], | ||||
|  | ||||
|     "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], | ||||
|  | ||||
|     "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], | ||||
|  | ||||
|     "node-fetch/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], | ||||
|  | ||||
|     "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], | ||||
|  | ||||
|     "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], | ||||
|  | ||||
|     "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], | ||||
|  | ||||
|     "vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], | ||||
|  | ||||
|     "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], | ||||
|  | ||||
|     "@vue/language-core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], | ||||
|  | ||||
|     "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], | ||||
|   } | ||||
| } | ||||
							
								
								
									
										1
									
								
								DysonNetwork.Drive/Client/env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								DysonNetwork.Drive/Client/env.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1 +0,0 @@ | ||||
| /// <reference types="vite/client" /> | ||||
| @@ -1,31 +0,0 @@ | ||||
| import { globalIgnores } from 'eslint/config' | ||||
| import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' | ||||
| import pluginVue from 'eslint-plugin-vue' | ||||
| import pluginOxlint from 'eslint-plugin-oxlint' | ||||
| import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' | ||||
|  | ||||
| // To allow more languages other than `ts` in `.vue` files, uncomment the following lines: | ||||
| // import { configureVueProject } from '@vue/eslint-config-typescript' | ||||
| // configureVueProject({ scriptLangs: ['ts', 'tsx'] }) | ||||
| // More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup | ||||
|  | ||||
| export default defineConfigWithVueTs( | ||||
|   { | ||||
|     name: 'app/files-to-lint', | ||||
|     files: ['**/*.{ts,mts,tsx,vue}'], | ||||
|   }, | ||||
|  | ||||
|   globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']), | ||||
|  | ||||
|   pluginVue.configs['flat/essential'], | ||||
|   vueTsConfigs.recommended, | ||||
|   ...pluginOxlint.configs['flat/recommended'], | ||||
|   { | ||||
|     rules: { | ||||
|       'vue/multi-word-component-names': 'off', | ||||
|       '@typescript-eslint/no-explicit-any': 'off', | ||||
|       '@typescript-eslint/ban-ts-comment': 'off', | ||||
|     }, | ||||
|   }, | ||||
|   skipFormatting, | ||||
| ) | ||||
| @@ -1,14 +0,0 @@ | ||||
| <!doctype html> | ||||
| <html lang=""> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" href="/favicon.png" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Solar Network Drive</title> | ||||
|     <app-data /> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"></div> | ||||
|     <script type="module" src="/src/main.ts"></script> | ||||
|   </body> | ||||
| </html> | ||||
| @@ -1,55 +0,0 @@ | ||||
| { | ||||
|   "name": "@solar-network/drive", | ||||
|   "version": "0.0.0", | ||||
|   "private": true, | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "dev": "vite", | ||||
|     "build": "run-p type-check \"build-only {@}\" --", | ||||
|     "preview": "vite preview", | ||||
|     "build-only": "vite build", | ||||
|     "type-check": "vue-tsc --build", | ||||
|     "lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore", | ||||
|     "lint:eslint": "eslint . --fix", | ||||
|     "lint": "run-s lint:*", | ||||
|     "format": "prettier --write src/" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@fingerprintjs/fingerprintjs": "^4.6.2", | ||||
|     "@fontsource-variable/nunito": "^5.2.6", | ||||
|     "@hcaptcha/vue3-hcaptcha": "^1.3.0", | ||||
|     "@tailwindcss/vite": "^4.1.11", | ||||
|     "@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": { | ||||
|     "@tsconfig/node22": "^22.0.2", | ||||
|     "@types/node": "^22.16.4", | ||||
|     "@vicons/material": "^0.13.0", | ||||
|     "@vitejs/plugin-vue": "^6.0.0", | ||||
|     "@vitejs/plugin-vue-jsx": "^5.0.1", | ||||
|     "@vue/eslint-config-prettier": "^10.2.0", | ||||
|     "@vue/eslint-config-typescript": "^14.6.0", | ||||
|     "@vue/tsconfig": "^0.7.0", | ||||
|     "eslint": "^9.31.0", | ||||
|     "eslint-plugin-oxlint": "~1.1.0", | ||||
|     "eslint-plugin-vue": "~10.2.0", | ||||
|     "jiti": "^2.4.2", | ||||
|     "naive-ui": "^2.42.0", | ||||
|     "npm-run-all2": "^8.0.4", | ||||
|     "oxlint": "~1.1.0", | ||||
|     "prettier": "3.5.3", | ||||
|     "typescript": "~5.8.3", | ||||
|     "vite": "npm:rolldown-vite@latest", | ||||
|     "vite-plugin-vue-devtools": "^7.7.7", | ||||
|     "vue-tsc": "^2.2.12" | ||||
|   } | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 70 KiB | 
| @@ -1,9 +0,0 @@ | ||||
| @import "tailwindcss"; | ||||
|  | ||||
| @layer theme, base, components, utilities; | ||||
|  | ||||
| @layer base { | ||||
|   body { | ||||
|     font-family: 'Nunito Variable', sans-serif; | ||||
|   } | ||||
| } | ||||
| @@ -1,50 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,199 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,271 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,75 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,7 +0,0 @@ | ||||
| export {} | ||||
|  | ||||
| declare global { | ||||
|   interface Window { | ||||
|     DyPrefetch?: any | ||||
|   } | ||||
| } | ||||
| @@ -1,62 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,115 +0,0 @@ | ||||
| <template> | ||||
|   <n-layout> | ||||
|     <n-layout-header class="border-b-1 flex justify-between items-center"> | ||||
|       <router-link to="/" class="text-lg font-bold">Solar Network Drive</router-link> | ||||
|       <div v-if="!hideUserMenu"> | ||||
|         <n-dropdown | ||||
|           v-if="!userStore.isAuthenticated" | ||||
|           :options="guestOptions" | ||||
|           @select="handleGuestMenuSelect" | ||||
|         > | ||||
|           <n-button>Account</n-button> | ||||
|         </n-dropdown> | ||||
|         <n-dropdown v-else :options="userOptions" @select="handleUserMenuSelect" type="primary"> | ||||
|           <n-button>{{ userStore.user.nick }}</n-button> | ||||
|         </n-dropdown> | ||||
|       </div> | ||||
|     </n-layout-header> | ||||
|     <n-layout-content embedded> | ||||
|       <router-view /> | ||||
|     </n-layout-content> | ||||
|   </n-layout> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { computed, h } from 'vue' | ||||
| import { NLayout, NLayoutHeader, NLayoutContent, NButton, NDropdown, NIcon } from 'naive-ui' | ||||
| 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(() => { | ||||
|   return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string) | ||||
| }) | ||||
|  | ||||
| const guestOptions = [ | ||||
|   { | ||||
|     label: 'Login', | ||||
|     key: 'login', | ||||
|     icon: () => | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(LogInOutlined), | ||||
|       }), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Create Account', | ||||
|     key: 'create-account', | ||||
|     icon: () => | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(PersonAddAlt1Outlined), | ||||
|       }), | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const userOptions = computed(() => [ | ||||
|   { | ||||
|     label: 'Dashboard', | ||||
|     key: 'dashboardUsage', | ||||
|     icon: () => | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(DataUsageRound), | ||||
|       }), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Profile', | ||||
|     key: 'profile', | ||||
|     icon: () => | ||||
|       h(NIcon, null, { | ||||
|         default: () => h(PersonOutlineRound), | ||||
|       }), | ||||
|   }, | ||||
| ]) | ||||
|  | ||||
| const servicesStore = useServicesStore() | ||||
|  | ||||
| function handleGuestMenuSelect(key: string) { | ||||
|   if (key === 'login') { | ||||
|     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'login')!, '_blank') | ||||
|   } else if (key === 'create-account') { | ||||
|     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'create-account')!, '_blank') | ||||
|   } | ||||
| } | ||||
|  | ||||
| function handleUserMenuSelect(key: string) { | ||||
|   if (key === 'profile') { | ||||
|     window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'accounts/me')!, '_blank') | ||||
|   } else { | ||||
|     router.push({ name: key }) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .n-layout-header { | ||||
|   padding: 8px 24px; | ||||
|   border-color: var(--n-border-color); | ||||
|   height: 57px; /* Fixed height */ | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .n-layout-content { | ||||
|   height: calc(100vh - 57px); /* Adjust based on header height */ | ||||
| } | ||||
| </style> | ||||
| @@ -1,16 +0,0 @@ | ||||
| import '@fontsource-variable/nunito'; | ||||
|  | ||||
| import './assets/main.css' | ||||
|  | ||||
| import { createApp } from 'vue' | ||||
| import { createPinia } from 'pinia' | ||||
|  | ||||
| import Root from './root.vue' | ||||
| import router from './router' | ||||
|  | ||||
| const app = createApp(Root) | ||||
|  | ||||
| app.use(createPinia()) | ||||
| app.use(router) | ||||
|  | ||||
| app.mount('#app') | ||||
| @@ -1,55 +0,0 @@ | ||||
| <script setup lang="ts"> | ||||
| import LayoutDefault from './layouts/default.vue' | ||||
|  | ||||
| import { RouterView } from 'vue-router' | ||||
| 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' | ||||
| import { useServicesStore } from './stores/services' | ||||
|  | ||||
| const themeOverrides = { | ||||
|   common: { | ||||
|     fontFamily: 'Nunito Variable, v-sans, ui-system, -apple-system, sans-serif', | ||||
|     primaryColor: '#7D80BAFF', | ||||
|     primaryColorHover: '#9294C5FF', | ||||
|     primaryColorPressed: '#575B9DFF', | ||||
|     primaryColorSuppl: '#6B6FC1FF', | ||||
|   }, | ||||
| } | ||||
|  | ||||
| const isDark = usePreferredDark() | ||||
|  | ||||
| const userStore = useUserStore() | ||||
| const servicesStore = useServicesStore() | ||||
|  | ||||
| onMounted(() => { | ||||
|   userStore.initialize() | ||||
|  | ||||
|   userStore.fetchUser() | ||||
|   servicesStore.fetchServices() | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <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,86 +0,0 @@ | ||||
| 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), | ||||
|   routes: [ | ||||
|     { | ||||
|       path: '/', | ||||
|       name: 'index', | ||||
|       component: () => import('../views/index.vue'), | ||||
|     }, | ||||
|     { | ||||
|       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) { | ||||
|     await userStore.fetchUser() | ||||
|   } | ||||
|  | ||||
|   if (to.matched.some((record) => record.meta.requiresAuth) && !userStore.isAuthenticated) { | ||||
|     window.open( | ||||
|       servicesStore.getSerivceUrl( | ||||
|         'DysonNetwork.Pass', | ||||
|         'login?redirect=' + encodeURIComponent(window.location.href), | ||||
|       )!, | ||||
|       '_blank', | ||||
|     ) | ||||
|     next('/') | ||||
|   } else { | ||||
|     next() | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default router | ||||
| @@ -1,27 +0,0 @@ | ||||
| import { defineStore } from 'pinia' | ||||
| import { ref } from 'vue' | ||||
|  | ||||
| export const useServicesStore = defineStore('services', () => { | ||||
|   const services = ref<Record<string, string>>({}) | ||||
|  | ||||
|   async function fetchServices() { | ||||
|     try { | ||||
|       const response = await fetch('/cgi/.well-known/services') | ||||
|       if (!response.ok) { | ||||
|         throw new Error('Network response was not ok') | ||||
|       } | ||||
|       const data = await response.json() | ||||
|       services.value = data | ||||
|     } catch (error) { | ||||
|       console.error('Failed to fetch services:', error) | ||||
|       services.value = {} | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function getSerivceUrl(serviceName: string, ...parts: string[]): string | null { | ||||
|     const baseUrl = services.value[serviceName] || null | ||||
|     return baseUrl ? `${baseUrl}/${parts.join('/')}` : null | ||||
|   } | ||||
|  | ||||
|   return { services, fetchServices, getSerivceUrl } | ||||
| }) | ||||
| @@ -1,65 +0,0 @@ | ||||
| import { defineStore } from 'pinia' | ||||
| import { ref, computed } from 'vue' | ||||
|  | ||||
| export const useUserStore = defineStore('user', () => { | ||||
|   // State | ||||
|   const user = ref<any>(null) | ||||
|   const isLoading = ref(false) | ||||
|   const error = ref<string | null>(null) | ||||
|  | ||||
|   // Getters | ||||
|   const isAuthenticated = computed(() => !!user.value) | ||||
|  | ||||
|   // Actions | ||||
|   async function fetchUser(reload = true) { | ||||
|     if (!reload && user.value) return | ||||
|     isLoading.value = true | ||||
|     error.value = null | ||||
|     try { | ||||
|       const response = await fetch('/cgi/id/accounts/me', { | ||||
|         credentials: 'include', | ||||
|       }) | ||||
|  | ||||
|       if (!response.ok) { | ||||
|         // If the token is invalid, clear it and the user state | ||||
|         throw new Error('Failed to fetch user information.') | ||||
|       } | ||||
|  | ||||
|       user.value = await response.json() | ||||
|     } catch (e: any) { | ||||
|       error.value = e.message | ||||
|       user.value = null // Clear user data on error | ||||
|     } finally { | ||||
|       isLoading.value = false | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function initialize() { | ||||
|     const allowedOrigin = import.meta.env.DEV ? window.location.origin : 'https://id.solian.app' | ||||
|     window.addEventListener('message', (event) => { | ||||
|       // IMPORTANT: Always check the origin of the message for security! | ||||
|       // This prevents malicious scripts from sending fake login status updates. | ||||
|       // Ensure event.origin exactly matches your identity service's origin. | ||||
|       if (event.origin !== allowedOrigin) { | ||||
|         console.warn(`[SYNC] Message received from unexpected origin: ${event.origin}. Ignoring.`) | ||||
|         return // Ignore messages from unknown origins | ||||
|       } | ||||
|  | ||||
|       // Check if the message is the type we're expecting | ||||
|       if (event.data && event.data.type === 'DY:LOGIN_STATUS_CHANGE') { | ||||
|         const { loggedIn } = event.data | ||||
|         console.log(`[SYNC] Received login status change: ${loggedIn}`) | ||||
|         fetchUser() // Re-fetch user data on login status change | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     user, | ||||
|     isLoading, | ||||
|     error, | ||||
|     isAuthenticated, | ||||
|     fetchUser, | ||||
|     initialize, | ||||
|   } | ||||
| }) | ||||
| @@ -1,37 +0,0 @@ | ||||
| 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 | ||||
| } | ||||
| @@ -1,255 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,180 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,304 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,101 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,164 +0,0 @@ | ||||
| <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,262 +0,0 @@ | ||||
| <template> | ||||
|   <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-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, | ||||
|   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 = 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() | ||||
|  | ||||
| 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> | ||||
| @@ -1,8 +0,0 @@ | ||||
| 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,164 +0,0 @@ | ||||
| <template> | ||||
|   <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> | ||||
|     </n-card> | ||||
|  | ||||
|     <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> | ||||
|  | ||||
|         <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-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()" | ||||
|             > | ||||
|               {{ isBundleMode ? 'Cancel' : 'Proceed to Upload' }} | ||||
|             </n-button> | ||||
|           </div> | ||||
|  | ||||
|           <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> | ||||
|       <span v-else> | ||||
|         v{{ version.version }} @ | ||||
|         {{ version.commit.substring(0, 6) }} | ||||
|         {{ version.updatedAt }} | ||||
|       </span> | ||||
|     </p> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { NCard, NSwitch, NTabs, NTabPane, NButton, NModal } from 'naive-ui' | ||||
| import { computed, onMounted, ref } from 'vue' | ||||
| import { useUserStore } from '@/stores/user' | ||||
| 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 filePool = ref<string | null>(null) | ||||
|  | ||||
| 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: { | ||||
|         'Content-Type': 'application/json', | ||||
|       }, | ||||
|       body: JSON.stringify(newBundle.value), | ||||
|     }) | ||||
|     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) | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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> | ||||
| @@ -1,16 +0,0 @@ | ||||
| <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,94 +0,0 @@ | ||||
| export async function downloadAndDecryptFile( | ||||
|   url: string, | ||||
|   password: string, | ||||
|   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 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 | ||||
|     if (value) { | ||||
|       chunks.push(value) | ||||
|       received += value.length | ||||
|       if (contentLength && onProgress) { | ||||
|         onProgress(received / contentLength) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const fullBuffer = new Uint8Array(received) | ||||
|   let offset = 0 | ||||
|   for (const chunk of chunks) { | ||||
|     fullBuffer.set(chunk, offset) | ||||
|     offset += chunk.length | ||||
|   } | ||||
|  | ||||
|   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 = 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) | ||||
|  | ||||
|   const enc = new TextEncoder() | ||||
|   const keyMaterial = await crypto.subtle.importKey( | ||||
|     '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'], | ||||
|   ) | ||||
|  | ||||
|   const fullCiphertext = new Uint8Array(ciphertext.length + tag.length) | ||||
|   fullCiphertext.set(ciphertext) | ||||
|   fullCiphertext.set(tag, ciphertext.length) | ||||
|  | ||||
|   let decrypted: ArrayBuffer | ||||
|   try { | ||||
|     decrypted = await crypto.subtle.decrypt( | ||||
|       { name: 'AES-GCM', iv: nonce, tagLength: 128 }, | ||||
|       key, | ||||
|       fullCiphertext, | ||||
|     ) | ||||
|   } catch { | ||||
|     throw new Error('Incorrect password or corrupted file.') | ||||
|   } | ||||
|  | ||||
|   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.') | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return decryptedBytes.slice(magic.length) | ||||
| } | ||||
| @@ -1,12 +0,0 @@ | ||||
| { | ||||
|   "extends": "@vue/tsconfig/tsconfig.dom.json", | ||||
|   "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "./**/*.d.ts"], | ||||
|   "exclude": ["src/**/__tests__/*"], | ||||
|   "compilerOptions": { | ||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", | ||||
|  | ||||
|     "paths": { | ||||
|       "@/*": ["./src/*"] | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| { | ||||
|   "files": [], | ||||
|   "references": [ | ||||
|     { | ||||
|       "path": "./tsconfig.node.json" | ||||
|     }, | ||||
|     { | ||||
|       "path": "./tsconfig.app.json" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| { | ||||
|   "extends": "@tsconfig/node22/tsconfig.json", | ||||
|   "include": [ | ||||
|     "vite.config.*", | ||||
|     "vitest.config.*", | ||||
|     "cypress.config.*", | ||||
|     "nightwatch.conf.*", | ||||
|     "playwright.config.*", | ||||
|     "eslint.config.*" | ||||
|   ], | ||||
|   "compilerOptions": { | ||||
|     "noEmit": true, | ||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", | ||||
|  | ||||
|     "module": "ESNext", | ||||
|     "moduleResolution": "Bundler", | ||||
|     "types": ["node"] | ||||
|   } | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| import { fileURLToPath, URL } from 'node:url' | ||||
|  | ||||
| import { defineConfig } from 'vite' | ||||
| import vue from '@vitejs/plugin-vue' | ||||
| import vueJsx from '@vitejs/plugin-vue-jsx' | ||||
| import vueDevTools from 'vite-plugin-vue-devtools' | ||||
| import tailwindcss from '@tailwindcss/vite' | ||||
|  | ||||
| process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' | ||||
|  | ||||
| // https://vite.dev/config/ | ||||
| export default defineConfig({ | ||||
|   base: '/', | ||||
|   plugins: [vue(), vueJsx(), vueDevTools(), tailwindcss()], | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       '@': fileURLToPath(new URL('./src', import.meta.url)), | ||||
|     }, | ||||
|   }, | ||||
|   server: { | ||||
|     proxy: { | ||||
|       '/api': { | ||||
|         target: 'http://localhost:5090', | ||||
|         changeOrigin: true, | ||||
|       }, | ||||
|       '/cgi': { | ||||
|         target: 'http://localhost:5090', | ||||
|         changeOrigin: true, | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
| @@ -3,6 +3,8 @@ WORKDIR /app | ||||
| EXPOSE 8080 | ||||
| EXPOSE 8081 | ||||
|  | ||||
| # Stage 1: Install runtime dependencies | ||||
|  | ||||
| # Install only necessary dependencies | ||||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||||
|   libfontconfig1 \ | ||||
| @@ -17,24 +19,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ | ||||
|      | ||||
| USER $APP_UID | ||||
|  | ||||
| # Stage 2: Build SPA | ||||
| FROM node:22-alpine AS spa-builder | ||||
| WORKDIR /src | ||||
|  | ||||
| # Copy package files for SPA | ||||
| COPY ["DysonNetwork.Drive/Client/package.json", "DysonNetwork.Drive/Client/package-lock.json*", "./Client/"] | ||||
|  | ||||
| # Install SPA dependencies | ||||
| WORKDIR /src/Client | ||||
| RUN npm install | ||||
|  | ||||
| # Copy SPA source | ||||
| COPY ["DysonNetwork.Drive/Client/", "./"] | ||||
|  | ||||
| # Build SPA | ||||
| RUN npm run build | ||||
|  | ||||
| # Stage 3: Build .NET application | ||||
| # Stage 2: Build .NET application | ||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| WORKDIR /src | ||||
| @@ -42,9 +27,6 @@ COPY ["DysonNetwork.Drive/DysonNetwork.Drive.csproj", "DysonNetwork.Drive/"] | ||||
| RUN dotnet restore "DysonNetwork.Drive/DysonNetwork.Drive.csproj" | ||||
| COPY . . | ||||
|  | ||||
| # Copy built SPA to wwwroot | ||||
| COPY --from=spa-builder /src/Client/dist /src/DysonNetwork.Drive/wwwroot/dist | ||||
|  | ||||
| WORKDIR "/src/DysonNetwork.Drive" | ||||
| RUN dotnet build "./DysonNetwork.Drive.csproj" -c $BUILD_CONFIGURATION -o /app/build \ | ||||
|     -p:TypeScriptCompileBlocked=true \ | ||||
|   | ||||
| @@ -10,54 +10,49 @@ | ||||
|     <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="FFMpegCore" Version="5.3.0" /> | ||||
|         <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" /> | ||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7"> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" /> | ||||
|         <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10"> | ||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|           <PrivateAssets>all</PrivateAssets> | ||||
|         </PackageReference> | ||||
|         <PackageReference Include="MimeKit" Version="4.14.0" /> | ||||
|         <PackageReference Include="MimeTypes" Version="2.5.2"> | ||||
|           <PrivateAssets>all</PrivateAssets> | ||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|         </PackageReference> | ||||
|         <PackageReference Include="Minio" Version="6.0.5" /> | ||||
|         <PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115"> | ||||
|         <PackageReference Include="Nanoid" Version="3.1.0" /> | ||||
|         <PackageReference Include="Nerdbank.GitVersioning" Version="3.8.118"> | ||||
|           <PrivateAssets>all</PrivateAssets> | ||||
|           <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|         </PackageReference> | ||||
|         <PackageReference Include="NetVips" Version="3.1.0" /> | ||||
|         <PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" /> | ||||
|         <PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.1" /> | ||||
|         <PackageReference Include="NetVips.Native.linux-x64" Version="8.17.2" /> | ||||
|         <PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.2" /> | ||||
|         <PackageReference Include="NodaTime" Version="3.2.2" /> | ||||
|         <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" /> | ||||
|         <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" /> | ||||
|         <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" /> | ||||
|         <PackageReference Include="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" /> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" /> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" /> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" /> | ||||
|         <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" /> | ||||
|         <PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" /> | ||||
|         <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1" /> | ||||
|         <PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5" /> | ||||
|         <PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0" /> | ||||
|         <PackageReference Include="Quartz" Version="3.14.0" /> | ||||
|         <PackageReference Include="Quartz.AspNetCore" Version="3.14.0" /> | ||||
|         <PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" /> | ||||
|         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" /> | ||||
|         <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" /> | ||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" /> | ||||
|         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" /> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" /> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" /> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" /> | ||||
|         <PackageReference Include="Quartz" Version="3.15.0" /> | ||||
|         <PackageReference Include="Quartz.AspNetCore" Version="3.15.0" /> | ||||
|         <PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" /> | ||||
|         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.2" /> | ||||
|         <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.2" /> | ||||
|         <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> | ||||
|         <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" /> | ||||
|         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" /> | ||||
|         <PackageReference Include="tusdotnet" Version="2.10.0" /> | ||||
|         <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.1" /> | ||||
|         <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" /> | ||||
|         <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
| @@ -69,13 +64,4 @@ | ||||
|     <ItemGroup> | ||||
|       <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <_ContentIncludedByDefault Remove="Pages\Emails\AccountDeletionEmail.razor" /> | ||||
|       <_ContentIncludedByDefault Remove="Pages\Emails\ContactVerificationEmail.razor" /> | ||||
|       <_ContentIncludedByDefault Remove="Pages\Emails\EmailLayout.razor" /> | ||||
|       <_ContentIncludedByDefault Remove="Pages\Emails\LandingEmail.razor" /> | ||||
|       <_ContentIncludedByDefault Remove="Pages\Emails\PasswordResetEmail.razor" /> | ||||
|       <_ContentIncludedByDefault Remove="Pages\Emails\VerificationEmail.razor" /> | ||||
|     </ItemGroup> | ||||
| </Project> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -1,7 +1,4 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System.Collections.Generic; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
|   | ||||
							
								
								
									
										402
									
								
								DysonNetwork.Drive/Migrations/20250907070034_RemoveNetTopo.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										402
									
								
								DysonNetwork.Drive/Migrations/20250907070034_RemoveNetTopo.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,402 @@ | ||||
| // <auto-generated /> | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Shared.Models; | ||||
| 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", ",,"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -2,8 +2,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| @@ -24,7 +23,6 @@ namespace DysonNetwork.Drive.Migrations | ||||
|                 .HasAnnotation("ProductVersion", "9.0.7") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
|  | ||||
|             NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
|  | ||||
|             modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => | ||||
|   | ||||
| @@ -1,24 +0,0 @@ | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.PageData; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Pages.Data; | ||||
|  | ||||
| public class VersionPageData : IPageDataProvider | ||||
| { | ||||
|     public bool CanHandlePath(PathString path) => true; | ||||
|  | ||||
|     public Task<IDictionary<string, object?>> GetAppDataAsync(HttpContext context) | ||||
|     { | ||||
|         var versionData = new AppVersion | ||||
|         { | ||||
|             Version = ThisAssembly.AssemblyVersion, | ||||
|             Commit = ThisAssembly.GitCommitId, | ||||
|             UpdateDate = ThisAssembly.GitCommitDate | ||||
|         }; | ||||
|  | ||||
|         var result = typeof(AppVersion).GetProperties() | ||||
|             .ToDictionary(property => property.Name, property => property.GetValue(versionData)); | ||||
|  | ||||
|         return Task.FromResult<IDictionary<string, object?>>(result); | ||||
|     } | ||||
| } | ||||
| @@ -1,44 +1,37 @@ | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Pages.Data; | ||||
| using DysonNetwork.Drive.Startup; | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using DysonNetwork.Shared.PageData; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using tusdotnet.Stores; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| builder.AddServiceDefaults(); | ||||
|  | ||||
| // Configure Kestrel and server options | ||||
| builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue); | ||||
|  | ||||
| // Add application services | ||||
| builder.Services.AddRegistryService(builder.Configuration); | ||||
| builder.Services.AddStreamConnection(builder.Configuration); | ||||
|  | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
| builder.Services.AddAppRateLimiting(); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| builder.Services.AddAppSwagger(); | ||||
| builder.Services.AddDysonAuth(); | ||||
| builder.Services.AddAccountService(); | ||||
|  | ||||
| builder.Services.AddAppFileStorage(builder.Configuration); | ||||
|  | ||||
| // Add flush handlers and websocket handlers | ||||
| builder.Services.AddAppFlushHandlers(); | ||||
|  | ||||
| // Add business services | ||||
| builder.Services.AddAppBusinessServices(); | ||||
|  | ||||
| // Add scheduled jobs | ||||
| builder.Services.AddAppScheduledJobs(); | ||||
|  | ||||
| builder.Services.AddTransient<IPageDataProvider, VersionPageData>(); | ||||
| builder.AddSwaggerManifest( | ||||
|     "DysonNetwork.Drive", | ||||
|     "The file upload and storage service in the Solar Network." | ||||
| ); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| app.MapDefaultEndpoints(); | ||||
|  | ||||
| // Run database migrations | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
| @@ -46,16 +39,11 @@ using (var scope = app.Services.CreateScope()) | ||||
|     await db.Database.MigrateAsync(); | ||||
| } | ||||
|  | ||||
| var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>(); | ||||
|  | ||||
| // Configure application middleware pipeline | ||||
| app.ConfigureAppMiddleware(tusDiskStore, builder.Environment.ContentRootPath); | ||||
|  | ||||
| app.MapGatewayProxy(); | ||||
|  | ||||
| app.MapPages(Path.Combine(app.Environment.WebRootPath, "dist", "index.html")); | ||||
| app.ConfigureAppMiddleware(); | ||||
|  | ||||
| // Configure gRPC | ||||
| app.ConfigureGrpcServices(); | ||||
|  | ||||
| app.Run(); | ||||
| app.UseSwaggerManifest("DysonNetwork.Drive"); | ||||
|  | ||||
| app.Run(); | ||||
|   | ||||
| @@ -5,7 +5,6 @@ | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "http://localhost:5090", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
| @@ -14,7 +13,6 @@ | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "https://localhost:7092;http://localhost:5090", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
|   | ||||
| @@ -1,40 +1,13 @@ | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using Microsoft.Extensions.FileProviders; | ||||
| using tusdotnet; | ||||
| using tusdotnet.Interfaces; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Startup; | ||||
|  | ||||
| public static class ApplicationBuilderExtensions | ||||
| { | ||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app, ITusStore tusStore, string contentRoot) | ||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app) | ||||
|     { | ||||
|         // Configure the HTTP request pipeline. | ||||
|         if (app.Environment.IsDevelopment()) | ||||
|         { | ||||
|             app.UseSwagger(); | ||||
|             app.UseSwaggerUI(); | ||||
|         } | ||||
|  | ||||
|         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; | ||||
|     } | ||||
| @@ -44,6 +17,7 @@ public static class ApplicationBuilderExtensions | ||||
|         // Map your gRPC services here | ||||
|         app.MapGrpcService<FileServiceGrpc>(); | ||||
|         app.MapGrpcService<FileReferenceServiceGrpc>(); | ||||
|         app.MapGrpcReflectionService(); | ||||
|  | ||||
|         return app; | ||||
|     } | ||||
|   | ||||
| @@ -1,8 +1,16 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using DysonNetwork.Drive.Storage.Model; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using FFMpegCore; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NATS.Client.Core; | ||||
| using NATS.Client.JetStream; | ||||
| using NATS.Client.JetStream.Models; | ||||
| using NATS.Net; | ||||
| using NetVips; | ||||
| using NodaTime; | ||||
| using FileService = DysonNetwork.Drive.Storage.FileService; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Startup; | ||||
|  | ||||
| @@ -12,14 +20,79 @@ public class BroadcastEventHandler( | ||||
|     IServiceProvider serviceProvider | ||||
| ) : BackgroundService | ||||
| { | ||||
|     private const string TempFileSuffix = "dypart"; | ||||
|  | ||||
|     private static readonly string[] AnimatedImageTypes = | ||||
|         ["image/gif", "image/apng", "image/avif"]; | ||||
|  | ||||
|     private static readonly string[] AnimatedImageExtensions = | ||||
|         [".gif", ".apng", ".avif"]; | ||||
|  | ||||
|  | ||||
|  | ||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|     { | ||||
|         await foreach (var msg in nats.SubscribeAsync<byte[]>(AccountDeletedEvent.Type, cancellationToken: stoppingToken)) | ||||
|         var js = nats.CreateJetStreamContext(); | ||||
|  | ||||
|         await js.EnsureStreamCreated("account_events", [AccountDeletedEvent.Type]); | ||||
|          | ||||
|         var accountEventConsumer = await js.CreateOrUpdateConsumerAsync("account_events", | ||||
|             new ConsumerConfig("drive_account_deleted_handler"), cancellationToken: stoppingToken); | ||||
|          | ||||
|         await js.EnsureStreamCreated("file_events", [FileUploadedEvent.Type]); | ||||
|         var fileUploadedConsumer = await js.CreateOrUpdateConsumerAsync("file_events", | ||||
|             new ConsumerConfig("drive_file_uploaded_handler") { MaxDeliver = 3 }, cancellationToken: stoppingToken); | ||||
|  | ||||
|         var accountDeletedTask = HandleAccountDeleted(accountEventConsumer, stoppingToken); | ||||
|         var fileUploadedTask = HandleFileUploaded(fileUploadedConsumer, stoppingToken); | ||||
|  | ||||
|         await Task.WhenAll(accountDeletedTask, fileUploadedTask); | ||||
|     } | ||||
|  | ||||
|     private async Task HandleFileUploaded(INatsJSConsumer consumer, CancellationToken stoppingToken) | ||||
|     { | ||||
|         await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken)) | ||||
|         { | ||||
|             var payload = JsonSerializer.Deserialize<FileUploadedEventPayload>(msg.Data, GrpcTypeHelper.SerializerOptions); | ||||
|             if (payload == null) | ||||
|             { | ||||
|                 await msg.AckAsync(cancellationToken: stoppingToken); | ||||
|                 continue; | ||||
|             } | ||||
|              | ||||
|             try | ||||
|             { | ||||
|                 await ProcessAndUploadInBackgroundAsync( | ||||
|                     payload.FileId, | ||||
|                     payload.RemoteId, | ||||
|                     payload.StorageId, | ||||
|                     payload.ContentType, | ||||
|                     payload.ProcessingFilePath, | ||||
|                     payload.IsTempFile | ||||
|                 ); | ||||
|  | ||||
|                 await msg.AckAsync(cancellationToken: stoppingToken); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 logger.LogError(ex, "Error processing FileUploadedEvent for file {FileId}", payload.FileId); | ||||
|                 await msg.NakAsync(cancellationToken: stoppingToken, delay: TimeSpan.FromSeconds(60)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private async Task HandleAccountDeleted(INatsJSConsumer consumer, CancellationToken stoppingToken) | ||||
|     { | ||||
|         await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken)) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var evt = JsonSerializer.Deserialize<AccountDeletedEvent>(msg.Data); | ||||
|                 if (evt == null) continue; | ||||
|                 var evt = JsonSerializer.Deserialize<AccountDeletedEvent>(msg.Data, GrpcTypeHelper.SerializerOptions); | ||||
|                 if (evt == null) | ||||
|                 { | ||||
|                     await msg.AckAsync(cancellationToken: stoppingToken); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 logger.LogInformation("Account deleted: {AccountId}", evt.AccountId); | ||||
|  | ||||
| @@ -46,11 +119,179 @@ public class BroadcastEventHandler( | ||||
|                     await transaction.RollbackAsync(cancellationToken: stoppingToken); | ||||
|                     throw; | ||||
|                 } | ||||
|  | ||||
|                 await msg.AckAsync(cancellationToken: stoppingToken); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 logger.LogError(ex, "Error processing AccountDeleted"); | ||||
|                 await msg.NakAsync(cancellationToken: stoppingToken); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|      | ||||
|      private async Task ProcessAndUploadInBackgroundAsync( | ||||
|         string fileId, | ||||
|         Guid remoteId, | ||||
|         string storageId, | ||||
|         string contentType, | ||||
|         string processingFilePath, | ||||
|         bool isTempFile | ||||
|     ) | ||||
|     { | ||||
|         using var scope = serviceProvider.CreateScope(); | ||||
|         var fs = scope.ServiceProvider.GetRequiredService<FileService>(); | ||||
|         var scopedDb = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||
|  | ||||
|         var pool = await fs.GetPoolAsync(remoteId); | ||||
|         if (pool is null) return; | ||||
|  | ||||
|         var uploads = new List<(string FilePath, string Suffix, string ContentType, bool SelfDestruct)>(); | ||||
|         var newMimeType = contentType; | ||||
|         var hasCompression = false; | ||||
|         var hasThumbnail = false; | ||||
|  | ||||
|         logger.LogInformation("Processing file {FileId} in background...", fileId); | ||||
|  | ||||
|         var fileToUpdate = await scopedDb.Files.AsNoTracking().FirstAsync(f => f.Id == fileId); | ||||
|  | ||||
|         if (fileToUpdate.IsEncrypted) | ||||
|         { | ||||
|             uploads.Add((processingFilePath, string.Empty, contentType, false)); | ||||
|         } | ||||
|         else if (!pool.PolicyConfig.NoOptimization) | ||||
|         { | ||||
|             var fileExtension = Path.GetExtension(processingFilePath); | ||||
|             switch (contentType.Split('/')[0]) | ||||
|             { | ||||
|                 case "image": | ||||
|                     if (AnimatedImageTypes.Contains(contentType) || AnimatedImageExtensions.Contains(fileExtension)) | ||||
|                     { | ||||
|                         logger.LogInformation("Skip optimize file {FileId} due to it is animated...", fileId); | ||||
|                         uploads.Add((processingFilePath, string.Empty, contentType, false)); | ||||
|                         break; | ||||
|                     } | ||||
|  | ||||
|                     try | ||||
|                     { | ||||
|                         newMimeType = "image/webp"; | ||||
|                         using var vipsImage = Image.NewFromFile(processingFilePath); | ||||
|                         var imageToWrite = vipsImage; | ||||
|  | ||||
|                         if (vipsImage.Interpretation is Enums.Interpretation.Scrgb or Enums.Interpretation.Xyz) | ||||
|                         { | ||||
|                             imageToWrite = vipsImage.Colourspace(Enums.Interpretation.Srgb); | ||||
|                         } | ||||
|  | ||||
|                         var webpPath = Path.Join(Path.GetTempPath(), $"{fileId}.{TempFileSuffix}.webp"); | ||||
|                         imageToWrite.Autorot().WriteToFile(webpPath, | ||||
|                             new VOption { { "lossless", true }, { "strip", true } }); | ||||
|                         uploads.Add((webpPath, string.Empty, newMimeType, true)); | ||||
|  | ||||
|                         if (imageToWrite.Width * imageToWrite.Height >= 1024 * 1024) | ||||
|                         { | ||||
|                             var scale = 1024.0 / Math.Max(imageToWrite.Width, imageToWrite.Height); | ||||
|                             var compressedPath = | ||||
|                                 Path.Join(Path.GetTempPath(), $"{fileId}.{TempFileSuffix}.compressed.webp"); | ||||
|                             using var compressedImage = imageToWrite.Resize(scale); | ||||
|                             compressedImage.Autorot().WriteToFile(compressedPath, | ||||
|                                 new VOption { { "Q", 80 }, { "strip", true } }); | ||||
|                             uploads.Add((compressedPath, ".compressed", newMimeType, true)); | ||||
|                             hasCompression = true; | ||||
|                         } | ||||
|  | ||||
|                         if (!ReferenceEquals(imageToWrite, vipsImage)) | ||||
|                         { | ||||
|                             imageToWrite.Dispose(); | ||||
|                         } | ||||
|                     } | ||||
|                     catch (Exception ex) | ||||
|                     { | ||||
|                         logger.LogError(ex, "Failed to optimize image {FileId}, uploading original", fileId); | ||||
|                         uploads.Add((processingFilePath, string.Empty, contentType, false)); | ||||
|                         newMimeType = contentType; | ||||
|                     } | ||||
|  | ||||
|                     break; | ||||
|  | ||||
|                 case "video": | ||||
|                     uploads.Add((processingFilePath, string.Empty, contentType, false)); | ||||
|  | ||||
|                     var thumbnailPath = Path.Join(Path.GetTempPath(), $"{fileId}.{TempFileSuffix}.thumbnail.jpg"); | ||||
|                     try | ||||
|                     { | ||||
|                         await FFMpegArguments | ||||
|                             .FromFileInput(processingFilePath, verifyExists: true) | ||||
|                             .OutputToFile(thumbnailPath, overwrite: true, options => options | ||||
|                                 .Seek(TimeSpan.FromSeconds(0)) | ||||
|                                 .WithFrameOutputCount(1) | ||||
|                                 .WithCustomArgument("-q:v 2") | ||||
|                             ) | ||||
|                             .NotifyOnOutput(line => logger.LogInformation("[FFmpeg] {Line}", line)) | ||||
|                             .NotifyOnError(line => logger.LogWarning("[FFmpeg] {Line}", line)) | ||||
|                             .ProcessAsynchronously(); | ||||
|  | ||||
|                         if (File.Exists(thumbnailPath)) | ||||
|                         { | ||||
|                             uploads.Add((thumbnailPath, ".thumbnail", "image/jpeg", true)); | ||||
|                             hasThumbnail = true; | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|                             logger.LogWarning("FFMpeg did not produce thumbnail for video {FileId}", fileId); | ||||
|                         } | ||||
|                     } | ||||
|                     catch (Exception ex) | ||||
|                     { | ||||
|                         logger.LogError(ex, "Failed to generate thumbnail for video {FileId}", fileId); | ||||
|                     } | ||||
|  | ||||
|                     break; | ||||
|  | ||||
|                 default: | ||||
|                     uploads.Add((processingFilePath, string.Empty, contentType, false)); | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             uploads.Add((processingFilePath, string.Empty, contentType, false)); | ||||
|         } | ||||
|  | ||||
|         logger.LogInformation("Optimized file {FileId}, now uploading...", fileId); | ||||
|  | ||||
|         if (uploads.Count > 0) | ||||
|         { | ||||
|             var destPool = remoteId; | ||||
|             var uploadTasks = uploads.Select(item => | ||||
|                 fs.UploadFileToRemoteAsync( | ||||
|                     storageId, | ||||
|                     destPool, | ||||
|                     item.FilePath, | ||||
|                     item.Suffix, | ||||
|                     item.ContentType, | ||||
|                     item.SelfDestruct | ||||
|                 ) | ||||
|             ).ToList(); | ||||
|  | ||||
|             await Task.WhenAll(uploadTasks); | ||||
|  | ||||
|             logger.LogInformation("Uploaded file {FileId} done!", fileId); | ||||
|  | ||||
|             var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|             await scopedDb.Files.Where(f => f.Id == fileId).ExecuteUpdateAsync(setter => setter | ||||
|                 .SetProperty(f => f.UploadedAt, now) | ||||
|                 .SetProperty(f => f.PoolId, destPool) | ||||
|                 .SetProperty(f => f.MimeType, newMimeType) | ||||
|                 .SetProperty(f => f.HasCompression, hasCompression) | ||||
|                 .SetProperty(f => f.HasThumbnail, hasThumbnail) | ||||
|             ); | ||||
|  | ||||
|             // Only delete temp file after successful upload and db update | ||||
|             if (isTempFile) | ||||
|                 File.Delete(processingFilePath); | ||||
|         } | ||||
|  | ||||
|         await fs._PurgeCacheAsync(fileId); | ||||
|     } | ||||
| } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user