Compare commits
	
		
			118 Commits
		
	
	
		
			71fe2a30e7
			...
			refactor/w
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						28d60c722a
	
				 | 
					
					
						|||
| 
						
						
							
						
						4626529eb5
	
				 | 
					
					
						|||
| 
						
						
							
						
						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
	
				 | 
					
					
						
@@ -1,3 +1,4 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "appHostPath": "../DysonNetwork.Control/DysonNetwork.Control.csproj"
 | 
					  "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
 | 
				
			||||||
							
								
								
									
										73
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										73
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
name: Aspire Publish Workflow
 | 
					name: Build and Push Microservices
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
@@ -7,19 +7,40 @@ on:
 | 
				
			|||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  publish:
 | 
					  build-and-push:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    permissions:
 | 
					    permissions:
 | 
				
			||||||
      contents: read
 | 
					      contents: read
 | 
				
			||||||
      packages: write
 | 
					      packages: write
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        include:
 | 
				
			||||||
 | 
					          - service: Sphere
 | 
				
			||||||
 | 
					            image: sphere
 | 
				
			||||||
 | 
					          - service: Pass
 | 
				
			||||||
 | 
					            image: pass
 | 
				
			||||||
 | 
					          - service: Ring
 | 
				
			||||||
 | 
					            image: ring
 | 
				
			||||||
 | 
					          - service: Drive
 | 
				
			||||||
 | 
					            image: drive
 | 
				
			||||||
 | 
					          - service: Develop
 | 
				
			||||||
 | 
					            image: develop
 | 
				
			||||||
 | 
					          - service: Gateway
 | 
				
			||||||
 | 
					            image: gateway
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - name: Checkout repository
 | 
					      - name: Checkout repository
 | 
				
			||||||
        uses: actions/checkout@v3
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Setup .NET
 | 
					 | 
				
			||||||
        uses: actions/setup-dotnet@v3
 | 
					 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          dotnet-version: "9.0.x"
 | 
					          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
 | 
					      - name: Log in to GitHub Container Registry
 | 
				
			||||||
        uses: docker/login-action@v3
 | 
					        uses: docker/login-action@v3
 | 
				
			||||||
@@ -28,33 +49,13 @@ jobs:
 | 
				
			|||||||
          username: ${{ github.actor }}
 | 
					          username: ${{ github.actor }}
 | 
				
			||||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
					          password: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Install Aspire CLI
 | 
					      - name: Build and push Docker image for ${{ matrix.service }}
 | 
				
			||||||
        run: dotnet tool install -g Aspire.Cli --prerelease
 | 
					        uses: docker/build-push-action@v6
 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Build and Publish Aspire Application
 | 
					 | 
				
			||||||
        run: aspire publish --project ./DysonNetwork.Control/DysonNetwork.Control.csproj --output publish
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Tag and Push Images
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          IMAGES=( "sphere" "pass" "ring" "drive" "develop" )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          for image in "${IMAGES[@]}"; do
 | 
					 | 
				
			||||||
            IMAGE_NAME="ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-$image:alpha"
 | 
					 | 
				
			||||||
            SOURCE_IMAGE_NAME="$image:latest" # Aspire's default local image name
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            echo "Tagging and pushing $SOURCE_IMAGE_NAME to $IMAGE_NAME..."
 | 
					 | 
				
			||||||
            docker tag $SOURCE_IMAGE_NAME $IMAGE_NAME
 | 
					 | 
				
			||||||
            docker push $IMAGE_NAME
 | 
					 | 
				
			||||||
          done
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      - name: Upload Aspire Publish Directory
 | 
					 | 
				
			||||||
        uses: actions/upload-artifact@v3
 | 
					 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: aspire-publish-output
 | 
					          context: .
 | 
				
			||||||
          path: ./publish/
 | 
					          file: DysonNetwork.${{ matrix.service }}/Dockerfile
 | 
				
			||||||
 | 
					          push: true
 | 
				
			||||||
      - name: Upload Docker Compose file
 | 
					          tags: |
 | 
				
			||||||
        uses: actions/upload-artifact@v3
 | 
					            ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:${{ steps.nbgv.outputs.SimpleVersion }}
 | 
				
			||||||
        with:
 | 
					            ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:latest
 | 
				
			||||||
          name: docker-compose-output
 | 
					          platforms: linux/amd64
 | 
				
			||||||
          path: ./publish/docker-compose.yml
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -6,3 +6,4 @@ riderModule.iml
 | 
				
			|||||||
/_ReSharper.Caches/
 | 
					/_ReSharper.Caches/
 | 
				
			||||||
.idea
 | 
					.idea
 | 
				
			||||||
.DS_Store
 | 
					.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
 | 
				
			||||||
@@ -1,66 +1,65 @@
 | 
				
			|||||||
using Aspire.Hosting.Yarp.Transforms;
 | 
					using Microsoft.Extensions.Hosting;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var builder = DistributedApplication.CreateBuilder(args);
 | 
					var builder = DistributedApplication.CreateBuilder(args);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Database was configured separately in each service.
 | 
					var isDev = builder.Environment.IsDevelopment();
 | 
				
			||||||
// var database = builder.AddPostgres("database");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
var cache = builder.AddRedis("cache");
 | 
					var cache = builder.AddRedis("cache");
 | 
				
			||||||
var queue = builder.AddNats("queue").WithJetStream();
 | 
					var queue = builder.AddNats("queue").WithJetStream();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring")
 | 
					var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring");
 | 
				
			||||||
    .WithReference(queue);
 | 
					 | 
				
			||||||
var passService = builder.AddProject<Projects.DysonNetwork_Pass>("pass")
 | 
					var passService = builder.AddProject<Projects.DysonNetwork_Pass>("pass")
 | 
				
			||||||
    .WithReference(cache)
 | 
					 | 
				
			||||||
    .WithReference(queue)
 | 
					 | 
				
			||||||
    .WithReference(ringService);
 | 
					    .WithReference(ringService);
 | 
				
			||||||
var driveService = builder.AddProject<Projects.DysonNetwork_Drive>("drive")
 | 
					var driveService = builder.AddProject<Projects.DysonNetwork_Drive>("drive")
 | 
				
			||||||
    .WithReference(cache)
 | 
					 | 
				
			||||||
    .WithReference(queue)
 | 
					 | 
				
			||||||
    .WithReference(passService)
 | 
					    .WithReference(passService)
 | 
				
			||||||
    .WithReference(ringService);
 | 
					    .WithReference(ringService);
 | 
				
			||||||
var sphereService = builder.AddProject<Projects.DysonNetwork_Sphere>("sphere")
 | 
					var sphereService = builder.AddProject<Projects.DysonNetwork_Sphere>("sphere")
 | 
				
			||||||
    .WithReference(cache)
 | 
					 | 
				
			||||||
    .WithReference(queue)
 | 
					 | 
				
			||||||
    .WithReference(passService)
 | 
					    .WithReference(passService)
 | 
				
			||||||
    .WithReference(ringService);
 | 
					    .WithReference(ringService)
 | 
				
			||||||
 | 
					    .WithReference(driveService);
 | 
				
			||||||
var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop")
 | 
					var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop")
 | 
				
			||||||
    .WithReference(cache)
 | 
					 | 
				
			||||||
    .WithReference(passService)
 | 
					    .WithReference(passService)
 | 
				
			||||||
    .WithReference(ringService);
 | 
					    .WithReference(ringService)
 | 
				
			||||||
 | 
					    .WithReference(sphereService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					passService.WithReference(developService).WithReference(driveService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					List<IResourceBuilder<ProjectResource>> services =
 | 
				
			||||||
 | 
					    [ringService, passService, driveService, sphereService, developService];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
					// Extra double-ended references
 | 
				
			||||||
ringService.WithReference(passService);
 | 
					ringService.WithReference(passService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
builder.AddYarp("gateway")
 | 
					var gateway = builder.AddProject<Projects.DysonNetwork_Gateway>("gateway")
 | 
				
			||||||
    .WithHostPort(5000)
 | 
					    .WithEnvironment("HTTP_PORTS", "5001")
 | 
				
			||||||
    .WithConfiguration(yarp =>
 | 
					    .WithHttpEndpoint(port: 5001, targetPort: null, isProxied: false, name: "http");
 | 
				
			||||||
    {
 | 
					
 | 
				
			||||||
        var ringCluster = yarp.AddCluster(ringService.GetEndpoint("http"));
 | 
					foreach (var service in services)
 | 
				
			||||||
        yarp.AddRoute("/ws", ringCluster);
 | 
					    gateway.WithReference(service);
 | 
				
			||||||
        yarp.AddRoute("/ring/{**catch-all}", ringCluster)
 | 
					 | 
				
			||||||
            .WithTransformPathRemovePrefix("/ring")
 | 
					 | 
				
			||||||
            .WithTransformPathPrefix("/api");
 | 
					 | 
				
			||||||
        var passCluster = yarp.AddCluster(passService.GetEndpoint("http"));
 | 
					 | 
				
			||||||
        yarp.AddRoute("/.well-known/openid-configuration", passCluster);
 | 
					 | 
				
			||||||
        yarp.AddRoute("/.well-known/jwks", passCluster);
 | 
					 | 
				
			||||||
        yarp.AddRoute("/id/{**catch-all}", passCluster)
 | 
					 | 
				
			||||||
            .WithTransformPathRemovePrefix("/id")
 | 
					 | 
				
			||||||
            .WithTransformPathPrefix("/api");
 | 
					 | 
				
			||||||
        var driveCluster = yarp.AddCluster(driveService.GetEndpoint("http"));
 | 
					 | 
				
			||||||
        yarp.AddRoute("/api/tus", driveCluster);
 | 
					 | 
				
			||||||
        yarp.AddRoute("/drive/{**catch-all}", driveCluster)
 | 
					 | 
				
			||||||
            .WithTransformPathRemovePrefix("/drive")
 | 
					 | 
				
			||||||
            .WithTransformPathPrefix("/api");
 | 
					 | 
				
			||||||
        var sphereCluster = yarp.AddCluster(sphereService.GetEndpoint("http"));
 | 
					 | 
				
			||||||
        yarp.AddRoute("/sphere/{**catch-all}", sphereCluster)
 | 
					 | 
				
			||||||
            .WithTransformPathRemovePrefix("/sphere")
 | 
					 | 
				
			||||||
            .WithTransformPathPrefix("/api");
 | 
					 | 
				
			||||||
        var developCluster = yarp.AddCluster(developService.GetEndpoint("http"));
 | 
					 | 
				
			||||||
        yarp.AddRoute("/develop/{**catch-all}", developCluster)
 | 
					 | 
				
			||||||
            .WithTransformPathRemovePrefix("/develop")
 | 
					 | 
				
			||||||
            .WithTransformPathPrefix("/api");
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
builder.AddDockerComposeEnvironment("docker-compose");
 | 
					builder.AddDockerComposeEnvironment("docker-compose");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,5 @@
 | 
				
			|||||||
<Project Sdk="Microsoft.NET.Sdk">
 | 
					<Project Sdk="Microsoft.NET.Sdk">
 | 
				
			||||||
 | 
					  <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.1" />
 | 
				
			||||||
    <Sdk Name="Aspire.AppHost.Sdk" Version="9.4.2"/>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <PropertyGroup>
 | 
					  <PropertyGroup>
 | 
				
			||||||
    <OutputType>Exe</OutputType>
 | 
					    <OutputType>Exe</OutputType>
 | 
				
			||||||
    <TargetFramework>net9.0</TargetFramework>
 | 
					    <TargetFramework>net9.0</TargetFramework>
 | 
				
			||||||
@@ -10,22 +8,18 @@
 | 
				
			|||||||
    <UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId>
 | 
					    <UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId>
 | 
				
			||||||
    <RootNamespace>DysonNetwork.Control</RootNamespace>
 | 
					    <RootNamespace>DysonNetwork.Control</RootNamespace>
 | 
				
			||||||
  </PropertyGroup>
 | 
					  </PropertyGroup>
 | 
				
			||||||
 | 
					 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
        <PackageReference Include="Aspire.Hosting.AppHost" Version="9.4.2"/>
 | 
					    <PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.1" />
 | 
				
			||||||
    <PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" />
 | 
					    <PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" />
 | 
				
			||||||
        <PackageReference Include="Aspire.Hosting.Nats" Version="9.4.2" />
 | 
					    <PackageReference Include="Aspire.Hosting.Nats" Version="9.5.1" />
 | 
				
			||||||
        <PackageReference Include="Aspire.Hosting.Redis" Version="9.4.2" />
 | 
					    <PackageReference Include="Aspire.Hosting.Redis" Version="9.5.1" />
 | 
				
			||||||
        <PackageReference Include="Aspire.Hosting.Yarp" Version="9.4.2-preview.1.25428.12" />
 | 
					 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
    <ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" />
 | 
					    <ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" />
 | 
				
			||||||
    <ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" />
 | 
					    <ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" />
 | 
				
			||||||
    <ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" />
 | 
					    <ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" />
 | 
				
			||||||
    <ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" />
 | 
					    <ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" />
 | 
				
			||||||
      <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
 | 
					 | 
				
			||||||
    <ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" />
 | 
					    <ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" />
 | 
				
			||||||
 | 
					    <ProjectReference Include="..\DysonNetwork.Gateway\DysonNetwork.Gateway.csproj" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					 | 
				
			||||||
</Project>
 | 
					</Project>
 | 
				
			||||||
@@ -10,7 +10,9 @@
 | 
				
			|||||||
        "ASPNETCORE_ENVIRONMENT": "Development",
 | 
					        "ASPNETCORE_ENVIRONMENT": "Development",
 | 
				
			||||||
        "DOTNET_ENVIRONMENT": "Development",
 | 
					        "DOTNET_ENVIRONMENT": "Development",
 | 
				
			||||||
        "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21175",
 | 
					        "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21175",
 | 
				
			||||||
        "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22189"
 | 
					        "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": {
 | 
					    "http": {
 | 
				
			||||||
@@ -22,7 +24,8 @@
 | 
				
			|||||||
        "ASPNETCORE_ENVIRONMENT": "Development",
 | 
					        "ASPNETCORE_ENVIRONMENT": "Development",
 | 
				
			||||||
        "DOTNET_ENVIRONMENT": "Development",
 | 
					        "DOTNET_ENVIRONMENT": "Development",
 | 
				
			||||||
        "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163",
 | 
					        "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163",
 | 
				
			||||||
        "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185"
 | 
					        "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185",
 | 
				
			||||||
 | 
					        "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:22108"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,4 @@
 | 
				
			|||||||
using System.Text.Json;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Develop.Identity;
 | 
					 | 
				
			||||||
using DysonNetwork.Develop.Project;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Design;
 | 
					using Microsoft.EntityFrameworkCore.Design;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -11,13 +9,13 @@ public class AppDatabase(
 | 
				
			|||||||
    IConfiguration configuration
 | 
					    IConfiguration configuration
 | 
				
			||||||
) : DbContext(options)
 | 
					) : 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<SnCustomApp> CustomApps { get; set; } = null!;
 | 
				
			||||||
    public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } = null!;
 | 
					    public DbSet<SnCustomAppSecret> CustomAppSecrets { get; set; } = null!;
 | 
				
			||||||
    public DbSet<BotAccount> BotAccounts { get; set; } = null!;
 | 
					    public DbSet<SnBotAccount> BotAccounts { get; set; } = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
 | 
					    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@
 | 
				
			|||||||
        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
 | 
					        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
 | 
				
			||||||
        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
 | 
					        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
 | 
				
			||||||
        <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
 | 
					        <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
 | 
				
			||||||
        <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/>
 | 
					        <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
 | 
				
			||||||
        <PackageReference Include="NodaTime" Version="3.2.2"/>
 | 
					        <PackageReference Include="NodaTime" Version="3.2.2"/>
 | 
				
			||||||
        <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
 | 
					        <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
 | 
				
			||||||
        <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
 | 
					        <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
 | 
				
			||||||
@@ -31,7 +31,6 @@
 | 
				
			|||||||
    </ItemGroup>
 | 
					    </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
    <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" />
 | 
					 | 
				
			||||||
    <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
 | 
					    <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
using System.ComponentModel.DataAnnotations;
 | 
					using System.ComponentModel.DataAnnotations;
 | 
				
			||||||
using DysonNetwork.Develop.Project;
 | 
					using DysonNetwork.Develop.Project;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Proto;
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
using DysonNetwork.Shared.Registry;
 | 
					using DysonNetwork.Shared.Registry;
 | 
				
			||||||
using Grpc.Core;
 | 
					using Grpc.Core;
 | 
				
			||||||
@@ -16,7 +16,7 @@ namespace DysonNetwork.Develop.Identity;
 | 
				
			|||||||
[Authorize]
 | 
					[Authorize]
 | 
				
			||||||
public class BotAccountController(
 | 
					public class BotAccountController(
 | 
				
			||||||
    BotAccountService botService,
 | 
					    BotAccountService botService,
 | 
				
			||||||
    DeveloperService developerService,
 | 
					    DeveloperService ds,
 | 
				
			||||||
    DevProjectService projectService,
 | 
					    DevProjectService projectService,
 | 
				
			||||||
    ILogger<BotAccountController> logger,
 | 
					    ILogger<BotAccountController> logger,
 | 
				
			||||||
    AccountClientHelper accounts,
 | 
					    AccountClientHelper accounts,
 | 
				
			||||||
@@ -83,12 +83,12 @@ public class BotAccountController(
 | 
				
			|||||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
				
			||||||
            return Unauthorized();
 | 
					            return Unauthorized();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
					        var developer = await ds.GetDeveloperByName(pubName);
 | 
				
			||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            return NotFound("Developer not found");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
					        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
				
			||||||
                PublisherMemberRole.Viewer))
 | 
					                Shared.Proto.PublisherMemberRole.Viewer))
 | 
				
			||||||
            return StatusCode(403, "You must be an viewer of the developer to list bots");
 | 
					            return StatusCode(403, "You must be an viewer of the developer to list bots");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -108,12 +108,12 @@ public class BotAccountController(
 | 
				
			|||||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
				
			||||||
            return Unauthorized();
 | 
					            return Unauthorized();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
					        var developer = await ds.GetDeveloperByName(pubName);
 | 
				
			||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            return NotFound("Developer not found");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
					        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
				
			||||||
                PublisherMemberRole.Viewer))
 | 
					                Shared.Proto.PublisherMemberRole.Viewer))
 | 
				
			||||||
            return StatusCode(403, "You must be an viewer of the developer to view bot details");
 | 
					            return StatusCode(403, "You must be an viewer of the developer to view bot details");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -137,12 +137,12 @@ public class BotAccountController(
 | 
				
			|||||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
				
			||||||
            return Unauthorized();
 | 
					            return Unauthorized();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
					        var developer = await ds.GetDeveloperByName(pubName);
 | 
				
			||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            return NotFound("Developer not found");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
					        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
				
			||||||
                PublisherMemberRole.Editor))
 | 
					                Shared.Proto.PublisherMemberRole.Editor))
 | 
				
			||||||
            return StatusCode(403, "You must be an editor of the developer to create a bot");
 | 
					            return StatusCode(403, "You must be an editor of the developer to create a bot");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -206,12 +206,12 @@ public class BotAccountController(
 | 
				
			|||||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
				
			||||||
            return Unauthorized();
 | 
					            return Unauthorized();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
					        var developer = await ds.GetDeveloperByName(pubName);
 | 
				
			||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            return NotFound("Developer not found");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
					        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
				
			||||||
                PublisherMemberRole.Editor))
 | 
					                Shared.Proto.PublisherMemberRole.Editor))
 | 
				
			||||||
            return StatusCode(403, "You must be an editor of the developer to update a bot");
 | 
					            return StatusCode(403, "You must be an editor of the developer to update a bot");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -267,12 +267,12 @@ public class BotAccountController(
 | 
				
			|||||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
				
			||||||
            return Unauthorized();
 | 
					            return Unauthorized();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
					        var developer = await ds.GetDeveloperByName(pubName);
 | 
				
			||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            return NotFound("Developer not found");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
					        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
				
			||||||
                PublisherMemberRole.Editor))
 | 
					                Shared.Proto.PublisherMemberRole.Editor))
 | 
				
			||||||
            return StatusCode(403, "You must be an editor of the developer to delete a bot");
 | 
					            return StatusCode(403, "You must be an editor of the developer to delete a bot");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -296,7 +296,7 @@ public class BotAccountController(
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [HttpGet("{botId:guid}/keys")]
 | 
					    [HttpGet("{botId:guid}/keys")]
 | 
				
			||||||
    public async Task<ActionResult<List<ApiKeyReference>>> ListBotKeys(
 | 
					    public async Task<ActionResult<List<SnApiKey>>> ListBotKeys(
 | 
				
			||||||
        [FromRoute] string pubName,
 | 
					        [FromRoute] string pubName,
 | 
				
			||||||
        [FromRoute] Guid projectId,
 | 
					        [FromRoute] Guid projectId,
 | 
				
			||||||
        [FromRoute] Guid botId
 | 
					        [FromRoute] Guid botId
 | 
				
			||||||
@@ -305,7 +305,7 @@ public class BotAccountController(
 | 
				
			|||||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
				
			||||||
            return Unauthorized();
 | 
					            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 (developer == null) return NotFound("Developer not found");
 | 
				
			||||||
        if (project == null) return NotFound("Project not found or you don't have access");
 | 
					        if (project == null) return NotFound("Project not found or you don't have access");
 | 
				
			||||||
        if (bot == null) return NotFound("Bot not found");
 | 
					        if (bot == null) return NotFound("Bot not found");
 | 
				
			||||||
@@ -314,13 +314,13 @@ public class BotAccountController(
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            AutomatedId = bot.Id.ToString()
 | 
					            AutomatedId = bot.Id.ToString()
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        var data = keys.Data.Select(ApiKeyReference.FromProtoValue).ToList();
 | 
					        var data = keys.Data.Select(SnApiKey.FromProtoValue).ToList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Ok(data);
 | 
					        return Ok(data);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [HttpGet("{botId:guid}/keys/{keyId:guid}")]
 | 
					    [HttpGet("{botId:guid}/keys/{keyId:guid}")]
 | 
				
			||||||
    public async Task<ActionResult<ApiKeyReference>> GetBotKey(
 | 
					    public async Task<ActionResult<SnApiKey>> GetBotKey(
 | 
				
			||||||
        [FromRoute] string pubName,
 | 
					        [FromRoute] string pubName,
 | 
				
			||||||
        [FromRoute] Guid projectId,
 | 
					        [FromRoute] Guid projectId,
 | 
				
			||||||
        [FromRoute] Guid botId,
 | 
					        [FromRoute] Guid botId,
 | 
				
			||||||
@@ -329,7 +329,7 @@ public class BotAccountController(
 | 
				
			|||||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
				
			||||||
            return Unauthorized();
 | 
					            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 (developer == null) return NotFound("Developer not found");
 | 
				
			||||||
        if (project == null) return NotFound("Project not found or you don't have access");
 | 
					        if (project == null) return NotFound("Project not found or you don't have access");
 | 
				
			||||||
        if (bot == null) return NotFound("Bot not found");
 | 
					        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() });
 | 
					            var key = await accountsReceiver.GetApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
 | 
				
			||||||
            if (key == null) return NotFound("API key not found");
 | 
					            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)
 | 
					        catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -353,7 +353,7 @@ public class BotAccountController(
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [HttpPost("{botId:guid}/keys")]
 | 
					    [HttpPost("{botId:guid}/keys")]
 | 
				
			||||||
    public async Task<ActionResult<ApiKeyReference>> CreateBotKey(
 | 
					    public async Task<ActionResult<SnApiKey>> CreateBotKey(
 | 
				
			||||||
        [FromRoute] string pubName,
 | 
					        [FromRoute] string pubName,
 | 
				
			||||||
        [FromRoute] Guid projectId,
 | 
					        [FromRoute] Guid projectId,
 | 
				
			||||||
        [FromRoute] Guid botId,
 | 
					        [FromRoute] Guid botId,
 | 
				
			||||||
@@ -362,7 +362,7 @@ public class BotAccountController(
 | 
				
			|||||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
				
			||||||
            return Unauthorized();
 | 
					            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 (developer == null) return NotFound("Developer not found");
 | 
				
			||||||
        if (project == null) return NotFound("Project not found or you don't have access");
 | 
					        if (project == null) return NotFound("Project not found or you don't have access");
 | 
				
			||||||
        if (bot == null) return NotFound("Bot not found");
 | 
					        if (bot == null) return NotFound("Bot not found");
 | 
				
			||||||
@@ -376,7 +376,7 @@ public class BotAccountController(
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var createdKey = await accountsReceiver.CreateApiKeyAsync(newKey);
 | 
					            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)
 | 
					        catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -385,7 +385,7 @@ public class BotAccountController(
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [HttpPost("{botId:guid}/keys/{keyId:guid}/rotate")]
 | 
					    [HttpPost("{botId:guid}/keys/{keyId:guid}/rotate")]
 | 
				
			||||||
    public async Task<ActionResult<ApiKeyReference>> RotateBotKey(
 | 
					    public async Task<ActionResult<SnApiKey>> RotateBotKey(
 | 
				
			||||||
        [FromRoute] string pubName,
 | 
					        [FromRoute] string pubName,
 | 
				
			||||||
        [FromRoute] Guid projectId,
 | 
					        [FromRoute] Guid projectId,
 | 
				
			||||||
        [FromRoute] Guid botId,
 | 
					        [FromRoute] Guid botId,
 | 
				
			||||||
@@ -394,7 +394,7 @@ public class BotAccountController(
 | 
				
			|||||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
				
			||||||
            return Unauthorized();
 | 
					            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 (developer == null) return NotFound("Developer not found");
 | 
				
			||||||
        if (project == null) return NotFound("Project not found or you don't have access");
 | 
					        if (project == null) return NotFound("Project not found or you don't have access");
 | 
				
			||||||
        if (bot == null) return NotFound("Bot not found");
 | 
					        if (bot == null) return NotFound("Bot not found");
 | 
				
			||||||
@@ -402,7 +402,7 @@ public class BotAccountController(
 | 
				
			|||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var rotatedKey = await accountsReceiver.RotateApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
 | 
					            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)
 | 
					        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)
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
				
			||||||
            return Unauthorized();
 | 
					            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 (developer == null) return NotFound("Developer not found");
 | 
				
			||||||
        if (project == null) return NotFound("Project not found or you don't have access");
 | 
					        if (project == null) return NotFound("Project not found or you don't have access");
 | 
				
			||||||
        if (bot == null) return NotFound("Bot not found");
 | 
					        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,
 | 
					        string pubName,
 | 
				
			||||||
        Guid projectId,
 | 
					        Guid projectId,
 | 
				
			||||||
        Guid botId,
 | 
					        Guid botId,
 | 
				
			||||||
        Account currentUser,
 | 
					        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 (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);
 | 
					            return (null, null, null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using Microsoft.AspNetCore.Mvc;
 | 
					using Microsoft.AspNetCore.Mvc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace DysonNetwork.Develop.Identity;
 | 
					namespace DysonNetwork.Develop.Identity;
 | 
				
			||||||
@@ -7,7 +8,7 @@ namespace DysonNetwork.Develop.Identity;
 | 
				
			|||||||
public class BotAccountPublicController(BotAccountService botService, DeveloperService developerService) : ControllerBase
 | 
					public class BotAccountPublicController(BotAccountService botService, DeveloperService developerService) : ControllerBase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    [HttpGet("{botId:guid}")]
 | 
					    [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);
 | 
					        var bot = await botService.GetBotByIdAsync(botId);
 | 
				
			||||||
        if (bot is null) return NotFound("Bot not found");
 | 
					        if (bot is null) return NotFound("Bot not found");
 | 
				
			||||||
@@ -21,7 +22,7 @@ public class BotAccountPublicController(BotAccountService botService, DeveloperS
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [HttpGet("{botId:guid}/developer")]
 | 
					    [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);
 | 
					        var bot = await botService.GetBotByIdAsync(botId);
 | 
				
			||||||
        if (bot is null) return NotFound("Bot not found");
 | 
					        if (bot is null) return NotFound("Bot not found");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
using DysonNetwork.Develop.Project;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using DysonNetwork.Shared.Proto;
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
using DysonNetwork.Shared.Registry;
 | 
					using DysonNetwork.Shared.Registry;
 | 
				
			||||||
using Grpc.Core;
 | 
					using Grpc.Core;
 | 
				
			||||||
@@ -14,22 +13,22 @@ public class BotAccountService(
 | 
				
			|||||||
    AccountClientHelper accounts
 | 
					    AccountClientHelper accounts
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public async Task<BotAccount?> GetBotByIdAsync(Guid id)
 | 
					    public async Task<SnBotAccount?> GetBotByIdAsync(Guid id)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return await db.BotAccounts
 | 
					        return await db.BotAccounts
 | 
				
			||||||
            .Include(b => b.Project)
 | 
					            .Include(b => b.Project)
 | 
				
			||||||
            .FirstOrDefaultAsync(b => b.Id == id);
 | 
					            .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
 | 
					        return await db.BotAccounts
 | 
				
			||||||
            .Where(b => b.ProjectId == projectId)
 | 
					            .Where(b => b.ProjectId == projectId)
 | 
				
			||||||
            .ToListAsync();
 | 
					            .ToListAsync();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task<BotAccount> CreateBotAsync(
 | 
					    public async Task<SnBotAccount> CreateBotAsync(
 | 
				
			||||||
        DevProject project,
 | 
					        SnDevProject project,
 | 
				
			||||||
        string slug,
 | 
					        string slug,
 | 
				
			||||||
        Account account,
 | 
					        Account account,
 | 
				
			||||||
        string? pictureId,
 | 
					        string? pictureId,
 | 
				
			||||||
@@ -58,7 +57,7 @@ public class BotAccountService(
 | 
				
			|||||||
            var botAccount = createResponse.Bot;
 | 
					            var botAccount = createResponse.Bot;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Then create the local bot account
 | 
					            // Then create the local bot account
 | 
				
			||||||
            var bot = new BotAccount
 | 
					            var bot = new SnBotAccount
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Id = automatedId,
 | 
					                Id = automatedId,
 | 
				
			||||||
                Slug = slug,
 | 
					                Slug = slug,
 | 
				
			||||||
@@ -89,8 +88,8 @@ public class BotAccountService(
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task<BotAccount> UpdateBotAsync(
 | 
					    public async Task<SnBotAccount> UpdateBotAsync(
 | 
				
			||||||
        BotAccount bot,
 | 
					        SnBotAccount bot,
 | 
				
			||||||
        Account account,
 | 
					        Account account,
 | 
				
			||||||
        string? pictureId,
 | 
					        string? pictureId,
 | 
				
			||||||
        string? backgroundId
 | 
					        string? backgroundId
 | 
				
			||||||
@@ -130,7 +129,7 @@ public class BotAccountService(
 | 
				
			|||||||
        return bot;
 | 
					        return bot;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task DeleteBotAsync(BotAccount bot)
 | 
					    public async Task DeleteBotAsync(SnBotAccount bot)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -153,22 +152,21 @@ public class BotAccountService(
 | 
				
			|||||||
        await db.SaveChangesAsync();
 | 
					        await db.SaveChangesAsync();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task<BotAccount?> LoadBotAccountAsync(BotAccount bot) =>
 | 
					    public async Task<SnBotAccount?> LoadBotAccountAsync(SnBotAccount bot) =>
 | 
				
			||||||
        (await LoadBotsAccountAsync([bot])).FirstOrDefault();
 | 
					        (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 automatedIds = bots.Select(b => b.Id).ToList();
 | 
				
			||||||
        var data = await accounts.GetBotAccountBatch(automatedIds);
 | 
					        var data = await accounts.GetBotAccountBatch(automatedIds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        foreach (var bot in bots)
 | 
					        foreach (var bot in bots)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            bot.Account = data
 | 
					            bot.Account = data
 | 
				
			||||||
                .Select(AccountReference.FromProtoValue)
 | 
					                .Select(SnAccount.FromProtoValue)
 | 
				
			||||||
                .FirstOrDefault(e => e.AutomatedId == bot.Id);
 | 
					                .FirstOrDefault(e => e.AutomatedId == bot.Id);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return bots as List<BotAccount> ?? [];
 | 
					        return bots;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
using System.ComponentModel.DataAnnotations;
 | 
					using System.ComponentModel.DataAnnotations;
 | 
				
			||||||
using DysonNetwork.Develop.Project;
 | 
					using DysonNetwork.Develop.Project;
 | 
				
			||||||
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Proto;
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
using Microsoft.AspNetCore.Authorization;
 | 
					using Microsoft.AspNetCore.Authorization;
 | 
				
			||||||
using Microsoft.AspNetCore.Mvc;
 | 
					using Microsoft.AspNetCore.Mvc;
 | 
				
			||||||
@@ -18,9 +19,9 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
        [MaxLength(4096)] string? Description,
 | 
					        [MaxLength(4096)] string? Description,
 | 
				
			||||||
        string? PictureId,
 | 
					        string? PictureId,
 | 
				
			||||||
        string? BackgroundId,
 | 
					        string? BackgroundId,
 | 
				
			||||||
        CustomAppStatus? Status,
 | 
					        Shared.Models.CustomAppStatus? Status,
 | 
				
			||||||
        CustomAppLinks? Links,
 | 
					        SnCustomAppLinks? Links,
 | 
				
			||||||
        CustomAppOauthConfig? OauthConfig
 | 
					        SnCustomAppOauthConfig? OauthConfig
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public record CreateSecretRequest(
 | 
					    public record CreateSecretRequest(
 | 
				
			||||||
@@ -50,7 +51,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
        if (developer is null) return NotFound();
 | 
					        if (developer is null) return NotFound();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
					        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");
 | 
					            return StatusCode(403, "You must be a viewer of the developer to list custom apps");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        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();
 | 
					        if (developer is null) return NotFound();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
					        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");
 | 
					            return StatusCode(403, "You must be a viewer of the developer to list custom apps");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -99,7 +100,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            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");
 | 
					            return StatusCode(403, "You must be an editor of the developer to create a custom app");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -143,7 +144,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            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");
 | 
					            return StatusCode(403, "You must be an editor of the developer to update a custom app");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -180,7 +181,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            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");
 | 
					            return StatusCode(403, "You must be an editor of the developer to delete a custom app");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -212,7 +213,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            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");
 | 
					            return StatusCode(403, "You must be an editor of the developer to view app secrets");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -250,7 +251,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            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");
 | 
					            return StatusCode(403, "You must be an editor of the developer to create app secrets");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -263,7 +264,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var secret = await customApps.CreateAppSecretAsync(new CustomAppSecret
 | 
					            var secret = await customApps.CreateAppSecretAsync(new SnCustomAppSecret
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                AppId = appId,
 | 
					                AppId = appId,
 | 
				
			||||||
                Description = request.Description,
 | 
					                Description = request.Description,
 | 
				
			||||||
@@ -309,7 +310,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            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");
 | 
					            return StatusCode(403, "You must be an editor of the developer to view app secrets");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -350,7 +351,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            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");
 | 
					            return StatusCode(403, "You must be an editor of the developer to delete app secrets");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -388,7 +389,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
        if (developer is null)
 | 
					        if (developer is null)
 | 
				
			||||||
            return NotFound("Developer not found");
 | 
					            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");
 | 
					            return StatusCode(403, "You must be an editor of the developer to rotate app secrets");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
					        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
				
			||||||
@@ -401,7 +402,7 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var secret = await customApps.RotateAppSecretAsync(new CustomAppSecret
 | 
					            var secret = await customApps.RotateAppSecretAsync(new SnCustomAppSecret
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Id = secretId,
 | 
					                Id = secretId,
 | 
				
			||||||
                AppId = appId,
 | 
					                AppId = appId,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
using DysonNetwork.Develop.Project;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using DysonNetwork.Shared.Proto;
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using System.Security.Cryptography;
 | 
					using System.Security.Cryptography;
 | 
				
			||||||
@@ -13,7 +12,7 @@ public class CustomAppService(
 | 
				
			|||||||
    FileService.FileServiceClient files
 | 
					    FileService.FileServiceClient files
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public async Task<CustomApp?> CreateAppAsync(
 | 
					    public async Task<SnCustomApp?> CreateAppAsync(
 | 
				
			||||||
        Guid projectId,
 | 
					        Guid projectId,
 | 
				
			||||||
        CustomAppController.CustomAppRequest request
 | 
					        CustomAppController.CustomAppRequest request
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@@ -25,12 +24,12 @@ public class CustomAppService(
 | 
				
			|||||||
        if (project == null)
 | 
					        if (project == null)
 | 
				
			||||||
            return null;
 | 
					            return null;
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
        var app = new CustomApp
 | 
					        var app = new SnCustomApp
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Slug = request.Slug!,
 | 
					            Slug = request.Slug!,
 | 
				
			||||||
            Name = request.Name!,
 | 
					            Name = request.Name!,
 | 
				
			||||||
            Description = request.Description,
 | 
					            Description = request.Description,
 | 
				
			||||||
            Status = request.Status ?? CustomAppStatus.Developing,
 | 
					            Status = request.Status ?? Shared.Models.CustomAppStatus.Developing,
 | 
				
			||||||
            Links = request.Links,
 | 
					            Links = request.Links,
 | 
				
			||||||
            OauthConfig = request.OauthConfig,
 | 
					            OauthConfig = request.OauthConfig,
 | 
				
			||||||
            ProjectId = projectId
 | 
					            ProjectId = projectId
 | 
				
			||||||
@@ -46,7 +45,7 @@ public class CustomAppService(
 | 
				
			|||||||
            );
 | 
					            );
 | 
				
			||||||
            if (picture is null)
 | 
					            if (picture is null)
 | 
				
			||||||
                throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
 | 
					                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
 | 
					            // Create a new reference
 | 
				
			||||||
            await fileRefs.CreateReferenceAsync(
 | 
					            await fileRefs.CreateReferenceAsync(
 | 
				
			||||||
@@ -65,7 +64,7 @@ public class CustomAppService(
 | 
				
			|||||||
            );
 | 
					            );
 | 
				
			||||||
            if (background is null)
 | 
					            if (background is null)
 | 
				
			||||||
                throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
 | 
					                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
 | 
					            // Create a new reference
 | 
				
			||||||
            await fileRefs.CreateReferenceAsync(
 | 
					            await fileRefs.CreateReferenceAsync(
 | 
				
			||||||
@@ -84,7 +83,7 @@ public class CustomAppService(
 | 
				
			|||||||
        return app;
 | 
					        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();
 | 
					        var query = db.CustomApps.AsQueryable();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
@@ -96,7 +95,7 @@ public class CustomAppService(
 | 
				
			|||||||
        return await query.FirstOrDefaultAsync(a => a.Id == id);
 | 
					        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
 | 
					        return await db.CustomAppSecrets
 | 
				
			||||||
            .Where(s => s.AppId == appId)
 | 
					            .Where(s => s.AppId == appId)
 | 
				
			||||||
@@ -104,13 +103,13 @@ public class CustomAppService(
 | 
				
			|||||||
            .ToListAsync();
 | 
					            .ToListAsync();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task<CustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId)
 | 
					    public async Task<SnCustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return await db.CustomAppSecrets
 | 
					        return await db.CustomAppSecrets
 | 
				
			||||||
            .FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId);
 | 
					            .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))
 | 
					        if (string.IsNullOrWhiteSpace(secret.Secret))
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -141,7 +140,7 @@ public class CustomAppService(
 | 
				
			|||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task<CustomAppSecret> RotateAppSecretAsync(CustomAppSecret secretUpdate)
 | 
					    public async Task<SnCustomAppSecret> RotateAppSecretAsync(SnCustomAppSecret secretUpdate)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var existingSecret = await db.CustomAppSecrets
 | 
					        var existingSecret = await db.CustomAppSecrets
 | 
				
			||||||
            .FirstOrDefaultAsync(s => s.Id == secretUpdate.Id && s.AppId == secretUpdate.AppId);
 | 
					            .FirstOrDefaultAsync(s => s.Id == secretUpdate.Id && s.AppId == secretUpdate.AppId);
 | 
				
			||||||
@@ -177,14 +176,14 @@ public class CustomAppService(
 | 
				
			|||||||
        return res.ToString();
 | 
					        return res.ToString();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task<List<CustomApp>> GetAppsByProjectAsync(Guid projectId)
 | 
					    public async Task<List<SnCustomApp>> GetAppsByProjectAsync(Guid projectId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return await db.CustomApps
 | 
					        return await db.CustomApps
 | 
				
			||||||
            .Where(a => a.ProjectId == projectId)
 | 
					            .Where(a => a.ProjectId == projectId)
 | 
				
			||||||
            .ToListAsync();
 | 
					            .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)
 | 
					        if (request.Slug is not null)
 | 
				
			||||||
            app.Slug = request.Slug;
 | 
					            app.Slug = request.Slug;
 | 
				
			||||||
@@ -209,7 +208,7 @@ public class CustomAppService(
 | 
				
			|||||||
            );
 | 
					            );
 | 
				
			||||||
            if (picture is null)
 | 
					            if (picture is null)
 | 
				
			||||||
                throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
 | 
					                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
 | 
					            // Create a new reference
 | 
				
			||||||
            await fileRefs.CreateReferenceAsync(
 | 
					            await fileRefs.CreateReferenceAsync(
 | 
				
			||||||
@@ -228,7 +227,7 @@ public class CustomAppService(
 | 
				
			|||||||
            );
 | 
					            );
 | 
				
			||||||
            if (background is null)
 | 
					            if (background is null)
 | 
				
			||||||
                throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
 | 
					                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
 | 
					            // Create a new reference
 | 
				
			||||||
            await fileRefs.CreateReferenceAsync(
 | 
					            await fileRefs.CreateReferenceAsync(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Proto;
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
using Grpc.Core;
 | 
					using Grpc.Core;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
@@ -37,7 +38,7 @@ public class CustomAppServiceGrpc(AppDatabase db) : Shared.Proto.CustomAppServic
 | 
				
			|||||||
        if (string.IsNullOrEmpty(request.Secret))
 | 
					        if (string.IsNullOrEmpty(request.Secret))
 | 
				
			||||||
            throw new RpcException(new Status(StatusCode.InvalidArgument, "secret required"));
 | 
					            throw new RpcException(new Status(StatusCode.InvalidArgument, "secret required"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        IQueryable<CustomAppSecret> q = db.CustomAppSecrets;
 | 
					        IQueryable<SnCustomAppSecret> q = db.CustomAppSecrets;
 | 
				
			||||||
        switch (request.SecretIdentifierCase)
 | 
					        switch (request.SecretIdentifierCase)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.SecretId:
 | 
					            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.Auth;
 | 
				
			||||||
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Proto;
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
using Grpc.Core;
 | 
					using Grpc.Core;
 | 
				
			||||||
using Microsoft.AspNetCore.Authorization;
 | 
					using Microsoft.AspNetCore.Authorization;
 | 
				
			||||||
@@ -18,7 +19,7 @@ public class DeveloperController(
 | 
				
			|||||||
    : ControllerBase
 | 
					    : ControllerBase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    [HttpGet("{name}")]
 | 
					    [HttpGet("{name}")]
 | 
				
			||||||
    public async Task<ActionResult<Developer>> GetDeveloper(string name)
 | 
					    public async Task<ActionResult<SnDeveloper>> GetDeveloper(string name)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var developer = await ds.GetDeveloperByName(name);
 | 
					        var developer = await ds.GetDeveloperByName(name);
 | 
				
			||||||
        if (developer is null) return NotFound();
 | 
					        if (developer is null) return NotFound();
 | 
				
			||||||
@@ -47,10 +48,9 @@ public class DeveloperController(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    [HttpGet]
 | 
					    [HttpGet]
 | 
				
			||||||
    [Authorize]
 | 
					    [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();
 | 
					        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 pubResponse = await ps.ListPublishersAsync(new ListPublishersRequest { AccountId = currentUser.Id });
 | 
				
			||||||
        var pubIds = pubResponse.Publishers.Select(p => p.Id).Select(Guid.Parse).ToList();
 | 
					        var pubIds = pubResponse.Publishers.Select(p => p.Id).Select(Guid.Parse).ToList();
 | 
				
			||||||
@@ -70,16 +70,16 @@ public class DeveloperController(
 | 
				
			|||||||
    [HttpPost("{name}/enroll")]
 | 
					    [HttpPost("{name}/enroll")]
 | 
				
			||||||
    [Authorize]
 | 
					    [Authorize]
 | 
				
			||||||
    [RequiredPermission("global", "developers.create")]
 | 
					    [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();
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
				
			||||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
					        var accountId = Guid.Parse(currentUser.Id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        PublisherInfo? pub;
 | 
					        SnPublisher? pub;
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
 | 
					            var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
 | 
				
			||||||
            pub = PublisherInfo.FromProto(pubResponse.Publisher);
 | 
					            pub = SnPublisher.FromProto(pubResponse.Publisher);
 | 
				
			||||||
        } catch (RpcException ex)
 | 
					        } catch (RpcException ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            return NotFound(ex.Status.Detail);
 | 
					            return NotFound(ex.Status.Detail);
 | 
				
			||||||
@@ -90,14 +90,14 @@ public class DeveloperController(
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            PublisherId = pub.Id.ToString(),
 | 
					            PublisherId = pub.Id.ToString(),
 | 
				
			||||||
            AccountId = currentUser.Id,
 | 
					            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");
 | 
					        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);
 | 
					        var hasDeveloper = await db.Developers.AnyAsync(d => d.PublisherId == pub.Id);
 | 
				
			||||||
        if (hasDeveloper) return BadRequest("Publisher is already in the developer program");
 | 
					        if (hasDeveloper) return BadRequest("Publisher is already in the developer program");
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        var developer = new Developer
 | 
					        var developer = new SnDeveloper
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Id = Guid.NewGuid(),
 | 
					            Id = Guid.NewGuid(),
 | 
				
			||||||
            PublisherId = pub.Id
 | 
					            PublisherId = pub.Id
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Proto;
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
using Grpc.Core;
 | 
					using Grpc.Core;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
@@ -9,22 +10,22 @@ public class DeveloperService(
 | 
				
			|||||||
    PublisherService.PublisherServiceClient ps,
 | 
					    PublisherService.PublisherServiceClient ps,
 | 
				
			||||||
    ILogger<DeveloperService> logger)
 | 
					    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() });
 | 
					        var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() });
 | 
				
			||||||
        developer.Publisher = PublisherInfo.FromProto(pubResponse.Publisher);
 | 
					        developer.Publisher = SnPublisher.FromProto(pubResponse.Publisher);
 | 
				
			||||||
        return developer;
 | 
					        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 enumerable = developers.ToList();
 | 
				
			||||||
        var pubIds = enumerable.Select(d => d.PublisherId).ToList();
 | 
					        var pubIds = enumerable.Select(d => d.PublisherId).ToList();
 | 
				
			||||||
        var pubRequest = new GetPublisherBatchRequest();
 | 
					        var pubRequest = new GetPublisherBatchRequest();
 | 
				
			||||||
        pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString()));
 | 
					        pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString()));
 | 
				
			||||||
        var pubResponse = await ps.GetPublisherBatchAsync(pubRequest);
 | 
					        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.FromProto);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return enumerable.Select(d =>
 | 
					        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
 | 
					        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);
 | 
					        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
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,7 @@
 | 
				
			|||||||
// <auto-generated />
 | 
					// <auto-generated />
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using DysonNetwork.Develop;
 | 
					using DysonNetwork.Develop;
 | 
				
			||||||
using DysonNetwork.Develop.Identity;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
@@ -35,7 +34,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("uuid")
 | 
					                        .HasColumnType("uuid")
 | 
				
			||||||
                        .HasColumnName("id");
 | 
					                        .HasColumnName("id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CloudFileReferenceObject>("Background")
 | 
					                    b.Property<SnCloudFileReferenceObject>("Background")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("background");
 | 
					                        .HasColumnName("background");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,7 +55,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("uuid")
 | 
					                        .HasColumnType("uuid")
 | 
				
			||||||
                        .HasColumnName("developer_id");
 | 
					                        .HasColumnName("developer_id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CustomAppLinks>("Links")
 | 
					                    b.Property<SnCustomAppLinks>("Links")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("links");
 | 
					                        .HasColumnName("links");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,11 +65,11 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("character varying(1024)")
 | 
					                        .HasColumnType("character varying(1024)")
 | 
				
			||||||
                        .HasColumnName("name");
 | 
					                        .HasColumnName("name");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CustomAppOauthConfig>("OauthConfig")
 | 
					                    b.Property<SnCustomAppOauthConfig>("OauthConfig")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("oauth_config");
 | 
					                        .HasColumnName("oauth_config");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CloudFileReferenceObject>("Picture")
 | 
					                    b.Property<SnCloudFileReferenceObject>("Picture")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("picture");
 | 
					                        .HasColumnName("picture");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -88,7 +87,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("timestamp with time zone")
 | 
					                        .HasColumnType("timestamp with time zone")
 | 
				
			||||||
                        .HasColumnName("updated_at");
 | 
					                        .HasColumnName("updated_at");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<VerificationMark>("Verification")
 | 
					                    b.Property<SnVerificationMark>("Verification")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("verification");
 | 
					                        .HasColumnName("verification");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,4 @@
 | 
				
			|||||||
using System;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Develop.Identity;
 | 
					 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -35,11 +33,11 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                    name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
					                    name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
				
			||||||
                    description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
 | 
					                    description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
 | 
				
			||||||
                    status = table.Column<int>(type: "integer", nullable: false),
 | 
					                    status = table.Column<int>(type: "integer", nullable: false),
 | 
				
			||||||
                    picture = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
 | 
					                    picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
 | 
				
			||||||
                    background = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
 | 
					                    background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
 | 
				
			||||||
                    verification = table.Column<VerificationMark>(type: "jsonb", nullable: true),
 | 
					                    verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true),
 | 
				
			||||||
                    oauth_config = table.Column<CustomAppOauthConfig>(type: "jsonb", nullable: true),
 | 
					                    oauth_config = table.Column<SnCustomAppOauthConfig>(type: "jsonb", nullable: true),
 | 
				
			||||||
                    links = table.Column<CustomAppLinks>(type: "jsonb", nullable: true),
 | 
					                    links = table.Column<SnCustomAppLinks>(type: "jsonb", nullable: true),
 | 
				
			||||||
                    developer_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
					                    developer_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
				
			||||||
                    created_at = table.Column<Instant>(type: "timestamp with time zone", 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),
 | 
					                    updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,7 @@
 | 
				
			|||||||
// <auto-generated />
 | 
					// <auto-generated />
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using DysonNetwork.Develop;
 | 
					using DysonNetwork.Develop;
 | 
				
			||||||
using DysonNetwork.Develop.Identity;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
@@ -35,7 +34,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("uuid")
 | 
					                        .HasColumnType("uuid")
 | 
				
			||||||
                        .HasColumnName("id");
 | 
					                        .HasColumnName("id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CloudFileReferenceObject>("Background")
 | 
					                    b.Property<SnCloudFileReferenceObject>("Background")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("background");
 | 
					                        .HasColumnName("background");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,7 +51,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("character varying(4096)")
 | 
					                        .HasColumnType("character varying(4096)")
 | 
				
			||||||
                        .HasColumnName("description");
 | 
					                        .HasColumnName("description");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CustomAppLinks>("Links")
 | 
					                    b.Property<SnCustomAppLinks>("Links")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("links");
 | 
					                        .HasColumnName("links");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -62,11 +61,11 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("character varying(1024)")
 | 
					                        .HasColumnType("character varying(1024)")
 | 
				
			||||||
                        .HasColumnName("name");
 | 
					                        .HasColumnName("name");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CustomAppOauthConfig>("OauthConfig")
 | 
					                    b.Property<SnCustomAppOauthConfig>("OauthConfig")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("oauth_config");
 | 
					                        .HasColumnName("oauth_config");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CloudFileReferenceObject>("Picture")
 | 
					                    b.Property<SnCloudFileReferenceObject>("Picture")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("picture");
 | 
					                        .HasColumnName("picture");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -88,7 +87,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("timestamp with time zone")
 | 
					                        .HasColumnType("timestamp with time zone")
 | 
				
			||||||
                        .HasColumnName("updated_at");
 | 
					                        .HasColumnName("updated_at");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<VerificationMark>("Verification")
 | 
					                    b.Property<SnVerificationMark>("Verification")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("verification");
 | 
					                        .HasColumnName("verification");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
using System;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#nullable disable
 | 
					#nullable disable
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,7 @@
 | 
				
			|||||||
// <auto-generated />
 | 
					// <auto-generated />
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using DysonNetwork.Develop;
 | 
					using DysonNetwork.Develop;
 | 
				
			||||||
using DysonNetwork.Develop.Identity;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
@@ -77,7 +76,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("uuid")
 | 
					                        .HasColumnType("uuid")
 | 
				
			||||||
                        .HasColumnName("id");
 | 
					                        .HasColumnName("id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CloudFileReferenceObject>("Background")
 | 
					                    b.Property<SnCloudFileReferenceObject>("Background")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("background");
 | 
					                        .HasColumnName("background");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -94,7 +93,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("character varying(4096)")
 | 
					                        .HasColumnType("character varying(4096)")
 | 
				
			||||||
                        .HasColumnName("description");
 | 
					                        .HasColumnName("description");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CustomAppLinks>("Links")
 | 
					                    b.Property<SnCustomAppLinks>("Links")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("links");
 | 
					                        .HasColumnName("links");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -104,11 +103,11 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("character varying(1024)")
 | 
					                        .HasColumnType("character varying(1024)")
 | 
				
			||||||
                        .HasColumnName("name");
 | 
					                        .HasColumnName("name");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CustomAppOauthConfig>("OauthConfig")
 | 
					                    b.Property<SnCustomAppOauthConfig>("OauthConfig")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("oauth_config");
 | 
					                        .HasColumnName("oauth_config");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CloudFileReferenceObject>("Picture")
 | 
					                    b.Property<SnCloudFileReferenceObject>("Picture")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("picture");
 | 
					                        .HasColumnName("picture");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -130,7 +129,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("timestamp with time zone")
 | 
					                        .HasColumnType("timestamp with time zone")
 | 
				
			||||||
                        .HasColumnName("updated_at");
 | 
					                        .HasColumnName("updated_at");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<VerificationMark>("Verification")
 | 
					                    b.Property<SnVerificationMark>("Verification")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("verification");
 | 
					                        .HasColumnName("verification");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
using System;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#nullable disable
 | 
					#nullable disable
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,7 @@
 | 
				
			|||||||
// <auto-generated />
 | 
					// <auto-generated />
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using DysonNetwork.Develop;
 | 
					using DysonNetwork.Develop;
 | 
				
			||||||
using DysonNetwork.Develop.Identity;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
					using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
				
			||||||
@@ -74,7 +73,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("uuid")
 | 
					                        .HasColumnType("uuid")
 | 
				
			||||||
                        .HasColumnName("id");
 | 
					                        .HasColumnName("id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CloudFileReferenceObject>("Background")
 | 
					                    b.Property<SnCloudFileReferenceObject>("Background")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("background");
 | 
					                        .HasColumnName("background");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -91,7 +90,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("character varying(4096)")
 | 
					                        .HasColumnType("character varying(4096)")
 | 
				
			||||||
                        .HasColumnName("description");
 | 
					                        .HasColumnName("description");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CustomAppLinks>("Links")
 | 
					                    b.Property<SnCustomAppLinks>("Links")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("links");
 | 
					                        .HasColumnName("links");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -101,11 +100,11 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("character varying(1024)")
 | 
					                        .HasColumnType("character varying(1024)")
 | 
				
			||||||
                        .HasColumnName("name");
 | 
					                        .HasColumnName("name");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CustomAppOauthConfig>("OauthConfig")
 | 
					                    b.Property<SnCustomAppOauthConfig>("OauthConfig")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("oauth_config");
 | 
					                        .HasColumnName("oauth_config");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<CloudFileReferenceObject>("Picture")
 | 
					                    b.Property<SnCloudFileReferenceObject>("Picture")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("picture");
 | 
					                        .HasColumnName("picture");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -127,7 +126,7 @@ namespace DysonNetwork.Develop.Migrations
 | 
				
			|||||||
                        .HasColumnType("timestamp with time zone")
 | 
					                        .HasColumnType("timestamp with time zone")
 | 
				
			||||||
                        .HasColumnName("updated_at");
 | 
					                        .HasColumnName("updated_at");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<VerificationMark>("Verification")
 | 
					                    b.Property<SnVerificationMark>("Verification")
 | 
				
			||||||
                        .HasColumnType("jsonb")
 | 
					                        .HasColumnType("jsonb")
 | 
				
			||||||
                        .HasColumnName("verification");
 | 
					                        .HasColumnName("verification");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,12 +13,16 @@ builder.ConfigureAppKestrel(builder.Configuration);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
builder.Services.AddAppServices(builder.Configuration);
 | 
					builder.Services.AddAppServices(builder.Configuration);
 | 
				
			||||||
builder.Services.AddAppAuthentication();
 | 
					builder.Services.AddAppAuthentication();
 | 
				
			||||||
builder.Services.AddAppSwagger();
 | 
					 | 
				
			||||||
builder.Services.AddDysonAuth();
 | 
					builder.Services.AddDysonAuth();
 | 
				
			||||||
builder.Services.AddPublisherService();
 | 
					builder.Services.AddPublisherService();
 | 
				
			||||||
builder.Services.AddAccountService();
 | 
					builder.Services.AddAccountService();
 | 
				
			||||||
builder.Services.AddDriveService();
 | 
					builder.Services.AddDriveService();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					builder.AddSwaggerManifest(
 | 
				
			||||||
 | 
					    "DysonNetwork.Develop",
 | 
				
			||||||
 | 
					    "The developer portal in the Solar Network."
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var app = builder.Build();
 | 
					var app = builder.Build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.MapDefaultEndpoints();
 | 
					app.MapDefaultEndpoints();
 | 
				
			||||||
@@ -31,4 +35,6 @@ using (var scope = app.Services.CreateScope())
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
app.ConfigureAppMiddleware(builder.Configuration);
 | 
					app.ConfigureAppMiddleware(builder.Configuration);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.UseSwaggerManifest();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.Run();
 | 
					app.Run();
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
using DysonNetwork.Develop.Identity;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using DysonNetwork.Shared.Proto;
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace DysonNetwork.Develop.Project;
 | 
					namespace DysonNetwork.Develop.Project;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,12 +10,12 @@ public class DevProjectService(
 | 
				
			|||||||
    FileService.FileServiceClient files
 | 
					    FileService.FileServiceClient files
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public async Task<DevProject> CreateProjectAsync(
 | 
					    public async Task<SnDevProject> CreateProjectAsync(
 | 
				
			||||||
        Developer developer,
 | 
					        SnDeveloper developer,
 | 
				
			||||||
        DevProjectController.DevProjectRequest request
 | 
					        DevProjectController.DevProjectRequest request
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var project = new DevProject
 | 
					        var project = new SnDevProject
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Slug = request.Slug!,
 | 
					            Slug = request.Slug!,
 | 
				
			||||||
            Name = request.Name!,
 | 
					            Name = request.Name!,
 | 
				
			||||||
@@ -29,7 +29,7 @@ public class DevProjectService(
 | 
				
			|||||||
        return project;
 | 
					        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();
 | 
					        var query = db.DevProjects.AsQueryable();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
@@ -41,14 +41,14 @@ public class DevProjectService(
 | 
				
			|||||||
        return await query.FirstOrDefaultAsync(p => p.Id == id);
 | 
					        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
 | 
					        return await db.DevProjects
 | 
				
			||||||
            .Where(p => p.DeveloperId == developerId)
 | 
					            .Where(p => p.DeveloperId == developerId)
 | 
				
			||||||
            .ToListAsync();
 | 
					            .ToListAsync();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task<DevProject?> UpdateProjectAsync(
 | 
					    public async Task<SnDevProject?> UpdateProjectAsync(
 | 
				
			||||||
        Guid id,
 | 
					        Guid id,
 | 
				
			||||||
        Guid developerId,
 | 
					        Guid developerId,
 | 
				
			||||||
        DevProjectController.DevProjectRequest request
 | 
					        DevProjectController.DevProjectRequest request
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,6 @@
 | 
				
			|||||||
      "commandName": "Project",
 | 
					      "commandName": "Project",
 | 
				
			||||||
      "dotnetRunMessages": true,
 | 
					      "dotnetRunMessages": true,
 | 
				
			||||||
      "launchBrowser": false,
 | 
					      "launchBrowser": false,
 | 
				
			||||||
      "applicationUrl": "http://localhost:5156",
 | 
					 | 
				
			||||||
      "environmentVariables": {
 | 
					      "environmentVariables": {
 | 
				
			||||||
        "ASPNETCORE_ENVIRONMENT": "Development"
 | 
					        "ASPNETCORE_ENVIRONMENT": "Development"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -14,7 +13,6 @@
 | 
				
			|||||||
      "commandName": "Project",
 | 
					      "commandName": "Project",
 | 
				
			||||||
      "dotnetRunMessages": true,
 | 
					      "dotnetRunMessages": true,
 | 
				
			||||||
      "launchBrowser": false,
 | 
					      "launchBrowser": false,
 | 
				
			||||||
      "applicationUrl": "https://localhost:7192;http://localhost:5156",
 | 
					 | 
				
			||||||
      "environmentVariables": {
 | 
					      "environmentVariables": {
 | 
				
			||||||
        "ASPNETCORE_ENVIRONMENT": "Development"
 | 
					        "ASPNETCORE_ENVIRONMENT": "Development"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,6 @@
 | 
				
			|||||||
using System.Net;
 | 
					 | 
				
			||||||
using DysonNetwork.Develop.Identity;
 | 
					using DysonNetwork.Develop.Identity;
 | 
				
			||||||
using DysonNetwork.Shared.Auth;
 | 
					using DysonNetwork.Shared.Auth;
 | 
				
			||||||
using DysonNetwork.Shared.Http;
 | 
					using DysonNetwork.Shared.Http;
 | 
				
			||||||
using Microsoft.AspNetCore.HttpOverrides;
 | 
					 | 
				
			||||||
using Prometheus;
 | 
					using Prometheus;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace DysonNetwork.Develop.Startup;
 | 
					namespace DysonNetwork.Develop.Startup;
 | 
				
			||||||
@@ -14,9 +12,6 @@ public static class ApplicationConfiguration
 | 
				
			|||||||
        app.MapMetrics();
 | 
					        app.MapMetrics();
 | 
				
			||||||
        app.MapOpenApi();
 | 
					        app.MapOpenApi();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        app.UseSwagger();
 | 
					 | 
				
			||||||
        app.UseSwaggerUI();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        app.UseRequestLocalization();
 | 
					        app.UseRequestLocalization();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        app.ConfigureForwardedHeaders(configuration);
 | 
					        app.ConfigureForwardedHeaders(configuration);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
using System.Globalization;
 | 
					using System.Globalization;
 | 
				
			||||||
using Microsoft.OpenApi.Models;
 | 
					 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
using NodaTime.Serialization.SystemTextJson;
 | 
					using NodaTime.Serialization.SystemTextJson;
 | 
				
			||||||
using System.Text.Json;
 | 
					using System.Text.Json;
 | 
				
			||||||
@@ -7,7 +6,6 @@ using System.Text.Json.Serialization;
 | 
				
			|||||||
using DysonNetwork.Develop.Identity;
 | 
					using DysonNetwork.Develop.Identity;
 | 
				
			||||||
using DysonNetwork.Develop.Project;
 | 
					using DysonNetwork.Develop.Project;
 | 
				
			||||||
using DysonNetwork.Shared.Cache;
 | 
					using DysonNetwork.Shared.Cache;
 | 
				
			||||||
using StackExchange.Redis;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace DysonNetwork.Develop.Startup;
 | 
					namespace DysonNetwork.Develop.Startup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -57,23 +55,7 @@ public static class ServiceCollectionExtensions
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
 | 
					    public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        services.AddCors();
 | 
					 | 
				
			||||||
        services.AddAuthorization();
 | 
					        services.AddAuthorization();
 | 
				
			||||||
        return services;
 | 
					        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,12 +10,12 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "AllowedHosts": "*",
 | 
					  "AllowedHosts": "*",
 | 
				
			||||||
  "ConnectionStrings": {
 | 
					  "ConnectionStrings": {
 | 
				
			||||||
    "App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
 | 
					    "App": "Host=localhost;Port=5432;Database=dyson_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": {
 | 
					  "Etcd": {
 | 
				
			||||||
    "Insecure": true
 | 
					    "Insecure": true
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,7 @@
 | 
				
			|||||||
using System.Linq.Expressions;
 | 
					using System.Linq.Expressions;
 | 
				
			||||||
using System.Reflection;
 | 
					using System.Reflection;
 | 
				
			||||||
using DysonNetwork.Drive.Billing;
 | 
					using DysonNetwork.Drive.Billing;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Design;
 | 
					using Microsoft.EntityFrameworkCore.Design;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Query;
 | 
					using Microsoft.EntityFrameworkCore.Query;
 | 
				
			||||||
@@ -17,11 +16,11 @@ public class AppDatabase(
 | 
				
			|||||||
) : DbContext(options)
 | 
					) : DbContext(options)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public DbSet<FilePool> Pools { get; set; } = null!;
 | 
					    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<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!;
 | 
					    public DbSet<CloudFileReference> FileReferences { get; set; } = null!;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
 | 
					    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
using DysonNetwork.Shared.Data;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace DysonNetwork.Drive.Billing;
 | 
					namespace DysonNetwork.Drive.Billing;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,7 @@ public class QuotaService(
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
        var (based, extra) = await GetQuotaVerbose(accountId);
 | 
					        var (based, extra) = await GetQuotaVerbose(accountId);
 | 
				
			||||||
        var quota = based + extra;
 | 
					        var quota = based + extra;
 | 
				
			||||||
        await cache.SetAsync(cacheKey, quota);
 | 
					        await cache.SetAsync(cacheKey, quota, expiry: TimeSpan.FromMinutes(30));
 | 
				
			||||||
        return quota;
 | 
					        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 8080
 | 
				
			||||||
EXPOSE 8081
 | 
					EXPOSE 8081
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Stage 1: Install runtime dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Install only necessary dependencies
 | 
					# Install only necessary dependencies
 | 
				
			||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
					RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
				
			||||||
  libfontconfig1 \
 | 
					  libfontconfig1 \
 | 
				
			||||||
@@ -17,24 +19,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
USER $APP_UID
 | 
					USER $APP_UID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 2: Build SPA
 | 
					# Stage 2: Build .NET application
 | 
				
			||||||
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
 | 
					 | 
				
			||||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
 | 
					FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
 | 
				
			||||||
ARG BUILD_CONFIGURATION=Release
 | 
					ARG BUILD_CONFIGURATION=Release
 | 
				
			||||||
WORKDIR /src
 | 
					WORKDIR /src
 | 
				
			||||||
@@ -42,9 +27,6 @@ COPY ["DysonNetwork.Drive/DysonNetwork.Drive.csproj", "DysonNetwork.Drive/"]
 | 
				
			|||||||
RUN dotnet restore "DysonNetwork.Drive/DysonNetwork.Drive.csproj"
 | 
					RUN dotnet restore "DysonNetwork.Drive/DysonNetwork.Drive.csproj"
 | 
				
			||||||
COPY . .
 | 
					COPY . .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Copy built SPA to wwwroot
 | 
					 | 
				
			||||||
COPY --from=spa-builder /src/Client/dist /src/DysonNetwork.Drive/wwwroot/dist
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
WORKDIR "/src/DysonNetwork.Drive"
 | 
					WORKDIR "/src/DysonNetwork.Drive"
 | 
				
			||||||
RUN dotnet build "./DysonNetwork.Drive.csproj" -c $BUILD_CONFIGURATION -o /app/build \
 | 
					RUN dotnet build "./DysonNetwork.Drive.csproj" -c $BUILD_CONFIGURATION -o /app/build \
 | 
				
			||||||
    -p:TypeScriptCompileBlocked=true \
 | 
					    -p:TypeScriptCompileBlocked=true \
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,11 +17,13 @@
 | 
				
			|||||||
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
					          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
				
			||||||
          <PrivateAssets>all</PrivateAssets>
 | 
					          <PrivateAssets>all</PrivateAssets>
 | 
				
			||||||
        </PackageReference>
 | 
					        </PackageReference>
 | 
				
			||||||
 | 
					        <PackageReference Include="MimeKit" Version="4.13.0" />
 | 
				
			||||||
        <PackageReference Include="MimeTypes" Version="2.5.2">
 | 
					        <PackageReference Include="MimeTypes" Version="2.5.2">
 | 
				
			||||||
          <PrivateAssets>all</PrivateAssets>
 | 
					          <PrivateAssets>all</PrivateAssets>
 | 
				
			||||||
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
					          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
				
			||||||
        </PackageReference>
 | 
					        </PackageReference>
 | 
				
			||||||
        <PackageReference Include="Minio" Version="6.0.5" />
 | 
					        <PackageReference Include="Minio" Version="6.0.5" />
 | 
				
			||||||
 | 
					        <PackageReference Include="Nanoid" Version="3.1.0" />
 | 
				
			||||||
        <PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
 | 
					        <PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
 | 
				
			||||||
          <PrivateAssets>all</PrivateAssets>
 | 
					          <PrivateAssets>all</PrivateAssets>
 | 
				
			||||||
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
					          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
				
			||||||
@@ -54,8 +56,8 @@
 | 
				
			|||||||
        <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
 | 
					        <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
 | 
				
			||||||
        <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
 | 
					        <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
 | 
				
			||||||
        <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" 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" Version="9.0.4" />
 | 
				
			||||||
        <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
 | 
					        <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
 | 
				
			||||||
        <PackageReference Include="tusdotnet" Version="2.10.0" />
 | 
					        <PackageReference Include="tusdotnet" Version="2.10.0" />
 | 
				
			||||||
    </ItemGroup>
 | 
					    </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,16 +68,6 @@
 | 
				
			|||||||
    </ItemGroup>
 | 
					    </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ItemGroup>
 | 
					    <ItemGroup>
 | 
				
			||||||
      <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" />
 | 
					 | 
				
			||||||
      <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
 | 
					      <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
 | 
				
			||||||
    </ItemGroup>
 | 
					    </ItemGroup>
 | 
				
			||||||
 | 
					 | 
				
			||||||
    <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>
 | 
					</Project>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@ using System;
 | 
				
			|||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Drive.Storage;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,4 @@
 | 
				
			|||||||
using System;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
using System.Collections.Generic;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#nullable disable
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,7 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,4 @@
 | 
				
			|||||||
using System;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,7 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,7 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,7 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
using System;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#nullable disable
 | 
					#nullable disable
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,7 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
using System;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#nullable disable
 | 
					#nullable disable
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,7 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,7 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,7 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
					using Microsoft.EntityFrameworkCore.Migrations;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,7 @@
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Data;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
					using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
					using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,9 +1,7 @@
 | 
				
			|||||||
using DysonNetwork.Drive;
 | 
					using DysonNetwork.Drive;
 | 
				
			||||||
using DysonNetwork.Drive.Pages.Data;
 | 
					 | 
				
			||||||
using DysonNetwork.Drive.Startup;
 | 
					using DysonNetwork.Drive.Startup;
 | 
				
			||||||
using DysonNetwork.Shared.Auth;
 | 
					using DysonNetwork.Shared.Auth;
 | 
				
			||||||
using DysonNetwork.Shared.Http;
 | 
					using DysonNetwork.Shared.Http;
 | 
				
			||||||
using DysonNetwork.Shared.PageData;
 | 
					 | 
				
			||||||
using DysonNetwork.Shared.Registry;
 | 
					using DysonNetwork.Shared.Registry;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using tusdotnet.Stores;
 | 
					using tusdotnet.Stores;
 | 
				
			||||||
@@ -20,22 +18,19 @@ builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxV
 | 
				
			|||||||
builder.Services.AddAppServices(builder.Configuration);
 | 
					builder.Services.AddAppServices(builder.Configuration);
 | 
				
			||||||
builder.Services.AddAppRateLimiting();
 | 
					builder.Services.AddAppRateLimiting();
 | 
				
			||||||
builder.Services.AddAppAuthentication();
 | 
					builder.Services.AddAppAuthentication();
 | 
				
			||||||
builder.Services.AddAppSwagger();
 | 
					 | 
				
			||||||
builder.Services.AddDysonAuth();
 | 
					builder.Services.AddDysonAuth();
 | 
				
			||||||
builder.Services.AddAccountService();
 | 
					builder.Services.AddAccountService();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
builder.Services.AddAppFileStorage(builder.Configuration);
 | 
					builder.Services.AddAppFileStorage(builder.Configuration);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Add flush handlers and websocket handlers
 | 
					 | 
				
			||||||
builder.Services.AddAppFlushHandlers();
 | 
					builder.Services.AddAppFlushHandlers();
 | 
				
			||||||
 | 
					 | 
				
			||||||
// Add business services
 | 
					 | 
				
			||||||
builder.Services.AddAppBusinessServices();
 | 
					builder.Services.AddAppBusinessServices();
 | 
				
			||||||
 | 
					 | 
				
			||||||
// Add scheduled jobs
 | 
					 | 
				
			||||||
builder.Services.AddAppScheduledJobs();
 | 
					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();
 | 
					var app = builder.Build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -49,13 +44,11 @@ using (var scope = app.Services.CreateScope())
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>();
 | 
					var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>();
 | 
				
			||||||
 | 
					app.ConfigureAppMiddleware(tusDiskStore);
 | 
				
			||||||
// Configure application middleware pipeline
 | 
					 | 
				
			||||||
app.ConfigureAppMiddleware(tusDiskStore, builder.Environment.ContentRootPath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
app.MapPages(Path.Combine(app.Environment.WebRootPath, "dist", "index.html"));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Configure gRPC
 | 
					// Configure gRPC
 | 
				
			||||||
app.ConfigureGrpcServices();
 | 
					app.ConfigureGrpcServices();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.UseSwaggerManifest();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.Run();
 | 
					app.Run();
 | 
				
			||||||
@@ -5,7 +5,6 @@
 | 
				
			|||||||
      "commandName": "Project",
 | 
					      "commandName": "Project",
 | 
				
			||||||
      "dotnetRunMessages": true,
 | 
					      "dotnetRunMessages": true,
 | 
				
			||||||
      "launchBrowser": false,
 | 
					      "launchBrowser": false,
 | 
				
			||||||
      "applicationUrl": "http://localhost:5090",
 | 
					 | 
				
			||||||
      "environmentVariables": {
 | 
					      "environmentVariables": {
 | 
				
			||||||
        "ASPNETCORE_ENVIRONMENT": "Development"
 | 
					        "ASPNETCORE_ENVIRONMENT": "Development"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -14,7 +13,6 @@
 | 
				
			|||||||
      "commandName": "Project",
 | 
					      "commandName": "Project",
 | 
				
			||||||
      "dotnetRunMessages": true,
 | 
					      "dotnetRunMessages": true,
 | 
				
			||||||
      "launchBrowser": false,
 | 
					      "launchBrowser": false,
 | 
				
			||||||
      "applicationUrl": "https://localhost:7092;http://localhost:5090",
 | 
					 | 
				
			||||||
      "environmentVariables": {
 | 
					      "environmentVariables": {
 | 
				
			||||||
        "ASPNETCORE_ENVIRONMENT": "Development"
 | 
					        "ASPNETCORE_ENVIRONMENT": "Development"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Drive.Storage;
 | 
				
			||||||
using Microsoft.Extensions.FileProviders;
 | 
					 | 
				
			||||||
using tusdotnet;
 | 
					using tusdotnet;
 | 
				
			||||||
using tusdotnet.Interfaces;
 | 
					using tusdotnet.Interfaces;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -7,33 +6,11 @@ namespace DysonNetwork.Drive.Startup;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
public static class ApplicationBuilderExtensions
 | 
					public static class ApplicationBuilderExtensions
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public static WebApplication ConfigureAppMiddleware(this WebApplication app, ITusStore tusStore, string contentRoot)
 | 
					    public static WebApplication ConfigureAppMiddleware(this WebApplication app, ITusStore tusStore)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        // Configure the HTTP request pipeline.
 | 
					 | 
				
			||||||
        if (app.Environment.IsDevelopment())
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            app.UseSwagger();
 | 
					 | 
				
			||||||
            app.UseSwaggerUI();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        app.UseAuthorization();
 | 
					        app.UseAuthorization();
 | 
				
			||||||
        app.MapControllers();
 | 
					        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)));
 | 
					        app.MapTus("/api/tus", _ => Task.FromResult(TusService.BuildConfiguration(tusStore, app.Configuration)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return app;
 | 
					        return app;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,16 @@
 | 
				
			|||||||
using System.Text.Json;
 | 
					using System.Text.Json;
 | 
				
			||||||
using DysonNetwork.Drive.Storage;
 | 
					using DysonNetwork.Drive.Storage.Model;
 | 
				
			||||||
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
using DysonNetwork.Shared.Stream;
 | 
					using DysonNetwork.Shared.Stream;
 | 
				
			||||||
 | 
					using FFMpegCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using NATS.Client.Core;
 | 
					using NATS.Client.Core;
 | 
				
			||||||
 | 
					using NATS.Client.JetStream;
 | 
				
			||||||
using NATS.Client.JetStream.Models;
 | 
					using NATS.Client.JetStream.Models;
 | 
				
			||||||
using NATS.Net;
 | 
					using NATS.Net;
 | 
				
			||||||
 | 
					using NetVips;
 | 
				
			||||||
 | 
					using NodaTime;
 | 
				
			||||||
 | 
					using FileService = DysonNetwork.Drive.Storage.FileService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace DysonNetwork.Drive.Startup;
 | 
					namespace DysonNetwork.Drive.Startup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,20 +20,74 @@ public class BroadcastEventHandler(
 | 
				
			|||||||
    IServiceProvider serviceProvider
 | 
					    IServiceProvider serviceProvider
 | 
				
			||||||
) : BackgroundService
 | 
					) : 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)
 | 
					    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var js = nats.CreateJetStreamContext();
 | 
					        var js = nats.CreateJetStreamContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await js.EnsureStreamCreated("account_events", [AccountDeletedEvent.Type]);
 | 
					        await js.EnsureStreamCreated("account_events", [AccountDeletedEvent.Type]);
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        var consumer = await js.CreateOrUpdateConsumerAsync("account_events",
 | 
					        var accountEventConsumer = await js.CreateOrUpdateConsumerAsync("account_events",
 | 
				
			||||||
            new ConsumerConfig("drive_account_deleted_handler"), cancellationToken: stoppingToken);
 | 
					            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))
 | 
					        await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken))
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            try
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var evt = JsonSerializer.Deserialize<AccountDeletedEvent>(msg.Data);
 | 
					                var evt = JsonSerializer.Deserialize<AccountDeletedEvent>(msg.Data, GrpcTypeHelper.SerializerOptions);
 | 
				
			||||||
                if (evt == null)
 | 
					                if (evt == null)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    await msg.AckAsync(cancellationToken: stoppingToken);
 | 
					                    await msg.AckAsync(cancellationToken: stoppingToken);
 | 
				
			||||||
@@ -69,4 +129,169 @@ public class BroadcastEventHandler(
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					     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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -3,11 +3,8 @@ using System.Text.Json.Serialization;
 | 
				
			|||||||
using System.Threading.RateLimiting;
 | 
					using System.Threading.RateLimiting;
 | 
				
			||||||
using DysonNetwork.Shared.Cache;
 | 
					using DysonNetwork.Shared.Cache;
 | 
				
			||||||
using Microsoft.AspNetCore.RateLimiting;
 | 
					using Microsoft.AspNetCore.RateLimiting;
 | 
				
			||||||
using Microsoft.OpenApi.Models;
 | 
					 | 
				
			||||||
using NodaTime;
 | 
					using NodaTime;
 | 
				
			||||||
using NodaTime.Serialization.SystemTextJson;
 | 
					using NodaTime.Serialization.SystemTextJson;
 | 
				
			||||||
using StackExchange.Redis;
 | 
					 | 
				
			||||||
using DysonNetwork.Shared.Proto;
 | 
					 | 
				
			||||||
using tusdotnet.Stores;
 | 
					using tusdotnet.Stores;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace DysonNetwork.Drive.Startup;
 | 
					namespace DysonNetwork.Drive.Startup;
 | 
				
			||||||
@@ -61,9 +58,7 @@ public static class ServiceCollectionExtensions
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
 | 
					    public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        services.AddCors();
 | 
					 | 
				
			||||||
        services.AddAuthorization();
 | 
					        services.AddAuthorization();
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return services;
 | 
					        return services;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -74,52 +69,6 @@ public static class ServiceCollectionExtensions
 | 
				
			|||||||
        return services;
 | 
					        return services;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static IServiceCollection AddAppSwagger(this IServiceCollection services)
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        services.AddEndpointsApiExplorer();
 | 
					 | 
				
			||||||
        services.AddSwaggerGen(options =>
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            options.SwaggerDoc("v1", new OpenApiInfo
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                Version = "v1",
 | 
					 | 
				
			||||||
                Title = "Dyson Drive",
 | 
					 | 
				
			||||||
                Description =
 | 
					 | 
				
			||||||
                    "The file service of the Dyson Network. Mainly handling file storage and sharing. Also provide image processing and media analysis. Powered the Solar Network Drive as well.",
 | 
					 | 
				
			||||||
                TermsOfService = new Uri("https://solsynth.dev/terms"), // Update with actual terms
 | 
					 | 
				
			||||||
                License = new OpenApiLicense
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    Name = "APGLv3", // Update with actual license
 | 
					 | 
				
			||||||
                    Url = new Uri("https://www.gnu.org/licenses/agpl-3.0.html")
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                In = ParameterLocation.Header,
 | 
					 | 
				
			||||||
                Description = "Please enter a valid token",
 | 
					 | 
				
			||||||
                Name = "Authorization",
 | 
					 | 
				
			||||||
                Type = SecuritySchemeType.Http,
 | 
					 | 
				
			||||||
                BearerFormat = "JWT",
 | 
					 | 
				
			||||||
                Scheme = "Bearer"
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            options.AddSecurityRequirement(new OpenApiSecurityRequirement
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    new OpenApiSecurityScheme
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Reference = new OpenApiReference
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            Type = ReferenceType.SecurityScheme,
 | 
					 | 
				
			||||||
                            Id = "Bearer"
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    []
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return services;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    public static IServiceCollection AddAppFileStorage(this IServiceCollection services, IConfiguration configuration)
 | 
					    public static IServiceCollection AddAppFileStorage(this IServiceCollection services, IConfiguration configuration)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var tusStorePath = configuration.GetSection("Tus").GetValue<string>("StorePath")!;
 | 
					        var tusStorePath = configuration.GetSection("Tus").GetValue<string>("StorePath")!;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
using System.ComponentModel.DataAnnotations;
 | 
					using System.ComponentModel.DataAnnotations;
 | 
				
			||||||
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
using DysonNetwork.Shared.Proto;
 | 
					using DysonNetwork.Shared.Proto;
 | 
				
			||||||
using Microsoft.AspNetCore.Authorization;
 | 
					using Microsoft.AspNetCore.Authorization;
 | 
				
			||||||
using Microsoft.AspNetCore.Mvc;
 | 
					using Microsoft.AspNetCore.Mvc;
 | 
				
			||||||
@@ -22,7 +23,7 @@ public class BundleController(AppDatabase db) : ControllerBase
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [HttpGet("{id:guid}")]
 | 
					    [HttpGet("{id:guid}")]
 | 
				
			||||||
    public async Task<ActionResult<FileBundle>> GetBundle([FromRoute] Guid id, [FromQuery] string? passcode)
 | 
					    public async Task<ActionResult<SnFileBundle>> GetBundle([FromRoute] Guid id, [FromQuery] string? passcode)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var bundle = await db.Bundles
 | 
					        var bundle = await db.Bundles
 | 
				
			||||||
            .Where(e => e.Id == id)
 | 
					            .Where(e => e.Id == id)
 | 
				
			||||||
@@ -36,7 +37,7 @@ public class BundleController(AppDatabase db) : ControllerBase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    [HttpGet("me")]
 | 
					    [HttpGet("me")]
 | 
				
			||||||
    [Authorize]
 | 
					    [Authorize]
 | 
				
			||||||
    public async Task<ActionResult<List<FileBundle>>> ListBundles(
 | 
					    public async Task<ActionResult<List<SnFileBundle>>> ListBundles(
 | 
				
			||||||
        [FromQuery] string? term,
 | 
					        [FromQuery] string? term,
 | 
				
			||||||
        [FromQuery] int offset = 0,
 | 
					        [FromQuery] int offset = 0,
 | 
				
			||||||
        [FromQuery] int take = 20
 | 
					        [FromQuery] int take = 20
 | 
				
			||||||
@@ -65,7 +66,7 @@ public class BundleController(AppDatabase db) : ControllerBase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    [HttpPost]
 | 
					    [HttpPost]
 | 
				
			||||||
    [Authorize]
 | 
					    [Authorize]
 | 
				
			||||||
    public async Task<ActionResult<FileBundle>> CreateBundle([FromBody] BundleRequest request)
 | 
					    public async Task<ActionResult<SnFileBundle>> CreateBundle([FromBody] BundleRequest request)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
				
			||||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
					        var accountId = Guid.Parse(currentUser.Id);
 | 
				
			||||||
@@ -77,7 +78,7 @@ public class BundleController(AppDatabase db) : ControllerBase
 | 
				
			|||||||
        if (string.IsNullOrEmpty(request.Name))
 | 
					        if (string.IsNullOrEmpty(request.Name))
 | 
				
			||||||
            request.Name = "Unnamed Bundle";
 | 
					            request.Name = "Unnamed Bundle";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var bundle = new FileBundle
 | 
					        var bundle = new SnFileBundle
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Slug = request.Slug,
 | 
					            Slug = request.Slug,
 | 
				
			||||||
            Name = request.Name,
 | 
					            Name = request.Name,
 | 
				
			||||||
@@ -95,7 +96,7 @@ public class BundleController(AppDatabase db) : ControllerBase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    [HttpPut("{id:guid}")]
 | 
					    [HttpPut("{id:guid}")]
 | 
				
			||||||
    [Authorize]
 | 
					    [Authorize]
 | 
				
			||||||
    public async Task<ActionResult<FileBundle>> UpdateBundle([FromRoute] Guid id, [FromBody] BundleRequest request)
 | 
					    public async Task<ActionResult<SnFileBundle>> UpdateBundle([FromRoute] Guid id, [FromBody] BundleRequest request)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
					        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
				
			||||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
					        var accountId = Guid.Parse(currentUser.Id);
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user