Compare commits
	
		
			7 Commits
		
	
	
		
			refactor/a
			...
			8d2f4a4c47
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8d2f4a4c47 | |||
| 1672d46038 | |||
| 15fb93c2bb | |||
| 4b220e7ed7 | |||
| 65450e8511 | |||
| cb4acbb3fc | |||
| bb2f88cc54 | 
@@ -1,3 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "appHostPath": "../DysonNetwork.Control/DysonNetwork.Control.csproj"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
**/.dockerignore
 | 
			
		||||
**/.env
 | 
			
		||||
**/.git
 | 
			
		||||
**/.gitignore
 | 
			
		||||
**/.project
 | 
			
		||||
**/.settings
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								.env
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								.env
									
									
									
									
									
								
							@@ -1,35 +0,0 @@
 | 
			
		||||
# 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
 | 
			
		||||
							
								
								
									
										59
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										59
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
name: Aspire Publish Workflow
 | 
			
		||||
name: Build and Push Dyson Sphere
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
@@ -7,54 +7,27 @@ on:
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  publish:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: read
 | 
			
		||||
      packages: write
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: ubuntu-latest # x86_64 (default), avoids arm64 native module issues
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout repository
 | 
			
		||||
        uses: actions/checkout@v3
 | 
			
		||||
 | 
			
		||||
      - name: Setup .NET
 | 
			
		||||
        uses: actions/setup-dotnet@v3
 | 
			
		||||
        with:
 | 
			
		||||
          dotnet-version: "9.0.x"
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3
 | 
			
		||||
 | 
			
		||||
      - name: Log in to GitHub Container Registry
 | 
			
		||||
      - name: Log in to DockerHub
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
 | 
			
		||||
          username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
 | 
			
		||||
 | 
			
		||||
      - name: Install Aspire CLI
 | 
			
		||||
        run: dotnet tool install -g Aspire.Cli --prerelease
 | 
			
		||||
 | 
			
		||||
      - 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
 | 
			
		||||
      - name: Build and push Docker image
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
        with:
 | 
			
		||||
          name: aspire-publish-output
 | 
			
		||||
          path: ./publish/
 | 
			
		||||
 | 
			
		||||
      - name: Upload Docker Compose file
 | 
			
		||||
        uses: actions/upload-artifact@v3
 | 
			
		||||
        with:
 | 
			
		||||
          name: docker-compose-output
 | 
			
		||||
          path: ./publish/docker-compose.yml
 | 
			
		||||
          file: DysonNetwork.Sphere/Dockerfile
 | 
			
		||||
          context: .
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: xsheep2010/dyson-sphere:latest
 | 
			
		||||
          platforms: linux/amd64
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,7 +1,6 @@
 | 
			
		||||
bin/
 | 
			
		||||
obj/
 | 
			
		||||
/packages/
 | 
			
		||||
/Certificates/
 | 
			
		||||
riderModule.iml
 | 
			
		||||
/_ReSharper.Caches/
 | 
			
		||||
.idea
 | 
			
		||||
 
 | 
			
		||||
@@ -1,77 +0,0 @@
 | 
			
		||||
using Aspire.Hosting.Yarp.Transforms;
 | 
			
		||||
 | 
			
		||||
var builder = DistributedApplication.CreateBuilder(args);
 | 
			
		||||
 | 
			
		||||
// Database was configured separately in each service.
 | 
			
		||||
// var database = builder.AddPostgres("database");
 | 
			
		||||
 | 
			
		||||
var cache = builder.AddRedis("cache");
 | 
			
		||||
var queue = builder.AddNats("queue").WithJetStream();
 | 
			
		||||
 | 
			
		||||
var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring")
 | 
			
		||||
    .WithReference(queue)
 | 
			
		||||
    .WithHttpHealthCheck()
 | 
			
		||||
    .WithEndpoint(5001, 5001, "https", name: "grpc");
 | 
			
		||||
var passService = builder.AddProject<Projects.DysonNetwork_Pass>("pass")
 | 
			
		||||
    .WithReference(cache)
 | 
			
		||||
    .WithReference(queue)
 | 
			
		||||
    .WithReference(ringService)
 | 
			
		||||
    .WithHttpHealthCheck()
 | 
			
		||||
    .WithEndpoint(5001, 5001, "https", name: "grpc");
 | 
			
		||||
var driveService = builder.AddProject<Projects.DysonNetwork_Drive>("drive")
 | 
			
		||||
    .WithReference(cache)
 | 
			
		||||
    .WithReference(queue)
 | 
			
		||||
    .WithReference(passService)
 | 
			
		||||
    .WithReference(ringService)
 | 
			
		||||
    .WithHttpHealthCheck()
 | 
			
		||||
    .WithEndpoint(5001, 5001, "https", name: "grpc");
 | 
			
		||||
var sphereService = builder.AddProject<Projects.DysonNetwork_Sphere>("sphere")
 | 
			
		||||
    .WithReference(cache)
 | 
			
		||||
    .WithReference(queue)
 | 
			
		||||
    .WithReference(passService)
 | 
			
		||||
    .WithReference(ringService)
 | 
			
		||||
    .WithHttpHealthCheck()
 | 
			
		||||
    .WithEndpoint(5001, 5001, "https", name: "grpc");
 | 
			
		||||
var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop")
 | 
			
		||||
    .WithReference(cache)
 | 
			
		||||
    .WithReference(passService)
 | 
			
		||||
    .WithReference(ringService)
 | 
			
		||||
    .WithHttpHealthCheck()
 | 
			
		||||
    .WithEndpoint(5001, 5001, "https", name: "grpc");
 | 
			
		||||
 | 
			
		||||
// Extra double-ended references
 | 
			
		||||
ringService.WithReference(passService);
 | 
			
		||||
 | 
			
		||||
builder.AddYarp("gateway")
 | 
			
		||||
    .WithHostPort(5000)
 | 
			
		||||
    .WithConfiguration(yarp =>
 | 
			
		||||
    {
 | 
			
		||||
        var ringCluster = yarp.AddCluster(ringService.GetEndpoint("http"));
 | 
			
		||||
        yarp.AddRoute("/ws", ringCluster);
 | 
			
		||||
        yarp.AddRoute("/ring/{**catch-all}", ringCluster)
 | 
			
		||||
            .WithTransformPathRemovePrefix("/ring")
 | 
			
		||||
            .WithTransformPathPrefix("/api");
 | 
			
		||||
        var passCluster = yarp.AddCluster(passService.GetEndpoint("http"));
 | 
			
		||||
        yarp.AddRoute("/.well-known/openid-configuration", passCluster);
 | 
			
		||||
        yarp.AddRoute("/.well-known/jwks", passCluster);
 | 
			
		||||
        yarp.AddRoute("/id/{**catch-all}", passCluster)
 | 
			
		||||
            .WithTransformPathRemovePrefix("/id")
 | 
			
		||||
            .WithTransformPathPrefix("/api");
 | 
			
		||||
        var driveCluster = yarp.AddCluster(driveService.GetEndpoint("http"));
 | 
			
		||||
        yarp.AddRoute("/api/tus", driveCluster);
 | 
			
		||||
        yarp.AddRoute("/drive/{**catch-all}", driveCluster)
 | 
			
		||||
            .WithTransformPathRemovePrefix("/drive")
 | 
			
		||||
            .WithTransformPathPrefix("/api");
 | 
			
		||||
        var sphereCluster = yarp.AddCluster(sphereService.GetEndpoint("http"));
 | 
			
		||||
        yarp.AddRoute("/sphere/{**catch-all}", sphereCluster)
 | 
			
		||||
            .WithTransformPathRemovePrefix("/sphere")
 | 
			
		||||
            .WithTransformPathPrefix("/api");
 | 
			
		||||
        var developCluster = yarp.AddCluster(developService.GetEndpoint("http"));
 | 
			
		||||
        yarp.AddRoute("/develop/{**catch-all}", developCluster)
 | 
			
		||||
            .WithTransformPathRemovePrefix("/develop")
 | 
			
		||||
            .WithTransformPathPrefix("/api");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
builder.AddDockerComposeEnvironment("docker-compose");
 | 
			
		||||
 | 
			
		||||
builder.Build().Run();
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk">
 | 
			
		||||
 | 
			
		||||
    <Sdk Name="Aspire.AppHost.Sdk" Version="9.4.2"/>
 | 
			
		||||
 | 
			
		||||
    <PropertyGroup>
 | 
			
		||||
        <OutputType>Exe</OutputType>
 | 
			
		||||
        <TargetFramework>net9.0</TargetFramework>
 | 
			
		||||
        <ImplicitUsings>enable</ImplicitUsings>
 | 
			
		||||
        <Nullable>enable</Nullable>
 | 
			
		||||
        <UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId>
 | 
			
		||||
        <RootNamespace>DysonNetwork.Control</RootNamespace>
 | 
			
		||||
    </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
        <PackageReference Include="Aspire.Hosting.AppHost" Version="9.4.2"/>
 | 
			
		||||
        <PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" />
 | 
			
		||||
        <PackageReference Include="Aspire.Hosting.Nats" Version="9.4.2" />
 | 
			
		||||
        <PackageReference Include="Aspire.Hosting.Redis" Version="9.4.2" />
 | 
			
		||||
        <PackageReference Include="Aspire.Hosting.Yarp" Version="9.4.2-preview.1.25428.12" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
      <ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" />
 | 
			
		||||
      <ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" />
 | 
			
		||||
      <ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" />
 | 
			
		||||
      <ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" />
 | 
			
		||||
      <ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://json.schemastore.org/launchsettings.json",
 | 
			
		||||
  "profiles": {
 | 
			
		||||
    "https": {
 | 
			
		||||
      "commandName": "Project",
 | 
			
		||||
      "dotnetRunMessages": true,
 | 
			
		||||
      "launchBrowser": true,
 | 
			
		||||
      "applicationUrl": "https://localhost:17025;http://localhost:15057",
 | 
			
		||||
      "environmentVariables": {
 | 
			
		||||
        "ASPNETCORE_ENVIRONMENT": "Development",
 | 
			
		||||
        "DOTNET_ENVIRONMENT": "Development",
 | 
			
		||||
        "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21175",
 | 
			
		||||
        "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22189"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "http": {
 | 
			
		||||
      "commandName": "Project",
 | 
			
		||||
      "dotnetRunMessages": true,
 | 
			
		||||
      "launchBrowser": true,
 | 
			
		||||
      "applicationUrl": "http://localhost:15057",
 | 
			
		||||
      "environmentVariables": {
 | 
			
		||||
        "ASPNETCORE_ENVIRONMENT": "Development",
 | 
			
		||||
        "DOTNET_ENVIRONMENT": "Development",
 | 
			
		||||
        "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163",
 | 
			
		||||
        "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "Logging": {
 | 
			
		||||
    "LogLevel": {
 | 
			
		||||
      "Default": "Information",
 | 
			
		||||
      "Microsoft.AspNetCore": "Warning"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "ConnectionStrings": {
 | 
			
		||||
    "cache": "localhost:6379"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,53 +0,0 @@
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using DysonNetwork.Develop.Identity;
 | 
			
		||||
using DysonNetwork.Develop.Project;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Design;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop;
 | 
			
		||||
 | 
			
		||||
public class AppDatabase(
 | 
			
		||||
    DbContextOptions<AppDatabase> options,
 | 
			
		||||
    IConfiguration configuration
 | 
			
		||||
) : DbContext(options)
 | 
			
		||||
{
 | 
			
		||||
    public DbSet<Developer> Developers { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    public DbSet<DevProject> DevProjects { get; set; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    public DbSet<CustomApp> CustomApps { get; set; } = null!;
 | 
			
		||||
    public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } = null!;
 | 
			
		||||
    public DbSet<BotAccount> BotAccounts { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
 | 
			
		||||
    {
 | 
			
		||||
        optionsBuilder.UseNpgsql(
 | 
			
		||||
            configuration.GetConnectionString("App"),
 | 
			
		||||
            opt => opt
 | 
			
		||||
                .ConfigureDataSource(optSource => optSource.EnableDynamicJson())
 | 
			
		||||
                .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
 | 
			
		||||
                .UseNodaTime()
 | 
			
		||||
        ).UseSnakeCaseNamingConvention();
 | 
			
		||||
 | 
			
		||||
        base.OnConfiguring(optionsBuilder);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override void OnModelCreating(ModelBuilder modelBuilder)
 | 
			
		||||
    {
 | 
			
		||||
        base.OnModelCreating(modelBuilder);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
 | 
			
		||||
{
 | 
			
		||||
    public AppDatabase CreateDbContext(string[] args)
 | 
			
		||||
    {
 | 
			
		||||
        var configuration = new ConfigurationBuilder()
 | 
			
		||||
            .SetBasePath(Directory.GetCurrentDirectory())
 | 
			
		||||
            .AddJsonFile("appsettings.json")
 | 
			
		||||
            .Build();
 | 
			
		||||
 | 
			
		||||
        var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>();
 | 
			
		||||
        return new AppDatabase(optionsBuilder.Options, configuration);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
 | 
			
		||||
USER $APP_UID
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
EXPOSE 8080
 | 
			
		||||
EXPOSE 8081
 | 
			
		||||
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
 | 
			
		||||
ARG BUILD_CONFIGURATION=Release
 | 
			
		||||
WORKDIR /src
 | 
			
		||||
COPY ["DysonNetwork.Develop/DysonNetwork.Develop.csproj", "DysonNetwork.Develop/"]
 | 
			
		||||
RUN dotnet restore "DysonNetwork.Develop/DysonNetwork.Develop.csproj"
 | 
			
		||||
COPY . .
 | 
			
		||||
WORKDIR "/src/DysonNetwork.Develop"
 | 
			
		||||
RUN dotnet build "./DysonNetwork.Develop.csproj" -c $BUILD_CONFIGURATION -o /app/build
 | 
			
		||||
 | 
			
		||||
FROM build AS publish
 | 
			
		||||
ARG BUILD_CONFIGURATION=Release
 | 
			
		||||
RUN dotnet publish "./DysonNetwork.Develop.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
 | 
			
		||||
 | 
			
		||||
FROM base AS final
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
COPY --from=publish /app/publish .
 | 
			
		||||
ENTRYPOINT ["dotnet", "DysonNetwork.Develop.dll"]
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk.Web">
 | 
			
		||||
 | 
			
		||||
    <PropertyGroup>
 | 
			
		||||
        <TargetFramework>net9.0</TargetFramework>
 | 
			
		||||
        <Nullable>enable</Nullable>
 | 
			
		||||
        <ImplicitUsings>enable</ImplicitUsings>
 | 
			
		||||
        <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
 | 
			
		||||
    </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
        <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
 | 
			
		||||
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
 | 
			
		||||
        <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
 | 
			
		||||
            <PrivateAssets>all</PrivateAssets>
 | 
			
		||||
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
			
		||||
        </PackageReference>
 | 
			
		||||
        <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
 | 
			
		||||
        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
 | 
			
		||||
        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
 | 
			
		||||
        <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
 | 
			
		||||
        <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/>
 | 
			
		||||
        <PackageReference Include="NodaTime" Version="3.2.2"/>
 | 
			
		||||
        <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
 | 
			
		||||
        <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
      <Content Include="..\.dockerignore">
 | 
			
		||||
        <Link>.dockerignore</Link>
 | 
			
		||||
      </Content>
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" />
 | 
			
		||||
    <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 
 | 
			
		||||
</Project>
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using DysonNetwork.Develop.Project;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using NodaTime.Serialization.Protobuf;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Identity;
 | 
			
		||||
 | 
			
		||||
public class BotAccount : ModelBase
 | 
			
		||||
{
 | 
			
		||||
    public Guid Id { get; set; } = Guid.NewGuid();
 | 
			
		||||
    [MaxLength(1024)] public string Slug { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    public bool IsActive { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    public Guid ProjectId { get; set; }
 | 
			
		||||
    public DevProject Project { get; set; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [NotMapped] public AccountReference? Account { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// This developer field is to serve the transparent info for user to know which developer
 | 
			
		||||
    /// published this robot. Not for relationships usage.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotMapped] public Developer? Developer { get; set; }
 | 
			
		||||
 | 
			
		||||
    public Shared.Proto.BotAccount ToProtoValue()
 | 
			
		||||
    {
 | 
			
		||||
        var proto = new Shared.Proto.BotAccount
 | 
			
		||||
        {
 | 
			
		||||
            Slug = Slug,
 | 
			
		||||
            IsActive = IsActive,
 | 
			
		||||
            AutomatedId = Id.ToString(),
 | 
			
		||||
            CreatedAt = CreatedAt.ToTimestamp(),
 | 
			
		||||
            UpdatedAt = UpdatedAt.ToTimestamp()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return proto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static BotAccount FromProto(Shared.Proto.BotAccount proto)
 | 
			
		||||
    {
 | 
			
		||||
        var botAccount = new BotAccount
 | 
			
		||||
        {
 | 
			
		||||
            Id = Guid.Parse(proto.AutomatedId),
 | 
			
		||||
            Slug = proto.Slug,
 | 
			
		||||
            IsActive = proto.IsActive,
 | 
			
		||||
            CreatedAt = proto.CreatedAt.ToInstant(),
 | 
			
		||||
            UpdatedAt = proto.UpdatedAt.ToInstant()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return botAccount;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,460 +0,0 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using DysonNetwork.Develop.Project;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using DysonNetwork.Shared.Registry;
 | 
			
		||||
using Grpc.Core;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using NodaTime.Serialization.Protobuf;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Identity;
 | 
			
		||||
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Route("/api/developers/{pubName}/projects/{projectId:guid}/bots")]
 | 
			
		||||
[Authorize]
 | 
			
		||||
public class BotAccountController(
 | 
			
		||||
    BotAccountService botService,
 | 
			
		||||
    DeveloperService developerService,
 | 
			
		||||
    DevProjectService projectService,
 | 
			
		||||
    ILogger<BotAccountController> logger,
 | 
			
		||||
    AccountClientHelper accounts,
 | 
			
		||||
    BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver
 | 
			
		||||
)
 | 
			
		||||
    : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    public class CommonBotRequest
 | 
			
		||||
    {
 | 
			
		||||
        [MaxLength(256)] public string? FirstName { get; set; }
 | 
			
		||||
        [MaxLength(256)] public string? MiddleName { get; set; }
 | 
			
		||||
        [MaxLength(256)] public string? LastName { get; set; }
 | 
			
		||||
        [MaxLength(1024)] public string? Gender { get; set; }
 | 
			
		||||
        [MaxLength(1024)] public string? Pronouns { get; set; }
 | 
			
		||||
        [MaxLength(1024)] public string? TimeZone { get; set; }
 | 
			
		||||
        [MaxLength(1024)] public string? Location { get; set; }
 | 
			
		||||
        [MaxLength(4096)] public string? Bio { get; set; }
 | 
			
		||||
        public Instant? Birthday { get; set; }
 | 
			
		||||
 | 
			
		||||
        [MaxLength(32)] public string? PictureId { get; set; }
 | 
			
		||||
        [MaxLength(32)] public string? BackgroundId { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class BotCreateRequest : CommonBotRequest
 | 
			
		||||
    {
 | 
			
		||||
        [Required]
 | 
			
		||||
        [MinLength(2)]
 | 
			
		||||
        [MaxLength(256)]
 | 
			
		||||
        [RegularExpression(@"^[A-Za-z0-9_-]+$",
 | 
			
		||||
            ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.")
 | 
			
		||||
        ]
 | 
			
		||||
        public string Name { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
        [Required] [MaxLength(256)] public string Nick { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
        [Required] [MaxLength(1024)] public string Slug { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
        [MaxLength(128)] public string Language { get; set; } = "en-us";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class UpdateBotRequest : CommonBotRequest
 | 
			
		||||
    {
 | 
			
		||||
        [MinLength(2)]
 | 
			
		||||
        [MaxLength(256)]
 | 
			
		||||
        [RegularExpression(@"^[A-Za-z0-9_-]+$",
 | 
			
		||||
            ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.")
 | 
			
		||||
        ]
 | 
			
		||||
        public string? Name { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
        [MaxLength(256)] public string? Nick { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
        [Required] [MaxLength(1024)] public string? Slug { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
        [MaxLength(128)] public string? Language { get; set; }
 | 
			
		||||
 | 
			
		||||
        public bool? IsActive { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    public async Task<IActionResult> ListBots(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
			
		||||
                PublisherMemberRole.Viewer))
 | 
			
		||||
            return StatusCode(403, "You must be an viewer of the developer to list bots");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var bots = await botService.GetBotsByProjectAsync(projectId);
 | 
			
		||||
        return Ok(await botService.LoadBotsAccountAsync(bots));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("{botId:guid}")]
 | 
			
		||||
    public async Task<IActionResult> GetBot(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid botId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
			
		||||
                PublisherMemberRole.Viewer))
 | 
			
		||||
            return StatusCode(403, "You must be an viewer of the developer to view bot details");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var bot = await botService.GetBotByIdAsync(botId);
 | 
			
		||||
        if (bot is null || bot.ProjectId != projectId)
 | 
			
		||||
            return NotFound("Bot not found");
 | 
			
		||||
 | 
			
		||||
        return Ok(await botService.LoadBotAccountAsync(bot));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    public async Task<IActionResult> CreateBot(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromBody] BotCreateRequest createRequest
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
			
		||||
                PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to create a bot");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var now = SystemClock.Instance.GetCurrentInstant();
 | 
			
		||||
        var accountId = Guid.NewGuid();
 | 
			
		||||
        var account = new Account()
 | 
			
		||||
        {
 | 
			
		||||
            Id = accountId.ToString(),
 | 
			
		||||
            Name = createRequest.Name,
 | 
			
		||||
            Nick = createRequest.Nick,
 | 
			
		||||
            Language = createRequest.Language,
 | 
			
		||||
            Profile = new AccountProfile()
 | 
			
		||||
            {
 | 
			
		||||
                Id = Guid.NewGuid().ToString(),
 | 
			
		||||
                Bio = createRequest.Bio,
 | 
			
		||||
                Gender = createRequest.Gender,
 | 
			
		||||
                FirstName = createRequest.FirstName,
 | 
			
		||||
                MiddleName = createRequest.MiddleName,
 | 
			
		||||
                LastName = createRequest.LastName,
 | 
			
		||||
                TimeZone = createRequest.TimeZone,
 | 
			
		||||
                Pronouns = createRequest.Pronouns,
 | 
			
		||||
                Location = createRequest.Location,
 | 
			
		||||
                Birthday = createRequest.Birthday?.ToTimestamp(),
 | 
			
		||||
                AccountId = accountId.ToString(),
 | 
			
		||||
                CreatedAt = now.ToTimestamp(),
 | 
			
		||||
                UpdatedAt = now.ToTimestamp()
 | 
			
		||||
            },
 | 
			
		||||
            CreatedAt = now.ToTimestamp(),
 | 
			
		||||
            UpdatedAt = now.ToTimestamp()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var bot = await botService.CreateBotAsync(
 | 
			
		||||
                project,
 | 
			
		||||
                createRequest.Slug,
 | 
			
		||||
                account,
 | 
			
		||||
                createRequest.PictureId,
 | 
			
		||||
                createRequest.BackgroundId
 | 
			
		||||
            );
 | 
			
		||||
            return Ok(bot);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogError(ex, "Error creating bot account");
 | 
			
		||||
            return StatusCode(500, "An error occurred while creating the bot account");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPatch("{botId:guid}")]
 | 
			
		||||
    public async Task<IActionResult> UpdateBot(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid botId,
 | 
			
		||||
        [FromBody] UpdateBotRequest request
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
			
		||||
                PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to update a bot");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var bot = await botService.GetBotByIdAsync(botId);
 | 
			
		||||
        if (bot is null || bot.ProjectId != projectId)
 | 
			
		||||
            return NotFound("Bot not found");
 | 
			
		||||
 | 
			
		||||
        var botAccount = await accounts.GetBotAccount(bot.Id);
 | 
			
		||||
 | 
			
		||||
        if (request.Name is not null) botAccount.Name = request.Name;
 | 
			
		||||
        if (request.Nick is not null) botAccount.Nick = request.Nick;
 | 
			
		||||
        if (request.Language is not null) botAccount.Language = request.Language;
 | 
			
		||||
        if (request.Bio is not null) botAccount.Profile.Bio = request.Bio;
 | 
			
		||||
        if (request.Gender is not null) botAccount.Profile.Gender = request.Gender;
 | 
			
		||||
        if (request.FirstName is not null) botAccount.Profile.FirstName = request.FirstName;
 | 
			
		||||
        if (request.MiddleName is not null) botAccount.Profile.MiddleName = request.MiddleName;
 | 
			
		||||
        if (request.LastName is not null) botAccount.Profile.LastName = request.LastName;
 | 
			
		||||
        if (request.TimeZone is not null) botAccount.Profile.TimeZone = request.TimeZone;
 | 
			
		||||
        if (request.Pronouns is not null) botAccount.Profile.Pronouns = request.Pronouns;
 | 
			
		||||
        if (request.Location is not null) botAccount.Profile.Location = request.Location;
 | 
			
		||||
        if (request.Birthday is not null) botAccount.Profile.Birthday = request.Birthday?.ToTimestamp();
 | 
			
		||||
 | 
			
		||||
        if (request.Slug is not null) bot.Slug = request.Slug;
 | 
			
		||||
        if (request.IsActive is not null) bot.IsActive = request.IsActive.Value;
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var updatedBot = await botService.UpdateBotAsync(
 | 
			
		||||
                bot,
 | 
			
		||||
                botAccount,
 | 
			
		||||
                request.PictureId,
 | 
			
		||||
                request.BackgroundId
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return Ok(updatedBot);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogError(ex, "Error updating bot account {BotId}", botId);
 | 
			
		||||
            return StatusCode(500, "An error occurred while updating the bot account");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpDelete("{botId:guid}")]
 | 
			
		||||
    public async Task<IActionResult> DeleteBot(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid botId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
 | 
			
		||||
                PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to delete a bot");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var bot = await botService.GetBotByIdAsync(botId);
 | 
			
		||||
        if (bot is null || bot.ProjectId != projectId)
 | 
			
		||||
            return NotFound("Bot not found");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await botService.DeleteBotAsync(bot);
 | 
			
		||||
            return NoContent();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogError(ex, "Error deleting bot {BotId}", botId);
 | 
			
		||||
            return StatusCode(500, "An error occurred while deleting the bot account");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("{botId:guid}/keys")]
 | 
			
		||||
    public async Task<ActionResult<List<ApiKeyReference>>> ListBotKeys(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid botId
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer);
 | 
			
		||||
        if (developer == null) return NotFound("Developer not found");
 | 
			
		||||
        if (project == null) return NotFound("Project not found or you don't have access");
 | 
			
		||||
        if (bot == null) return NotFound("Bot not found");
 | 
			
		||||
 | 
			
		||||
        var keys = await accountsReceiver.ListApiKeyAsync(new ListApiKeyRequest
 | 
			
		||||
        {
 | 
			
		||||
            AutomatedId = bot.Id.ToString()
 | 
			
		||||
        });
 | 
			
		||||
        var data = keys.Data.Select(ApiKeyReference.FromProtoValue).ToList();
 | 
			
		||||
 | 
			
		||||
        return Ok(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("{botId:guid}/keys/{keyId:guid}")]
 | 
			
		||||
    public async Task<ActionResult<ApiKeyReference>> GetBotKey(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid botId,
 | 
			
		||||
        [FromRoute] Guid keyId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Viewer);
 | 
			
		||||
        if (developer == null) return NotFound("Developer not found");
 | 
			
		||||
        if (project == null) return NotFound("Project not found or you don't have access");
 | 
			
		||||
        if (bot == null) return NotFound("Bot not found");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var key = await accountsReceiver.GetApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
 | 
			
		||||
            if (key == null) return NotFound("API key not found");
 | 
			
		||||
            return Ok(ApiKeyReference.FromProtoValue(key));
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
 | 
			
		||||
        {
 | 
			
		||||
            return NotFound("API key not found");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class CreateApiKeyRequest
 | 
			
		||||
    {
 | 
			
		||||
        [Required, MaxLength(1024)]
 | 
			
		||||
        public string Label { get; set; } = null!;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost("{botId:guid}/keys")]
 | 
			
		||||
    public async Task<ActionResult<ApiKeyReference>> CreateBotKey(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid botId,
 | 
			
		||||
        [FromBody] CreateApiKeyRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor);
 | 
			
		||||
        if (developer == null) return NotFound("Developer not found");
 | 
			
		||||
        if (project == null) return NotFound("Project not found or you don't have access");
 | 
			
		||||
        if (bot == null) return NotFound("Bot not found");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var newKey = new ApiKey
 | 
			
		||||
            {
 | 
			
		||||
                AccountId = bot.Id.ToString(),
 | 
			
		||||
                Label = request.Label
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            var createdKey = await accountsReceiver.CreateApiKeyAsync(newKey);
 | 
			
		||||
            return Ok(ApiKeyReference.FromProtoValue(createdKey));
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument)
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest(ex.Status.Detail);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost("{botId:guid}/keys/{keyId:guid}/rotate")]
 | 
			
		||||
    public async Task<ActionResult<ApiKeyReference>> RotateBotKey(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid botId,
 | 
			
		||||
        [FromRoute] Guid keyId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor);
 | 
			
		||||
        if (developer == null) return NotFound("Developer not found");
 | 
			
		||||
        if (project == null) return NotFound("Project not found or you don't have access");
 | 
			
		||||
        if (bot == null) return NotFound("Bot not found");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var rotatedKey = await accountsReceiver.RotateApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
 | 
			
		||||
            return Ok(ApiKeyReference.FromProtoValue(rotatedKey));
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
 | 
			
		||||
        {
 | 
			
		||||
            return NotFound("API key not found");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpDelete("{botId:guid}/keys/{keyId:guid}")]
 | 
			
		||||
    public async Task<IActionResult> DeleteBotKey(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid botId,
 | 
			
		||||
        [FromRoute] Guid keyId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, PublisherMemberRole.Editor);
 | 
			
		||||
        if (developer == null) return NotFound("Developer not found");
 | 
			
		||||
        if (project == null) return NotFound("Project not found or you don't have access");
 | 
			
		||||
        if (bot == null) return NotFound("Bot not found");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await accountsReceiver.DeleteApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
 | 
			
		||||
            return NoContent();
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
 | 
			
		||||
        {
 | 
			
		||||
            return NotFound("API key not found");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<(Developer?, DevProject?, BotAccount?)> ValidateBotAccess(
 | 
			
		||||
        string pubName,
 | 
			
		||||
        Guid projectId,
 | 
			
		||||
        Guid botId,
 | 
			
		||||
        Account currentUser,
 | 
			
		||||
        PublisherMemberRole requiredRole)
 | 
			
		||||
    {
 | 
			
		||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer == null) return (null, null, null);
 | 
			
		||||
 | 
			
		||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), requiredRole))
 | 
			
		||||
            return (null, null, null);
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project == null) return (developer, null, null);
 | 
			
		||||
 | 
			
		||||
        var bot = await botService.GetBotByIdAsync(botId);
 | 
			
		||||
        if (bot == null || bot.ProjectId != projectId) return (developer, project, null);
 | 
			
		||||
 | 
			
		||||
        return (developer, project, bot);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Identity;
 | 
			
		||||
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Route("api/bots")]
 | 
			
		||||
public class BotAccountPublicController(BotAccountService botService, DeveloperService developerService) : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    [HttpGet("{botId:guid}")]
 | 
			
		||||
    public async Task<ActionResult<BotAccount>> GetBotTransparentInfo([FromRoute] Guid botId)
 | 
			
		||||
    {
 | 
			
		||||
        var bot = await botService.GetBotByIdAsync(botId);
 | 
			
		||||
        if (bot is null) return NotFound("Bot not found");
 | 
			
		||||
        bot = await botService.LoadBotAccountAsync(bot);
 | 
			
		||||
 | 
			
		||||
        var developer = await developerService.GetDeveloperById(bot!.Project.DeveloperId);
 | 
			
		||||
        if (developer is null) return NotFound("Developer not found");
 | 
			
		||||
        bot.Developer = await developerService.LoadDeveloperPublisher(developer);
 | 
			
		||||
 | 
			
		||||
        return Ok(bot);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("{botId:guid}/developer")]
 | 
			
		||||
    public async Task<ActionResult<Developer>> GetBotDeveloper([FromRoute] Guid botId)
 | 
			
		||||
    {
 | 
			
		||||
        var bot = await botService.GetBotByIdAsync(botId);
 | 
			
		||||
        if (bot is null) return NotFound("Bot not found");
 | 
			
		||||
        
 | 
			
		||||
        var developer = await developerService.GetDeveloperById(bot!.Project.DeveloperId);
 | 
			
		||||
        if (developer is null) return NotFound("Developer not found");
 | 
			
		||||
        developer = await developerService.LoadDeveloperPublisher(developer);
 | 
			
		||||
 | 
			
		||||
        return Ok(developer);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,174 +0,0 @@
 | 
			
		||||
using DysonNetwork.Develop.Project;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using DysonNetwork.Shared.Registry;
 | 
			
		||||
using Grpc.Core;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NodaTime.Serialization.Protobuf;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Identity;
 | 
			
		||||
 | 
			
		||||
public class BotAccountService(
 | 
			
		||||
    AppDatabase db,
 | 
			
		||||
    BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver,
 | 
			
		||||
    AccountClientHelper accounts
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    public async Task<BotAccount?> GetBotByIdAsync(Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        return await db.BotAccounts
 | 
			
		||||
            .Include(b => b.Project)
 | 
			
		||||
            .FirstOrDefaultAsync(b => b.Id == id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<IEnumerable<BotAccount>> GetBotsByProjectAsync(Guid projectId)
 | 
			
		||||
    {
 | 
			
		||||
        return await db.BotAccounts
 | 
			
		||||
            .Where(b => b.ProjectId == projectId)
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<BotAccount> CreateBotAsync(
 | 
			
		||||
        DevProject project,
 | 
			
		||||
        string slug,
 | 
			
		||||
        Account account,
 | 
			
		||||
        string? pictureId,
 | 
			
		||||
        string? backgroundId
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        // First, check if a bot with this slug already exists in this project
 | 
			
		||||
        var existingBot = await db.BotAccounts
 | 
			
		||||
            .FirstOrDefaultAsync(b => b.ProjectId == project.Id && b.Slug == slug);
 | 
			
		||||
 | 
			
		||||
        if (existingBot != null)
 | 
			
		||||
            throw new InvalidOperationException("A bot with this slug already exists in this project.");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var automatedId = Guid.NewGuid();
 | 
			
		||||
            var createRequest = new CreateBotAccountRequest
 | 
			
		||||
            {
 | 
			
		||||
                AutomatedId = automatedId.ToString(),
 | 
			
		||||
                Account = account,
 | 
			
		||||
                PictureId = pictureId,
 | 
			
		||||
                BackgroundId = backgroundId
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            var createResponse = await accountReceiver.CreateBotAccountAsync(createRequest);
 | 
			
		||||
            var botAccount = createResponse.Bot;
 | 
			
		||||
 | 
			
		||||
            // Then create the local bot account
 | 
			
		||||
            var bot = new BotAccount
 | 
			
		||||
            {
 | 
			
		||||
                Id = automatedId,
 | 
			
		||||
                Slug = slug,
 | 
			
		||||
                ProjectId = project.Id,
 | 
			
		||||
                Project = project,
 | 
			
		||||
                IsActive = botAccount.IsActive,
 | 
			
		||||
                CreatedAt = botAccount.CreatedAt.ToInstant(),
 | 
			
		||||
                UpdatedAt = botAccount.UpdatedAt.ToInstant()
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            db.BotAccounts.Add(bot);
 | 
			
		||||
            await db.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
            return bot;
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists)
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException(
 | 
			
		||||
                "A bot account with this ID already exists in the authentication service.", ex);
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument)
 | 
			
		||||
        {
 | 
			
		||||
            throw new ArgumentException($"Invalid bot account data: {ex.Status.Detail}", ex);
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException ex)
 | 
			
		||||
        {
 | 
			
		||||
            throw new Exception($"Failed to create bot account: {ex.Status.Detail}", ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<BotAccount> UpdateBotAsync(
 | 
			
		||||
        BotAccount bot,
 | 
			
		||||
        Account account,
 | 
			
		||||
        string? pictureId,
 | 
			
		||||
        string? backgroundId
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        db.Update(bot);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
        
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // Update the bot account in the Pass service
 | 
			
		||||
            var updateRequest = new UpdateBotAccountRequest
 | 
			
		||||
            {
 | 
			
		||||
                AutomatedId = bot.Id.ToString(),
 | 
			
		||||
                Account = account,
 | 
			
		||||
                PictureId = pictureId,
 | 
			
		||||
                BackgroundId = backgroundId
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            var updateResponse = await accountReceiver.UpdateBotAccountAsync(updateRequest);
 | 
			
		||||
            var updatedBot = updateResponse.Bot;
 | 
			
		||||
 | 
			
		||||
            // Update local bot account
 | 
			
		||||
            bot.UpdatedAt = updatedBot.UpdatedAt.ToInstant();
 | 
			
		||||
            bot.IsActive = updatedBot.IsActive;
 | 
			
		||||
            await db.SaveChangesAsync();
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
 | 
			
		||||
        {
 | 
			
		||||
            throw new Exception("Bot account not found in the authentication service", ex);
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException ex)
 | 
			
		||||
        {
 | 
			
		||||
            throw new Exception($"Failed to update bot account: {ex.Status.Detail}", ex);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return bot;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task DeleteBotAsync(BotAccount bot)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // Delete the bot account from the Pass service
 | 
			
		||||
            var deleteRequest = new DeleteBotAccountRequest
 | 
			
		||||
            {
 | 
			
		||||
                AutomatedId = bot.Id.ToString(),
 | 
			
		||||
                Force = false
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await accountReceiver.DeleteBotAccountAsync(deleteRequest);
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
 | 
			
		||||
        {
 | 
			
		||||
            // Account not found in Pass service, continue with local deletion
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Delete the local bot account
 | 
			
		||||
        db.BotAccounts.Remove(bot);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<BotAccount?> LoadBotAccountAsync(BotAccount bot) =>
 | 
			
		||||
        (await LoadBotsAccountAsync([bot])).FirstOrDefault();
 | 
			
		||||
 | 
			
		||||
    public async Task<List<BotAccount>> LoadBotsAccountAsync(IEnumerable<BotAccount> bots)
 | 
			
		||||
    {
 | 
			
		||||
        bots = bots.ToList();
 | 
			
		||||
        var automatedIds = bots.Select(b => b.Id).ToList();
 | 
			
		||||
        var data = await accounts.GetBotAccountBatch(automatedIds);
 | 
			
		||||
 | 
			
		||||
        foreach (var bot in bots)
 | 
			
		||||
        {
 | 
			
		||||
            bot.Account = data
 | 
			
		||||
                .Select(AccountReference.FromProtoValue)
 | 
			
		||||
                .FirstOrDefault(e => e.AutomatedId == bot.Id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return bots as List<BotAccount> ?? [];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,178 +0,0 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
using DysonNetwork.Develop.Project;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using Google.Protobuf;
 | 
			
		||||
using Google.Protobuf.WellKnownTypes;
 | 
			
		||||
using NodaTime.Serialization.Protobuf;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Identity;
 | 
			
		||||
 | 
			
		||||
public enum CustomAppStatus
 | 
			
		||||
{
 | 
			
		||||
    Developing,
 | 
			
		||||
    Staging,
 | 
			
		||||
    Production,
 | 
			
		||||
    Suspended
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class CustomApp : ModelBase, IIdentifiedResource
 | 
			
		||||
{
 | 
			
		||||
    public Guid Id { get; set; } = Guid.NewGuid();
 | 
			
		||||
    [MaxLength(1024)] public string Slug { get; set; } = null!;
 | 
			
		||||
    [MaxLength(1024)] public string Name { get; set; } = null!;
 | 
			
		||||
    [MaxLength(4096)] public string? Description { get; set; }
 | 
			
		||||
    public CustomAppStatus Status { get; set; } = CustomAppStatus.Developing;
 | 
			
		||||
 | 
			
		||||
    [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
 | 
			
		||||
    [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; }
 | 
			
		||||
    [Column(TypeName = "jsonb")] public CustomAppOauthConfig? OauthConfig { get; set; }
 | 
			
		||||
    [Column(TypeName = "jsonb")] public CustomAppLinks? Links { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
 | 
			
		||||
 | 
			
		||||
    public Guid ProjectId { get; set; }
 | 
			
		||||
    public DevProject Project { get; set; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [NotMapped]
 | 
			
		||||
    public Developer Developer => Project.Developer;
 | 
			
		||||
 | 
			
		||||
    [NotMapped] public string ResourceIdentifier => "custom-app:" + Id;
 | 
			
		||||
 | 
			
		||||
    public Shared.Proto.CustomApp ToProto()
 | 
			
		||||
    {
 | 
			
		||||
        return new Shared.Proto.CustomApp
 | 
			
		||||
        {
 | 
			
		||||
            Id = Id.ToString(),
 | 
			
		||||
            Slug = Slug,
 | 
			
		||||
            Name = Name,
 | 
			
		||||
            Description = Description ?? string.Empty,
 | 
			
		||||
            Status = Status switch
 | 
			
		||||
            {
 | 
			
		||||
                CustomAppStatus.Developing => Shared.Proto.CustomAppStatus.Developing,
 | 
			
		||||
                CustomAppStatus.Staging => Shared.Proto.CustomAppStatus.Staging,
 | 
			
		||||
                CustomAppStatus.Production => Shared.Proto.CustomAppStatus.Production,
 | 
			
		||||
                CustomAppStatus.Suspended => Shared.Proto.CustomAppStatus.Suspended,
 | 
			
		||||
                _ => Shared.Proto.CustomAppStatus.Unspecified
 | 
			
		||||
            },
 | 
			
		||||
            Picture = Picture?.ToProtoValue(),
 | 
			
		||||
            Background = Background?.ToProtoValue(),
 | 
			
		||||
            Verification = Verification?.ToProtoValue(),
 | 
			
		||||
            Links = Links is null ? null : new DysonNetwork.Shared.Proto.CustomAppLinks
 | 
			
		||||
            {
 | 
			
		||||
                HomePage = Links.HomePage ?? string.Empty,
 | 
			
		||||
                PrivacyPolicy = Links.PrivacyPolicy ?? string.Empty,
 | 
			
		||||
                TermsOfService = Links.TermsOfService ?? string.Empty
 | 
			
		||||
            },
 | 
			
		||||
            OauthConfig = OauthConfig is null ? null : new DysonNetwork.Shared.Proto.CustomAppOauthConfig
 | 
			
		||||
            {
 | 
			
		||||
                ClientUri = OauthConfig.ClientUri ?? string.Empty,
 | 
			
		||||
                RedirectUris = { OauthConfig.RedirectUris ?? [] },
 | 
			
		||||
                PostLogoutRedirectUris = { OauthConfig.PostLogoutRedirectUris ?? [] },
 | 
			
		||||
                AllowedScopes = { OauthConfig.AllowedScopes ?? [] },
 | 
			
		||||
                AllowedGrantTypes = { OauthConfig.AllowedGrantTypes ?? [] },
 | 
			
		||||
                RequirePkce = OauthConfig.RequirePkce,
 | 
			
		||||
                AllowOfflineAccess = OauthConfig.AllowOfflineAccess
 | 
			
		||||
            },
 | 
			
		||||
            ProjectId = ProjectId.ToString(),
 | 
			
		||||
            CreatedAt = CreatedAt.ToTimestamp(),
 | 
			
		||||
            UpdatedAt = UpdatedAt.ToTimestamp()
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public CustomApp FromProtoValue(Shared.Proto.CustomApp p)
 | 
			
		||||
    {
 | 
			
		||||
        Id = Guid.Parse(p.Id);
 | 
			
		||||
        Slug = p.Slug;
 | 
			
		||||
        Name = p.Name;
 | 
			
		||||
        Description = string.IsNullOrEmpty(p.Description) ? null : p.Description;
 | 
			
		||||
        Status = p.Status switch
 | 
			
		||||
        {
 | 
			
		||||
            Shared.Proto.CustomAppStatus.Developing => CustomAppStatus.Developing,
 | 
			
		||||
            Shared.Proto.CustomAppStatus.Staging => CustomAppStatus.Staging,
 | 
			
		||||
            Shared.Proto.CustomAppStatus.Production => CustomAppStatus.Production,
 | 
			
		||||
            Shared.Proto.CustomAppStatus.Suspended => CustomAppStatus.Suspended,
 | 
			
		||||
            _ => CustomAppStatus.Developing
 | 
			
		||||
        };
 | 
			
		||||
        ProjectId = string.IsNullOrEmpty(p.ProjectId) ? Guid.Empty : Guid.Parse(p.ProjectId);
 | 
			
		||||
        CreatedAt = p.CreatedAt.ToInstant();
 | 
			
		||||
        UpdatedAt = p.UpdatedAt.ToInstant();
 | 
			
		||||
        if (p.Picture is not null) Picture = CloudFileReferenceObject.FromProtoValue(p.Picture);
 | 
			
		||||
        if (p.Background is not null) Background = CloudFileReferenceObject.FromProtoValue(p.Background);
 | 
			
		||||
        if (p.Verification is not null) Verification = VerificationMark.FromProtoValue(p.Verification);
 | 
			
		||||
        if (p.Links is not null)
 | 
			
		||||
        {
 | 
			
		||||
            Links = new CustomAppLinks
 | 
			
		||||
            {
 | 
			
		||||
                HomePage = string.IsNullOrEmpty(p.Links.HomePage) ? null : p.Links.HomePage,
 | 
			
		||||
                PrivacyPolicy = string.IsNullOrEmpty(p.Links.PrivacyPolicy) ? null : p.Links.PrivacyPolicy,
 | 
			
		||||
                TermsOfService = string.IsNullOrEmpty(p.Links.TermsOfService) ? null : p.Links.TermsOfService
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class CustomAppLinks
 | 
			
		||||
{
 | 
			
		||||
    [MaxLength(8192)] public string? HomePage { get; set; }
 | 
			
		||||
    [MaxLength(8192)] public string? PrivacyPolicy { get; set; }
 | 
			
		||||
    [MaxLength(8192)] public string? TermsOfService { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class CustomAppOauthConfig
 | 
			
		||||
{
 | 
			
		||||
    [MaxLength(1024)] public string? ClientUri { get; set; }
 | 
			
		||||
    [MaxLength(4096)] public string[] RedirectUris { get; set; } = [];
 | 
			
		||||
    [MaxLength(4096)] public string[]? PostLogoutRedirectUris { get; set; }
 | 
			
		||||
    [MaxLength(256)] public string[]? AllowedScopes { get; set; } = ["openid", "profile", "email"];
 | 
			
		||||
    [MaxLength(256)] public string[] AllowedGrantTypes { get; set; } = ["authorization_code", "refresh_token"];
 | 
			
		||||
    public bool RequirePkce { get; set; } = true;
 | 
			
		||||
    public bool AllowOfflineAccess { get; set; } = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class CustomAppSecret : ModelBase
 | 
			
		||||
{
 | 
			
		||||
    public Guid Id { get; set; } = Guid.NewGuid();
 | 
			
		||||
    [MaxLength(1024)] public string Secret { get; set; } = null!;
 | 
			
		||||
    [MaxLength(4096)] public string? Description { get; set; } = null!;
 | 
			
		||||
    public Instant? ExpiredAt { get; set; }
 | 
			
		||||
    public bool IsOidc { get; set; } = false; // Indicates if this secret is for OIDC/OAuth
 | 
			
		||||
 | 
			
		||||
    public Guid AppId { get; set; }
 | 
			
		||||
    public CustomApp App { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static CustomAppSecret FromProtoValue(DysonNetwork.Shared.Proto.CustomAppSecret p)
 | 
			
		||||
    {
 | 
			
		||||
        return new CustomAppSecret
 | 
			
		||||
        {
 | 
			
		||||
            Id = Guid.Parse(p.Id),
 | 
			
		||||
            Secret = p.Secret,
 | 
			
		||||
            Description = p.Description,
 | 
			
		||||
            ExpiredAt = p.ExpiredAt?.ToInstant(),
 | 
			
		||||
            IsOidc = p.IsOidc,
 | 
			
		||||
            AppId = Guid.Parse(p.AppId),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public DysonNetwork.Shared.Proto.CustomAppSecret ToProto()
 | 
			
		||||
    {
 | 
			
		||||
        return new DysonNetwork.Shared.Proto.CustomAppSecret
 | 
			
		||||
        {
 | 
			
		||||
            Id = Id.ToString(),
 | 
			
		||||
            Secret = Secret,
 | 
			
		||||
            Description = Description,
 | 
			
		||||
            ExpiredAt = ExpiredAt?.ToTimestamp(),
 | 
			
		||||
            IsOidc = IsOidc,
 | 
			
		||||
            AppId = Id.ToString(),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,431 +0,0 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using DysonNetwork.Develop.Project;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Identity;
 | 
			
		||||
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Route("/api/developers/{pubName}/projects/{projectId:guid}/apps")]
 | 
			
		||||
public class CustomAppController(CustomAppService customApps, DeveloperService ds, DevProjectService projectService)
 | 
			
		||||
    : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    public record CustomAppRequest(
 | 
			
		||||
        [MaxLength(1024)] string? Slug,
 | 
			
		||||
        [MaxLength(1024)] string? Name,
 | 
			
		||||
        [MaxLength(4096)] string? Description,
 | 
			
		||||
        string? PictureId,
 | 
			
		||||
        string? BackgroundId,
 | 
			
		||||
        CustomAppStatus? Status,
 | 
			
		||||
        CustomAppLinks? Links,
 | 
			
		||||
        CustomAppOauthConfig? OauthConfig
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    public record CreateSecretRequest(
 | 
			
		||||
        [MaxLength(4096)] string? Description,
 | 
			
		||||
        TimeSpan? ExpiresIn = null,
 | 
			
		||||
        bool IsOidc = false
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    public record SecretResponse(
 | 
			
		||||
        string Id,
 | 
			
		||||
        string? Secret,
 | 
			
		||||
        string? Description,
 | 
			
		||||
        Instant? ExpiresAt,
 | 
			
		||||
        bool IsOidc,
 | 
			
		||||
        Instant CreatedAt,
 | 
			
		||||
        Instant UpdatedAt
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> ListApps([FromRoute] string pubName, [FromRoute] Guid projectId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null) return NotFound();
 | 
			
		||||
 | 
			
		||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
			
		||||
        if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Viewer))
 | 
			
		||||
            return StatusCode(403, "You must be a viewer of the developer to list custom apps");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null) return NotFound();
 | 
			
		||||
 | 
			
		||||
        var apps = await customApps.GetAppsByProjectAsync(projectId);
 | 
			
		||||
        return Ok(apps);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("{appId:guid}")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> GetApp([FromRoute] string pubName, [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid appId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
        
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null) return NotFound();
 | 
			
		||||
        
 | 
			
		||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
			
		||||
        if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, PublisherMemberRole.Viewer))
 | 
			
		||||
            return StatusCode(403, "You must be a viewer of the developer to list custom apps");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null) return NotFound();
 | 
			
		||||
 | 
			
		||||
        var app = await customApps.GetAppAsync(appId, projectId);
 | 
			
		||||
        if (app == null)
 | 
			
		||||
            return NotFound();
 | 
			
		||||
 | 
			
		||||
        return Ok(app);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> CreateApp(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromBody] CustomAppRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to create a custom app");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Slug))
 | 
			
		||||
            return BadRequest("Name and slug are required");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var app = await customApps.CreateAppAsync(projectId, request);
 | 
			
		||||
            if (app == null)
 | 
			
		||||
                return BadRequest("Failed to create app");
 | 
			
		||||
 | 
			
		||||
            return CreatedAtAction(
 | 
			
		||||
                nameof(GetApp),
 | 
			
		||||
                new { pubName, projectId, appId = app.Id },
 | 
			
		||||
                app
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        catch (InvalidOperationException ex)
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPatch("{appId:guid}")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> UpdateApp(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid appId,
 | 
			
		||||
        [FromBody] CustomAppRequest request
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to update a custom app");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var app = await customApps.GetAppAsync(appId, projectId);
 | 
			
		||||
        if (app == null)
 | 
			
		||||
            return NotFound();
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            app = await customApps.UpdateAppAsync(app, request);
 | 
			
		||||
            return Ok(app);
 | 
			
		||||
        }
 | 
			
		||||
        catch (InvalidOperationException ex)
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpDelete("{appId:guid}")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> DeleteApp(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid appId
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to delete a custom app");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var app = await customApps.GetAppAsync(appId, projectId);
 | 
			
		||||
        if (app == null)
 | 
			
		||||
            return NotFound();
 | 
			
		||||
 | 
			
		||||
        var result = await customApps.DeleteAppAsync(appId);
 | 
			
		||||
        if (!result)
 | 
			
		||||
            return NotFound();
 | 
			
		||||
 | 
			
		||||
        return NoContent();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("{appId:guid}/secrets")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> ListSecrets(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid appId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to view app secrets");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var app = await customApps.GetAppAsync(appId, projectId);
 | 
			
		||||
        if (app == null)
 | 
			
		||||
            return NotFound("App not found");
 | 
			
		||||
 | 
			
		||||
        var secrets = await customApps.GetAppSecretsAsync(appId);
 | 
			
		||||
        return Ok(secrets.Select(s => new SecretResponse(
 | 
			
		||||
            s.Id.ToString(),
 | 
			
		||||
            null,
 | 
			
		||||
            s.Description,
 | 
			
		||||
            s.ExpiredAt,
 | 
			
		||||
            s.IsOidc,
 | 
			
		||||
            s.CreatedAt,
 | 
			
		||||
            s.UpdatedAt
 | 
			
		||||
        )));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost("{appId:guid}/secrets")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> CreateSecret(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid appId,
 | 
			
		||||
        [FromBody] CreateSecretRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to create app secrets");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var app = await customApps.GetAppAsync(appId, projectId);
 | 
			
		||||
        if (app == null)
 | 
			
		||||
            return NotFound("App not found");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var secret = await customApps.CreateAppSecretAsync(new CustomAppSecret
 | 
			
		||||
            {
 | 
			
		||||
                AppId = appId,
 | 
			
		||||
                Description = request.Description,
 | 
			
		||||
                ExpiredAt = request.ExpiresIn.HasValue
 | 
			
		||||
                    ? NodaTime.SystemClock.Instance.GetCurrentInstant()
 | 
			
		||||
                        .Plus(Duration.FromTimeSpan(request.ExpiresIn.Value))
 | 
			
		||||
                    : (NodaTime.Instant?)null,
 | 
			
		||||
                IsOidc = request.IsOidc
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return CreatedAtAction(
 | 
			
		||||
                nameof(GetSecret),
 | 
			
		||||
                new { pubName, projectId, appId, secretId = secret.Id },
 | 
			
		||||
                new SecretResponse(
 | 
			
		||||
                    secret.Id.ToString(),
 | 
			
		||||
                    secret.Secret,
 | 
			
		||||
                    secret.Description,
 | 
			
		||||
                    secret.ExpiredAt,
 | 
			
		||||
                    secret.IsOidc,
 | 
			
		||||
                    secret.CreatedAt,
 | 
			
		||||
                    secret.UpdatedAt
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        catch (InvalidOperationException ex)
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("{appId:guid}/secrets/{secretId:guid}")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> GetSecret(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid appId,
 | 
			
		||||
        [FromRoute] Guid secretId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to view app secrets");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var app = await customApps.GetAppAsync(appId, projectId);
 | 
			
		||||
        if (app == null)
 | 
			
		||||
            return NotFound("App not found");
 | 
			
		||||
 | 
			
		||||
        var secret = await customApps.GetAppSecretAsync(secretId, appId);
 | 
			
		||||
        if (secret == null)
 | 
			
		||||
            return NotFound("Secret not found");
 | 
			
		||||
 | 
			
		||||
        return Ok(new SecretResponse(
 | 
			
		||||
            secret.Id.ToString(),
 | 
			
		||||
            null,
 | 
			
		||||
            secret.Description,
 | 
			
		||||
            secret.ExpiredAt,
 | 
			
		||||
            secret.IsOidc,
 | 
			
		||||
            secret.CreatedAt,
 | 
			
		||||
            secret.UpdatedAt
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpDelete("{appId:guid}/secrets/{secretId:guid}")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> DeleteSecret(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid appId,
 | 
			
		||||
        [FromRoute] Guid secretId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to delete app secrets");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var app = await customApps.GetAppAsync(appId, projectId);
 | 
			
		||||
        if (app == null)
 | 
			
		||||
            return NotFound("App not found");
 | 
			
		||||
 | 
			
		||||
        var secret = await customApps.GetAppSecretAsync(secretId, appId);
 | 
			
		||||
        if (secret == null)
 | 
			
		||||
            return NotFound("Secret not found");
 | 
			
		||||
 | 
			
		||||
        var result = await customApps.DeleteAppSecretAsync(secretId, appId);
 | 
			
		||||
        if (!result)
 | 
			
		||||
            return NotFound("Failed to delete secret");
 | 
			
		||||
 | 
			
		||||
        return NoContent();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost("{appId:guid}/secrets/{secretId:guid}/rotate")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> RotateSecret(
 | 
			
		||||
        [FromRoute] string pubName,
 | 
			
		||||
        [FromRoute] Guid projectId,
 | 
			
		||||
        [FromRoute] Guid appId,
 | 
			
		||||
        [FromRoute] Guid secretId,
 | 
			
		||||
        [FromBody] CreateSecretRequest? request = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser)
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
 | 
			
		||||
        if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to rotate app secrets");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(projectId, developer.Id);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound("Project not found or you don't have access");
 | 
			
		||||
 | 
			
		||||
        var app = await customApps.GetAppAsync(appId, projectId);
 | 
			
		||||
        if (app == null)
 | 
			
		||||
            return NotFound("App not found");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var secret = await customApps.RotateAppSecretAsync(new CustomAppSecret
 | 
			
		||||
            {
 | 
			
		||||
                Id = secretId,
 | 
			
		||||
                AppId = appId,
 | 
			
		||||
                Description = request?.Description,
 | 
			
		||||
                ExpiredAt = request?.ExpiresIn.HasValue == true
 | 
			
		||||
                    ? NodaTime.SystemClock.Instance.GetCurrentInstant()
 | 
			
		||||
                        .Plus(Duration.FromTimeSpan(request.ExpiresIn.Value))
 | 
			
		||||
                    : (NodaTime.Instant?)null,
 | 
			
		||||
                IsOidc = request?.IsOidc ?? false
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return Ok(new SecretResponse(
 | 
			
		||||
                secret.Id.ToString(),
 | 
			
		||||
                secret.Secret,
 | 
			
		||||
                secret.Description,
 | 
			
		||||
                secret.ExpiredAt,
 | 
			
		||||
                secret.IsOidc,
 | 
			
		||||
                secret.CreatedAt,
 | 
			
		||||
                secret.UpdatedAt
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
        catch (InvalidOperationException ex)
 | 
			
		||||
        {
 | 
			
		||||
            return BadRequest(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,269 +0,0 @@
 | 
			
		||||
using DysonNetwork.Develop.Project;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using System.Security.Cryptography;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Identity;
 | 
			
		||||
 | 
			
		||||
public class CustomAppService(
 | 
			
		||||
    AppDatabase db,
 | 
			
		||||
    FileReferenceService.FileReferenceServiceClient fileRefs,
 | 
			
		||||
    FileService.FileServiceClient files
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    public async Task<CustomApp?> CreateAppAsync(
 | 
			
		||||
        Guid projectId,
 | 
			
		||||
        CustomAppController.CustomAppRequest request
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        var project = await db.DevProjects
 | 
			
		||||
            .Include(p => p.Developer)
 | 
			
		||||
            .FirstOrDefaultAsync(p => p.Id == projectId);
 | 
			
		||||
            
 | 
			
		||||
        if (project == null)
 | 
			
		||||
            return null;
 | 
			
		||||
            
 | 
			
		||||
        var app = new CustomApp
 | 
			
		||||
        {
 | 
			
		||||
            Slug = request.Slug!,
 | 
			
		||||
            Name = request.Name!,
 | 
			
		||||
            Description = request.Description,
 | 
			
		||||
            Status = request.Status ?? CustomAppStatus.Developing,
 | 
			
		||||
            Links = request.Links,
 | 
			
		||||
            OauthConfig = request.OauthConfig,
 | 
			
		||||
            ProjectId = projectId
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (request.PictureId is not null)
 | 
			
		||||
        {
 | 
			
		||||
            var picture = await files.GetFileAsync(
 | 
			
		||||
                new GetFileRequest
 | 
			
		||||
                {
 | 
			
		||||
                    Id = request.PictureId
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            if (picture is null)
 | 
			
		||||
                throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
 | 
			
		||||
            app.Picture = CloudFileReferenceObject.FromProtoValue(picture);
 | 
			
		||||
 | 
			
		||||
            // Create a new reference
 | 
			
		||||
            await fileRefs.CreateReferenceAsync(
 | 
			
		||||
                new CreateReferenceRequest
 | 
			
		||||
                {
 | 
			
		||||
                    FileId = picture.Id,
 | 
			
		||||
                    Usage = "custom-apps.picture",
 | 
			
		||||
                    ResourceId = app.ResourceIdentifier
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        if (request.BackgroundId is not null)
 | 
			
		||||
        {
 | 
			
		||||
            var background = await files.GetFileAsync(
 | 
			
		||||
                new GetFileRequest { Id = request.BackgroundId }
 | 
			
		||||
            );
 | 
			
		||||
            if (background is null)
 | 
			
		||||
                throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
 | 
			
		||||
            app.Background = CloudFileReferenceObject.FromProtoValue(background);
 | 
			
		||||
 | 
			
		||||
            // Create a new reference
 | 
			
		||||
            await fileRefs.CreateReferenceAsync(
 | 
			
		||||
                new CreateReferenceRequest
 | 
			
		||||
                {
 | 
			
		||||
                    FileId = background.Id,
 | 
			
		||||
                    Usage = "custom-apps.background",
 | 
			
		||||
                    ResourceId = app.ResourceIdentifier
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        db.CustomApps.Add(app);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        return app;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<CustomApp?> GetAppAsync(Guid id, Guid? projectId = null)
 | 
			
		||||
    {
 | 
			
		||||
        var query = db.CustomApps.AsQueryable();
 | 
			
		||||
        
 | 
			
		||||
        if (projectId.HasValue)
 | 
			
		||||
        {
 | 
			
		||||
            query = query.Where(a => a.ProjectId == projectId.Value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await query.FirstOrDefaultAsync(a => a.Id == id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<List<CustomAppSecret>> GetAppSecretsAsync(Guid appId)
 | 
			
		||||
    {
 | 
			
		||||
        return await db.CustomAppSecrets
 | 
			
		||||
            .Where(s => s.AppId == appId)
 | 
			
		||||
            .OrderByDescending(s => s.CreatedAt)
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<CustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId)
 | 
			
		||||
    {
 | 
			
		||||
        return await db.CustomAppSecrets
 | 
			
		||||
            .FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<CustomAppSecret> CreateAppSecretAsync(CustomAppSecret secret)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(secret.Secret))
 | 
			
		||||
        {
 | 
			
		||||
            // Generate a new random secret if not provided
 | 
			
		||||
            secret.Secret = GenerateRandomSecret();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        secret.Id = Guid.NewGuid();
 | 
			
		||||
        secret.CreatedAt = NodaTime.SystemClock.Instance.GetCurrentInstant();
 | 
			
		||||
        secret.UpdatedAt = secret.CreatedAt;
 | 
			
		||||
 | 
			
		||||
        db.CustomAppSecrets.Add(secret);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        return secret;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> DeleteAppSecretAsync(Guid secretId, Guid appId)
 | 
			
		||||
    {
 | 
			
		||||
        var secret = await db.CustomAppSecrets
 | 
			
		||||
            .FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId);
 | 
			
		||||
 | 
			
		||||
        if (secret == null)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        db.CustomAppSecrets.Remove(secret);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<CustomAppSecret> RotateAppSecretAsync(CustomAppSecret secretUpdate)
 | 
			
		||||
    {
 | 
			
		||||
        var existingSecret = await db.CustomAppSecrets
 | 
			
		||||
            .FirstOrDefaultAsync(s => s.Id == secretUpdate.Id && s.AppId == secretUpdate.AppId);
 | 
			
		||||
 | 
			
		||||
        if (existingSecret == null)
 | 
			
		||||
            throw new InvalidOperationException("Secret not found");
 | 
			
		||||
 | 
			
		||||
        // Update the existing secret with new values
 | 
			
		||||
        existingSecret.Secret = GenerateRandomSecret();
 | 
			
		||||
        existingSecret.Description = secretUpdate.Description ?? existingSecret.Description;
 | 
			
		||||
        existingSecret.ExpiredAt = secretUpdate.ExpiredAt ?? existingSecret.ExpiredAt;
 | 
			
		||||
        existingSecret.IsOidc = secretUpdate.IsOidc;
 | 
			
		||||
        existingSecret.UpdatedAt = NodaTime.SystemClock.Instance.GetCurrentInstant();
 | 
			
		||||
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
        return existingSecret;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static string GenerateRandomSecret(int length = 64)
 | 
			
		||||
    {
 | 
			
		||||
        const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-._~+";
 | 
			
		||||
        var res = new StringBuilder();
 | 
			
		||||
        using (var rng = RandomNumberGenerator.Create())
 | 
			
		||||
        {
 | 
			
		||||
            var uintBuffer = new byte[sizeof(uint)];
 | 
			
		||||
            while (length-- > 0)
 | 
			
		||||
            {
 | 
			
		||||
                rng.GetBytes(uintBuffer);
 | 
			
		||||
                var num = BitConverter.ToUInt32(uintBuffer, 0);
 | 
			
		||||
                res.Append(valid[(int)(num % (uint)valid.Length)]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return res.ToString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<List<CustomApp>> GetAppsByProjectAsync(Guid projectId)
 | 
			
		||||
    {
 | 
			
		||||
        return await db.CustomApps
 | 
			
		||||
            .Where(a => a.ProjectId == projectId)
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<CustomApp?> UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        if (request.Slug is not null)
 | 
			
		||||
            app.Slug = request.Slug;
 | 
			
		||||
        if (request.Name is not null)
 | 
			
		||||
            app.Name = request.Name;
 | 
			
		||||
        if (request.Description is not null)
 | 
			
		||||
            app.Description = request.Description;
 | 
			
		||||
        if (request.Status is not null)
 | 
			
		||||
            app.Status = request.Status.Value;
 | 
			
		||||
        if (request.Links is not null)
 | 
			
		||||
            app.Links = request.Links;
 | 
			
		||||
        if (request.OauthConfig is not null)
 | 
			
		||||
            app.OauthConfig = request.OauthConfig;
 | 
			
		||||
 | 
			
		||||
        if (request.PictureId is not null)
 | 
			
		||||
        {
 | 
			
		||||
            var picture = await files.GetFileAsync(
 | 
			
		||||
                new GetFileRequest
 | 
			
		||||
                {
 | 
			
		||||
                    Id = request.PictureId
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            if (picture is null)
 | 
			
		||||
                throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
 | 
			
		||||
            app.Picture = CloudFileReferenceObject.FromProtoValue(picture);
 | 
			
		||||
 | 
			
		||||
            // Create a new reference
 | 
			
		||||
            await fileRefs.CreateReferenceAsync(
 | 
			
		||||
                new CreateReferenceRequest
 | 
			
		||||
                {
 | 
			
		||||
                    FileId = picture.Id,
 | 
			
		||||
                    Usage = "custom-apps.picture",
 | 
			
		||||
                    ResourceId = app.ResourceIdentifier
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        if (request.BackgroundId is not null)
 | 
			
		||||
        {
 | 
			
		||||
            var background = await files.GetFileAsync(
 | 
			
		||||
                new GetFileRequest { Id = request.BackgroundId }
 | 
			
		||||
            );
 | 
			
		||||
            if (background is null)
 | 
			
		||||
                throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
 | 
			
		||||
            app.Background = CloudFileReferenceObject.FromProtoValue(background);
 | 
			
		||||
 | 
			
		||||
            // Create a new reference
 | 
			
		||||
            await fileRefs.CreateReferenceAsync(
 | 
			
		||||
                new CreateReferenceRequest
 | 
			
		||||
                {
 | 
			
		||||
                    FileId = background.Id,
 | 
			
		||||
                    Usage = "custom-apps.background",
 | 
			
		||||
                    ResourceId = app.ResourceIdentifier
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        db.Update(app);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        return app;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> DeleteAppAsync(Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        var app = await db.CustomApps.FindAsync(id);
 | 
			
		||||
        if (app == null)
 | 
			
		||||
        {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        db.CustomApps.Remove(app);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
 | 
			
		||||
            {
 | 
			
		||||
                ResourceId = app.ResourceIdentifier
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,68 +0,0 @@
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using Grpc.Core;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Identity;
 | 
			
		||||
 | 
			
		||||
public class CustomAppServiceGrpc(AppDatabase db) : Shared.Proto.CustomAppService.CustomAppServiceBase
 | 
			
		||||
{
 | 
			
		||||
    public override async Task<GetCustomAppResponse> GetCustomApp(GetCustomAppRequest request, ServerCallContext context)
 | 
			
		||||
    {
 | 
			
		||||
        var q = db.CustomApps.AsQueryable();
 | 
			
		||||
        switch (request.QueryCase)
 | 
			
		||||
        {
 | 
			
		||||
            case GetCustomAppRequest.QueryOneofCase.Id when !string.IsNullOrWhiteSpace(request.Id):
 | 
			
		||||
            {
 | 
			
		||||
                if (!Guid.TryParse(request.Id, out var id))
 | 
			
		||||
                    throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid id"));
 | 
			
		||||
                var appById = await q.FirstOrDefaultAsync(a => a.Id == id);
 | 
			
		||||
                if (appById is null)
 | 
			
		||||
                    throw new RpcException(new Status(StatusCode.NotFound, "app not found"));
 | 
			
		||||
                return new GetCustomAppResponse { App = appById.ToProto() };
 | 
			
		||||
            }
 | 
			
		||||
            case GetCustomAppRequest.QueryOneofCase.Slug when !string.IsNullOrWhiteSpace(request.Slug):
 | 
			
		||||
            {
 | 
			
		||||
                var appBySlug = await q.FirstOrDefaultAsync(a => a.Slug == request.Slug);
 | 
			
		||||
                if (appBySlug is null)
 | 
			
		||||
                    throw new RpcException(new Status(StatusCode.NotFound, "app not found"));
 | 
			
		||||
                return new GetCustomAppResponse { App = appBySlug.ToProto() };
 | 
			
		||||
            }
 | 
			
		||||
            default:
 | 
			
		||||
                throw new RpcException(new Status(StatusCode.InvalidArgument, "id or slug required"));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<CheckCustomAppSecretResponse> CheckCustomAppSecret(CheckCustomAppSecretRequest request, ServerCallContext context)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrEmpty(request.Secret))
 | 
			
		||||
            throw new RpcException(new Status(StatusCode.InvalidArgument, "secret required"));
 | 
			
		||||
 | 
			
		||||
        IQueryable<CustomAppSecret> q = db.CustomAppSecrets;
 | 
			
		||||
        switch (request.SecretIdentifierCase)
 | 
			
		||||
        {
 | 
			
		||||
            case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.SecretId:
 | 
			
		||||
            {
 | 
			
		||||
                if (!Guid.TryParse(request.SecretId, out var sid))
 | 
			
		||||
                    throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid secret_id"));
 | 
			
		||||
                q = q.Where(s => s.Id == sid);
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.AppId:
 | 
			
		||||
            {
 | 
			
		||||
                if (!Guid.TryParse(request.AppId, out var aid))
 | 
			
		||||
                    throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid app_id"));
 | 
			
		||||
                q = q.Where(s => s.AppId == aid);
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            default:
 | 
			
		||||
                throw new RpcException(new Status(StatusCode.InvalidArgument, "secret_id or app_id required"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (request.HasIsOidc)
 | 
			
		||||
            q = q.Where(s => s.IsOidc == request.IsOidc);
 | 
			
		||||
 | 
			
		||||
        var now = NodaTime.SystemClock.Instance.GetCurrentInstant();
 | 
			
		||||
        var exists = await q.AnyAsync(s => s.Secret == request.Secret && (s.ExpiredAt == null || s.ExpiredAt > now));
 | 
			
		||||
        return new CheckCustomAppSecretResponse { Valid = exists };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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,129 +0,0 @@
 | 
			
		||||
using DysonNetwork.Shared.Auth;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using Grpc.Core;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Identity;
 | 
			
		||||
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Route("/api/developers")]
 | 
			
		||||
public class DeveloperController(
 | 
			
		||||
    AppDatabase db,
 | 
			
		||||
    PublisherService.PublisherServiceClient ps,
 | 
			
		||||
    ActionLogService.ActionLogServiceClient als,
 | 
			
		||||
    DeveloperService ds
 | 
			
		||||
)
 | 
			
		||||
    : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    [HttpGet("{name}")]
 | 
			
		||||
    public async Task<ActionResult<Developer>> GetDeveloper(string name)
 | 
			
		||||
    {
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(name);
 | 
			
		||||
        if (developer is null) return NotFound();
 | 
			
		||||
        return Ok(await ds.LoadDeveloperPublisher(developer));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("{name}/stats")]
 | 
			
		||||
    public async Task<ActionResult<DeveloperStats>> GetDeveloperStats(string name)
 | 
			
		||||
    {
 | 
			
		||||
        var developer = await ds.GetDeveloperByName(name);
 | 
			
		||||
        if (developer is null) return NotFound();
 | 
			
		||||
 | 
			
		||||
        // Get custom apps count
 | 
			
		||||
        var customAppsCount = await db.CustomApps
 | 
			
		||||
            .Include(a => a.Project)
 | 
			
		||||
            .Where(a => a.Project.DeveloperId == developer.Id)
 | 
			
		||||
            .CountAsync();
 | 
			
		||||
 | 
			
		||||
        var stats = new DeveloperStats
 | 
			
		||||
        {
 | 
			
		||||
            TotalCustomApps = customAppsCount
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return Ok(stats);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<ActionResult<List<Developer>>> ListJoinedDevelopers()
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
			
		||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
			
		||||
        
 | 
			
		||||
        var pubResponse = await ps.ListPublishersAsync(new ListPublishersRequest { AccountId = currentUser.Id });
 | 
			
		||||
        var pubIds = pubResponse.Publishers.Select(p => p.Id).Select(Guid.Parse).ToList();
 | 
			
		||||
 | 
			
		||||
        var developerQuery = db.Developers
 | 
			
		||||
            .Where(d => pubIds.Contains(d.PublisherId))
 | 
			
		||||
            .AsQueryable();
 | 
			
		||||
        
 | 
			
		||||
        var totalCount = await developerQuery.CountAsync(); 
 | 
			
		||||
        Response.Headers.Append("X-Total", totalCount.ToString());
 | 
			
		||||
        
 | 
			
		||||
        var developers = await developerQuery.ToListAsync();
 | 
			
		||||
 | 
			
		||||
        return Ok(await ds.LoadDeveloperPublisher(developers));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost("{name}/enroll")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [RequiredPermission("global", "developers.create")]
 | 
			
		||||
    public async Task<ActionResult<Developer>> EnrollDeveloperProgram(string name)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
			
		||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
			
		||||
 | 
			
		||||
        PublisherInfo? pub;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
 | 
			
		||||
            pub = PublisherInfo.FromProto(pubResponse.Publisher);
 | 
			
		||||
        } catch (RpcException ex)
 | 
			
		||||
        {
 | 
			
		||||
            return NotFound(ex.Status.Detail);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if the user is an owner of the publisher
 | 
			
		||||
        var permResponse = await ps.IsPublisherMemberAsync(new IsPublisherMemberRequest
 | 
			
		||||
        {
 | 
			
		||||
            PublisherId = pub.Id.ToString(),
 | 
			
		||||
            AccountId = currentUser.Id,
 | 
			
		||||
            Role = PublisherMemberRole.Owner
 | 
			
		||||
        });
 | 
			
		||||
        if (!permResponse.Valid) return StatusCode(403, "You must be the owner of the publisher to join the developer program");
 | 
			
		||||
 | 
			
		||||
        var hasDeveloper = await db.Developers.AnyAsync(d => d.PublisherId == pub.Id);
 | 
			
		||||
        if (hasDeveloper) return BadRequest("Publisher is already in the developer program");
 | 
			
		||||
        
 | 
			
		||||
        var developer = new Developer
 | 
			
		||||
        {
 | 
			
		||||
            Id = Guid.NewGuid(),
 | 
			
		||||
            PublisherId = pub.Id
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        db.Developers.Add(developer);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        _ = als.CreateActionLogAsync(new CreateActionLogRequest
 | 
			
		||||
        {
 | 
			
		||||
            Action = "developers.enroll",
 | 
			
		||||
            Meta = 
 | 
			
		||||
            { 
 | 
			
		||||
                { "publisher_id", Google.Protobuf.WellKnownTypes.Value.ForString(pub.Id.ToString()) },
 | 
			
		||||
                { "publisher_name", Google.Protobuf.WellKnownTypes.Value.ForString(pub.Name) }
 | 
			
		||||
            },
 | 
			
		||||
            AccountId = currentUser.Id,
 | 
			
		||||
            UserAgent = Request.Headers.UserAgent,
 | 
			
		||||
            IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return Ok(await ds.LoadDeveloperPublisher(developer));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class DeveloperStats
 | 
			
		||||
    {
 | 
			
		||||
        public int TotalCustomApps { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,75 +0,0 @@
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using Grpc.Core;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Identity;
 | 
			
		||||
 | 
			
		||||
public class DeveloperService(
 | 
			
		||||
    AppDatabase db,
 | 
			
		||||
    PublisherService.PublisherServiceClient ps,
 | 
			
		||||
    ILogger<DeveloperService> logger)
 | 
			
		||||
{
 | 
			
		||||
    public async Task<Developer> LoadDeveloperPublisher(Developer developer)
 | 
			
		||||
    {
 | 
			
		||||
        var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() });
 | 
			
		||||
        developer.Publisher = PublisherInfo.FromProto(pubResponse.Publisher);
 | 
			
		||||
        return developer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public async Task<IEnumerable<Developer>> LoadDeveloperPublisher(IEnumerable<Developer> developers)
 | 
			
		||||
    {
 | 
			
		||||
        var enumerable = developers.ToList();
 | 
			
		||||
        var pubIds = enumerable.Select(d => d.PublisherId).ToList();
 | 
			
		||||
        var pubRequest = new GetPublisherBatchRequest();
 | 
			
		||||
        pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString()));
 | 
			
		||||
        var pubResponse = await ps.GetPublisherBatchAsync(pubRequest);
 | 
			
		||||
        var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), PublisherInfo.FromProto);
 | 
			
		||||
 | 
			
		||||
        return enumerable.Select(d =>
 | 
			
		||||
        {
 | 
			
		||||
            d.Publisher = pubs[d.PublisherId];
 | 
			
		||||
            return d;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<Developer?> GetDeveloperByName(string name)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
 | 
			
		||||
            var pubId = Guid.Parse(pubResponse.Publisher.Id);
 | 
			
		||||
 | 
			
		||||
            var developer = await db.Developers.FirstOrDefaultAsync(d => d.PublisherId == pubId);
 | 
			
		||||
            return developer;
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogError(ex, "Developer {name} not found", name);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<Developer?> GetDeveloperById(Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        return await db.Developers.FirstOrDefaultAsync(d => d.Id == id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, PublisherMemberRole role)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var permResponse = await ps.IsPublisherMemberAsync(new IsPublisherMemberRequest
 | 
			
		||||
            {
 | 
			
		||||
                PublisherId = pubId.ToString(),
 | 
			
		||||
                AccountId = accountId.ToString(),
 | 
			
		||||
                Role = role
 | 
			
		||||
            });
 | 
			
		||||
            return permResponse.Valid;
 | 
			
		||||
        }
 | 
			
		||||
        catch (RpcException)
 | 
			
		||||
        {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,203 +0,0 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using DysonNetwork.Develop;
 | 
			
		||||
using DysonNetwork.Develop.Identity;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Migrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(AppDatabase))]
 | 
			
		||||
    [Migration("20250807133702_InitialMigration")]
 | 
			
		||||
    partial class InitialMigration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CloudFileReferenceObject>("Background")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("background");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("DeveloperId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CustomAppLinks>("Links")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("links");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CustomAppOauthConfig>("OauthConfig")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("oauth_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CloudFileReferenceObject>("Picture")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("picture");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Slug")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("slug");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("Status")
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                        .HasColumnName("status");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<VerificationMark>("Verification")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("verification");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_custom_apps");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("DeveloperId")
 | 
			
		||||
                        .HasDatabaseName("ix_custom_apps_developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("custom_apps", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AppId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("app_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsOidc")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_oidc");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Secret")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("secret");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_custom_app_secrets");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("AppId")
 | 
			
		||||
                        .HasDatabaseName("ix_custom_app_secrets_app_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("custom_app_secrets", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("PublisherId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("publisher_id");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_developers");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("developers", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("DeveloperId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_custom_apps_developers_developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Developer");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App")
 | 
			
		||||
                        .WithMany("Secrets")
 | 
			
		||||
                        .HasForeignKey("AppId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("App");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Navigation("Secrets");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,108 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using DysonNetwork.Develop.Identity;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Migrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class InitialMigration : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "developers",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    publisher_id = table.Column<Guid>(type: "uuid", nullable: false)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_developers", x => x.id);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "custom_apps",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
 | 
			
		||||
                    status = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
                    picture = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
 | 
			
		||||
                    background = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
 | 
			
		||||
                    verification = table.Column<VerificationMark>(type: "jsonb", nullable: true),
 | 
			
		||||
                    oauth_config = table.Column<CustomAppOauthConfig>(type: "jsonb", nullable: true),
 | 
			
		||||
                    links = table.Column<CustomAppLinks>(type: "jsonb", nullable: true),
 | 
			
		||||
                    developer_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_custom_apps", x => x.id);
 | 
			
		||||
                    table.ForeignKey(
 | 
			
		||||
                        name: "fk_custom_apps_developers_developer_id",
 | 
			
		||||
                        column: x => x.developer_id,
 | 
			
		||||
                        principalTable: "developers",
 | 
			
		||||
                        principalColumn: "id",
 | 
			
		||||
                        onDelete: ReferentialAction.Cascade);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "custom_app_secrets",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    secret = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
 | 
			
		||||
                    expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
 | 
			
		||||
                    is_oidc = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
                    app_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_custom_app_secrets", x => x.id);
 | 
			
		||||
                    table.ForeignKey(
 | 
			
		||||
                        name: "fk_custom_app_secrets_custom_apps_app_id",
 | 
			
		||||
                        column: x => x.app_id,
 | 
			
		||||
                        principalTable: "custom_apps",
 | 
			
		||||
                        principalColumn: "id",
 | 
			
		||||
                        onDelete: ReferentialAction.Cascade);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_custom_app_secrets_app_id",
 | 
			
		||||
                table: "custom_app_secrets",
 | 
			
		||||
                column: "app_id");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_custom_apps_developer_id",
 | 
			
		||||
                table: "custom_apps",
 | 
			
		||||
                column: "developer_id");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "custom_app_secrets");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "custom_apps");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "developers");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,270 +0,0 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using DysonNetwork.Develop;
 | 
			
		||||
using DysonNetwork.Develop.Identity;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Migrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(AppDatabase))]
 | 
			
		||||
    [Migration("20250818124844_AddDevProject")]
 | 
			
		||||
    partial class AddDevProject
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CloudFileReferenceObject>("Background")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("background");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CustomAppLinks>("Links")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("links");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CustomAppOauthConfig>("OauthConfig")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("oauth_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CloudFileReferenceObject>("Picture")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("picture");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("ProjectId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("project_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Slug")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("slug");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("Status")
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                        .HasColumnName("status");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<VerificationMark>("Verification")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("verification");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_custom_apps");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("ProjectId")
 | 
			
		||||
                        .HasDatabaseName("ix_custom_apps_project_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("custom_apps", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AppId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("app_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsOidc")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_oidc");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Secret")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("secret");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_custom_app_secrets");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("AppId")
 | 
			
		||||
                        .HasDatabaseName("ix_custom_app_secrets_app_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("custom_app_secrets", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("PublisherId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("publisher_id");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_developers");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("developers", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("DeveloperId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Slug")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("slug");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_dev_projects");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("DeveloperId")
 | 
			
		||||
                        .HasDatabaseName("ix_dev_projects_developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("dev_projects", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("ProjectId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_custom_apps_dev_projects_project_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Project");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App")
 | 
			
		||||
                        .WithMany("Secrets")
 | 
			
		||||
                        .HasForeignKey("AppId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("App");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer")
 | 
			
		||||
                        .WithMany("Projects")
 | 
			
		||||
                        .HasForeignKey("DeveloperId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_dev_projects_developers_developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Developer");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Navigation("Secrets");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Navigation("Projects");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,96 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Migrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class AddDevProject : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropForeignKey(
 | 
			
		||||
                name: "fk_custom_apps_developers_developer_id",
 | 
			
		||||
                table: "custom_apps");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameColumn(
 | 
			
		||||
                name: "developer_id",
 | 
			
		||||
                table: "custom_apps",
 | 
			
		||||
                newName: "project_id");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameIndex(
 | 
			
		||||
                name: "ix_custom_apps_developer_id",
 | 
			
		||||
                table: "custom_apps",
 | 
			
		||||
                newName: "ix_custom_apps_project_id");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "dev_projects",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
 | 
			
		||||
                    developer_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_dev_projects", x => x.id);
 | 
			
		||||
                    table.ForeignKey(
 | 
			
		||||
                        name: "fk_dev_projects_developers_developer_id",
 | 
			
		||||
                        column: x => x.developer_id,
 | 
			
		||||
                        principalTable: "developers",
 | 
			
		||||
                        principalColumn: "id",
 | 
			
		||||
                        onDelete: ReferentialAction.Cascade);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_dev_projects_developer_id",
 | 
			
		||||
                table: "dev_projects",
 | 
			
		||||
                column: "developer_id");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddForeignKey(
 | 
			
		||||
                name: "fk_custom_apps_dev_projects_project_id",
 | 
			
		||||
                table: "custom_apps",
 | 
			
		||||
                column: "project_id",
 | 
			
		||||
                principalTable: "dev_projects",
 | 
			
		||||
                principalColumn: "id",
 | 
			
		||||
                onDelete: ReferentialAction.Cascade);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropForeignKey(
 | 
			
		||||
                name: "fk_custom_apps_dev_projects_project_id",
 | 
			
		||||
                table: "custom_apps");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "dev_projects");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameColumn(
 | 
			
		||||
                name: "project_id",
 | 
			
		||||
                table: "custom_apps",
 | 
			
		||||
                newName: "developer_id");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameIndex(
 | 
			
		||||
                name: "ix_custom_apps_project_id",
 | 
			
		||||
                table: "custom_apps",
 | 
			
		||||
                newName: "ix_custom_apps_developer_id");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddForeignKey(
 | 
			
		||||
                name: "fk_custom_apps_developers_developer_id",
 | 
			
		||||
                table: "custom_apps",
 | 
			
		||||
                column: "developer_id",
 | 
			
		||||
                principalTable: "developers",
 | 
			
		||||
                principalColumn: "id",
 | 
			
		||||
                onDelete: ReferentialAction.Cascade);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,324 +0,0 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using DysonNetwork.Develop;
 | 
			
		||||
using DysonNetwork.Develop.Identity;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Migrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(AppDatabase))]
 | 
			
		||||
    [Migration("20250819163227_AddBotAccount")]
 | 
			
		||||
    partial class AddBotAccount
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsActive")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_active");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("ProjectId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("project_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Slug")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("slug");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_bot_accounts");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("ProjectId")
 | 
			
		||||
                        .HasDatabaseName("ix_bot_accounts_project_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("bot_accounts", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CloudFileReferenceObject>("Background")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("background");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CustomAppLinks>("Links")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("links");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CustomAppOauthConfig>("OauthConfig")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("oauth_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CloudFileReferenceObject>("Picture")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("picture");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("ProjectId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("project_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Slug")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("slug");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("Status")
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                        .HasColumnName("status");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<VerificationMark>("Verification")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("verification");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_custom_apps");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("ProjectId")
 | 
			
		||||
                        .HasDatabaseName("ix_custom_apps_project_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("custom_apps", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AppId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("app_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsOidc")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_oidc");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Secret")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("secret");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_custom_app_secrets");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("AppId")
 | 
			
		||||
                        .HasDatabaseName("ix_custom_app_secrets_app_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("custom_app_secrets", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("PublisherId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("publisher_id");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_developers");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("developers", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("DeveloperId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Slug")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("slug");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_dev_projects");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("DeveloperId")
 | 
			
		||||
                        .HasDatabaseName("ix_dev_projects_developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("dev_projects", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("ProjectId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_bot_accounts_dev_projects_project_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Project");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("ProjectId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_custom_apps_dev_projects_project_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Project");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App")
 | 
			
		||||
                        .WithMany("Secrets")
 | 
			
		||||
                        .HasForeignKey("AppId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("App");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer")
 | 
			
		||||
                        .WithMany("Projects")
 | 
			
		||||
                        .HasForeignKey("DeveloperId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_dev_projects_developers_developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Developer");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Navigation("Secrets");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Navigation("Projects");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,51 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Migrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class AddBotAccount : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "bot_accounts",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    is_active = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
                    project_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_bot_accounts", x => x.id);
 | 
			
		||||
                    table.ForeignKey(
 | 
			
		||||
                        name: "fk_bot_accounts_dev_projects_project_id",
 | 
			
		||||
                        column: x => x.project_id,
 | 
			
		||||
                        principalTable: "dev_projects",
 | 
			
		||||
                        principalColumn: "id",
 | 
			
		||||
                        onDelete: ReferentialAction.Cascade);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_bot_accounts_project_id",
 | 
			
		||||
                table: "bot_accounts",
 | 
			
		||||
                column: "project_id");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "bot_accounts");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,321 +0,0 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using DysonNetwork.Develop;
 | 
			
		||||
using DysonNetwork.Develop.Identity;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Migrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(AppDatabase))]
 | 
			
		||||
    partial class AppDatabaseModelSnapshot : ModelSnapshot
 | 
			
		||||
    {
 | 
			
		||||
        protected override void BuildModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsActive")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_active");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("ProjectId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("project_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Slug")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("slug");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_bot_accounts");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("ProjectId")
 | 
			
		||||
                        .HasDatabaseName("ix_bot_accounts_project_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("bot_accounts", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CloudFileReferenceObject>("Background")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("background");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CustomAppLinks>("Links")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("links");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CustomAppOauthConfig>("OauthConfig")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("oauth_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<CloudFileReferenceObject>("Picture")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("picture");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("ProjectId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("project_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Slug")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("slug");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("Status")
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                        .HasColumnName("status");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<VerificationMark>("Verification")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("verification");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_custom_apps");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("ProjectId")
 | 
			
		||||
                        .HasDatabaseName("ix_custom_apps_project_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("custom_apps", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AppId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("app_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsOidc")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_oidc");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Secret")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("secret");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_custom_app_secrets");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("AppId")
 | 
			
		||||
                        .HasDatabaseName("ix_custom_app_secrets_app_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("custom_app_secrets", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("PublisherId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("publisher_id");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_developers");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("developers", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("DeveloperId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Slug")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("slug");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_dev_projects");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("DeveloperId")
 | 
			
		||||
                        .HasDatabaseName("ix_dev_projects_developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("dev_projects", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("ProjectId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_bot_accounts_dev_projects_project_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Project");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("ProjectId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_custom_apps_dev_projects_project_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Project");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App")
 | 
			
		||||
                        .WithMany("Secrets")
 | 
			
		||||
                        .HasForeignKey("AppId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("App");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer")
 | 
			
		||||
                        .WithMany("Projects")
 | 
			
		||||
                        .HasForeignKey("DeveloperId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_dev_projects_developers_developer_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Developer");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Navigation("Secrets");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Navigation("Projects");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
using DysonNetwork.Develop;
 | 
			
		||||
using DysonNetwork.Shared.Auth;
 | 
			
		||||
using DysonNetwork.Shared.Http;
 | 
			
		||||
using DysonNetwork.Develop.Startup;
 | 
			
		||||
using DysonNetwork.Shared.Registry;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
 | 
			
		||||
var builder = WebApplication.CreateBuilder(args);
 | 
			
		||||
 | 
			
		||||
builder.AddServiceDefaults();
 | 
			
		||||
 | 
			
		||||
builder.ConfigureAppKestrel(builder.Configuration);
 | 
			
		||||
 | 
			
		||||
builder.Services.AddAppServices(builder.Configuration);
 | 
			
		||||
builder.Services.AddAppAuthentication();
 | 
			
		||||
builder.Services.AddAppSwagger();
 | 
			
		||||
builder.Services.AddDysonAuth();
 | 
			
		||||
builder.Services.AddPublisherService();
 | 
			
		||||
builder.Services.AddAccountService();
 | 
			
		||||
builder.Services.AddDriveService();
 | 
			
		||||
 | 
			
		||||
var app = builder.Build();
 | 
			
		||||
 | 
			
		||||
app.MapDefaultEndpoints();
 | 
			
		||||
 | 
			
		||||
using (var scope = app.Services.CreateScope())
 | 
			
		||||
{
 | 
			
		||||
    var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
 | 
			
		||||
    await db.Database.MigrateAsync();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
app.ConfigureAppMiddleware(builder.Configuration);
 | 
			
		||||
 | 
			
		||||
app.Run();
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using DysonNetwork.Develop.Identity;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Project;
 | 
			
		||||
 | 
			
		||||
public class DevProject : ModelBase
 | 
			
		||||
{
 | 
			
		||||
    public Guid Id { get; set; } = Guid.NewGuid();
 | 
			
		||||
    [MaxLength(1024)] public string Slug { get; set; } = string.Empty;
 | 
			
		||||
    [MaxLength(1024)] public string Name { get; set; } = string.Empty;
 | 
			
		||||
    [MaxLength(4096)] public string Description { get; set; } = string.Empty;
 | 
			
		||||
    
 | 
			
		||||
    public Developer Developer { get; set; } = null!;
 | 
			
		||||
    public Guid DeveloperId { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,107 +0,0 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using DysonNetwork.Develop.Identity;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Project;
 | 
			
		||||
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Route("/api/developers/{pubName}/projects")]
 | 
			
		||||
public class DevProjectController(DevProjectService projectService, DeveloperService developerService) : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    public record DevProjectRequest(
 | 
			
		||||
        [MaxLength(1024)] string? Slug,
 | 
			
		||||
        [MaxLength(1024)] string? Name,
 | 
			
		||||
        [MaxLength(4096)] string? Description
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    public async Task<IActionResult> ListProjects([FromRoute] string pubName)
 | 
			
		||||
    {
 | 
			
		||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null) return NotFound();
 | 
			
		||||
        
 | 
			
		||||
        var projects = await projectService.GetProjectsByDeveloperAsync(developer.Id);
 | 
			
		||||
        return Ok(projects);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("{id:guid}")]
 | 
			
		||||
    public async Task<IActionResult> GetProject([FromRoute] string pubName, Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null) return NotFound();
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.GetProjectAsync(id, developer.Id);
 | 
			
		||||
        if (project is null) return NotFound();
 | 
			
		||||
 | 
			
		||||
        return Ok(project);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> CreateProject([FromRoute] string pubName, [FromBody] DevProjectRequest request)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) 
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
			
		||||
        if (developer is null)
 | 
			
		||||
            return NotFound("Developer not found");
 | 
			
		||||
            
 | 
			
		||||
        if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
 | 
			
		||||
            return StatusCode(403, "You must be an editor of the developer to create a project");
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(request.Slug) || string.IsNullOrWhiteSpace(request.Name))
 | 
			
		||||
            return BadRequest("Slug and Name are required");
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.CreateProjectAsync(developer, request);
 | 
			
		||||
        return CreatedAtAction(
 | 
			
		||||
            nameof(GetProject), 
 | 
			
		||||
            new { pubName, id = project.Id },
 | 
			
		||||
            project
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPut("{id:guid}")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> UpdateProject(
 | 
			
		||||
        [FromRoute] string pubName, 
 | 
			
		||||
        [FromRoute] Guid id,
 | 
			
		||||
        [FromBody] DevProjectRequest request
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) 
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
			
		||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
			
		||||
        if (developer is null || developer.Id != accountId)
 | 
			
		||||
            return Forbid();
 | 
			
		||||
 | 
			
		||||
        var project = await projectService.UpdateProjectAsync(id, developer.Id, request);
 | 
			
		||||
        if (project is null)
 | 
			
		||||
            return NotFound();
 | 
			
		||||
 | 
			
		||||
        return Ok(project);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpDelete("{id:guid}")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<IActionResult> DeleteProject([FromRoute] string pubName, [FromRoute] Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) 
 | 
			
		||||
            return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var developer = await developerService.GetDeveloperByName(pubName);
 | 
			
		||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
			
		||||
        if (developer is null || developer.Id != accountId)
 | 
			
		||||
            return Forbid();
 | 
			
		||||
 | 
			
		||||
        var success = await projectService.DeleteProjectAsync(id, developer.Id);
 | 
			
		||||
        if (!success)
 | 
			
		||||
            return NotFound();
 | 
			
		||||
 | 
			
		||||
        return NoContent();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,77 +0,0 @@
 | 
			
		||||
using DysonNetwork.Develop.Identity;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Project;
 | 
			
		||||
 | 
			
		||||
public class DevProjectService(
 | 
			
		||||
    AppDatabase db,
 | 
			
		||||
    FileReferenceService.FileReferenceServiceClient fileRefs,
 | 
			
		||||
    FileService.FileServiceClient files
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    public async Task<DevProject> CreateProjectAsync(
 | 
			
		||||
        Developer developer,
 | 
			
		||||
        DevProjectController.DevProjectRequest request
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        var project = new DevProject
 | 
			
		||||
        {
 | 
			
		||||
            Slug = request.Slug!,
 | 
			
		||||
            Name = request.Name!,
 | 
			
		||||
            Description = request.Description ?? string.Empty,
 | 
			
		||||
            DeveloperId = developer.Id
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        db.DevProjects.Add(project);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
        
 | 
			
		||||
        return project;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<DevProject?> GetProjectAsync(Guid id, Guid? developerId = null)
 | 
			
		||||
    {
 | 
			
		||||
        var query = db.DevProjects.AsQueryable();
 | 
			
		||||
        
 | 
			
		||||
        if (developerId.HasValue)
 | 
			
		||||
        {
 | 
			
		||||
            query = query.Where(p => p.DeveloperId == developerId.Value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await query.FirstOrDefaultAsync(p => p.Id == id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<List<DevProject>> GetProjectsByDeveloperAsync(Guid developerId)
 | 
			
		||||
    {
 | 
			
		||||
        return await db.DevProjects
 | 
			
		||||
            .Where(p => p.DeveloperId == developerId)
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<DevProject?> UpdateProjectAsync(
 | 
			
		||||
        Guid id,
 | 
			
		||||
        Guid developerId,
 | 
			
		||||
        DevProjectController.DevProjectRequest request
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        var project = await GetProjectAsync(id, developerId);
 | 
			
		||||
        if (project == null) return null;
 | 
			
		||||
 | 
			
		||||
        if (request.Slug != null) project.Slug = request.Slug;
 | 
			
		||||
        if (request.Name != null) project.Name = request.Name;
 | 
			
		||||
        if (request.Description != null) project.Description = request.Description;
 | 
			
		||||
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
        return project;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> DeleteProjectAsync(Guid id, Guid developerId)
 | 
			
		||||
    {
 | 
			
		||||
        var project = await GetProjectAsync(id, developerId);
 | 
			
		||||
        if (project == null) return false;
 | 
			
		||||
 | 
			
		||||
        db.DevProjects.Remove(project);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://json.schemastore.org/launchsettings.json",
 | 
			
		||||
  "profiles": {
 | 
			
		||||
    "http": {
 | 
			
		||||
      "commandName": "Project",
 | 
			
		||||
      "dotnetRunMessages": true,
 | 
			
		||||
      "launchBrowser": false,
 | 
			
		||||
      "applicationUrl": "http://localhost:5156",
 | 
			
		||||
      "environmentVariables": {
 | 
			
		||||
        "ASPNETCORE_ENVIRONMENT": "Development"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "https": {
 | 
			
		||||
      "commandName": "Project",
 | 
			
		||||
      "dotnetRunMessages": true,
 | 
			
		||||
      "launchBrowser": false,
 | 
			
		||||
      "applicationUrl": "https://localhost:7192;http://localhost:5156",
 | 
			
		||||
      "environmentVariables": {
 | 
			
		||||
        "ASPNETCORE_ENVIRONMENT": "Development"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
using System.Net;
 | 
			
		||||
using DysonNetwork.Develop.Identity;
 | 
			
		||||
using DysonNetwork.Shared.Auth;
 | 
			
		||||
using DysonNetwork.Shared.Http;
 | 
			
		||||
using Microsoft.AspNetCore.HttpOverrides;
 | 
			
		||||
using Prometheus;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Startup;
 | 
			
		||||
 | 
			
		||||
public static class ApplicationConfiguration
 | 
			
		||||
{
 | 
			
		||||
    public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
 | 
			
		||||
    {
 | 
			
		||||
        app.MapMetrics();
 | 
			
		||||
        app.MapOpenApi();
 | 
			
		||||
 | 
			
		||||
        app.UseSwagger();
 | 
			
		||||
        app.UseSwaggerUI();
 | 
			
		||||
        
 | 
			
		||||
        app.UseRequestLocalization();
 | 
			
		||||
 | 
			
		||||
        app.ConfigureForwardedHeaders(configuration);
 | 
			
		||||
 | 
			
		||||
        app.UseAuthentication();
 | 
			
		||||
        app.UseAuthorization();
 | 
			
		||||
        app.UseMiddleware<PermissionMiddleware>();
 | 
			
		||||
 | 
			
		||||
        app.MapControllers();
 | 
			
		||||
        
 | 
			
		||||
        app.MapGrpcService<CustomAppServiceGrpc>();
 | 
			
		||||
 | 
			
		||||
        return app;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,79 +0,0 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using Microsoft.OpenApi.Models;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using NodaTime.Serialization.SystemTextJson;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
using DysonNetwork.Develop.Identity;
 | 
			
		||||
using DysonNetwork.Develop.Project;
 | 
			
		||||
using DysonNetwork.Shared.Cache;
 | 
			
		||||
using StackExchange.Redis;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Develop.Startup;
 | 
			
		||||
 | 
			
		||||
public static class ServiceCollectionExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
 | 
			
		||||
    {
 | 
			
		||||
        services.AddLocalization();
 | 
			
		||||
 | 
			
		||||
        services.AddDbContext<AppDatabase>();
 | 
			
		||||
        services.AddSingleton<IClock>(SystemClock.Instance);
 | 
			
		||||
        services.AddHttpContextAccessor();
 | 
			
		||||
        services.AddSingleton<ICacheService, CacheServiceRedis>();
 | 
			
		||||
 | 
			
		||||
        services.AddHttpClient();
 | 
			
		||||
 | 
			
		||||
        services.AddControllers().AddJsonOptions(options =>
 | 
			
		||||
        {
 | 
			
		||||
            options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
 | 
			
		||||
            options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
 | 
			
		||||
            options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
 | 
			
		||||
            
 | 
			
		||||
            options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        services.AddGrpc(options => { options.EnableDetailedErrors = true; });
 | 
			
		||||
 | 
			
		||||
        services.Configure<RequestLocalizationOptions>(options =>
 | 
			
		||||
        {
 | 
			
		||||
            var supportedCultures = new[]
 | 
			
		||||
            {
 | 
			
		||||
                new CultureInfo("en-US"),
 | 
			
		||||
                new CultureInfo("zh-Hans"),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            options.SupportedCultures = supportedCultures;
 | 
			
		||||
            options.SupportedUICultures = supportedCultures;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        services.AddScoped<DeveloperService>();
 | 
			
		||||
        services.AddScoped<CustomAppService>();
 | 
			
		||||
        services.AddScoped<DevProjectService>();
 | 
			
		||||
        services.AddScoped<BotAccountService>();
 | 
			
		||||
 | 
			
		||||
        return services;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
 | 
			
		||||
    {
 | 
			
		||||
        services.AddCors();
 | 
			
		||||
        services.AddAuthorization();
 | 
			
		||||
        return services;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IServiceCollection AddAppSwagger(this IServiceCollection services)
 | 
			
		||||
    {
 | 
			
		||||
        services.AddEndpointsApiExplorer();
 | 
			
		||||
        services.AddSwaggerGen(options =>
 | 
			
		||||
        {
 | 
			
		||||
            options.SwaggerDoc("v1", new OpenApiInfo
 | 
			
		||||
            {
 | 
			
		||||
                Version = "v1",
 | 
			
		||||
                Title = "Develop API",
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        services.AddOpenApi();
 | 
			
		||||
        return services;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "Debug": true,
 | 
			
		||||
  "BaseUrl": "http://localhost:5071",
 | 
			
		||||
  "SiteUrl": "https://solian.app",
 | 
			
		||||
  "Logging": {
 | 
			
		||||
    "LogLevel": {
 | 
			
		||||
      "Default": "Information",
 | 
			
		||||
      "Microsoft.AspNetCore": "Warning"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "AllowedHosts": "*",
 | 
			
		||||
  "ConnectionStrings": {
 | 
			
		||||
    "App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
 | 
			
		||||
  },
 | 
			
		||||
  "KnownProxies": [
 | 
			
		||||
    "127.0.0.1",
 | 
			
		||||
    "::1"
 | 
			
		||||
  ],
 | 
			
		||||
  "Etcd": {
 | 
			
		||||
    "Insecure": true
 | 
			
		||||
  },
 | 
			
		||||
  "Service": {
 | 
			
		||||
    "Name": "DysonNetwork.Develop",
 | 
			
		||||
    "Url": "https://localhost:7192"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								DysonNetwork.Drive/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								DysonNetwork.Drive/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,3 +0,0 @@
 | 
			
		||||
/Uploads/
 | 
			
		||||
/Client/node_modules/
 | 
			
		||||
/wwwroot/dist
 | 
			
		||||
@@ -1,184 +0,0 @@
 | 
			
		||||
using System.Linq.Expressions;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using DysonNetwork.Drive.Billing;
 | 
			
		||||
using DysonNetwork.Drive.Storage;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Design;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Query;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Quartz;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive;
 | 
			
		||||
 | 
			
		||||
public class AppDatabase(
 | 
			
		||||
    DbContextOptions<AppDatabase> options,
 | 
			
		||||
    IConfiguration configuration
 | 
			
		||||
) : DbContext(options)
 | 
			
		||||
{
 | 
			
		||||
    public DbSet<FilePool> Pools { get; set; } = null!;
 | 
			
		||||
    public DbSet<FileBundle> Bundles { get; set; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    public DbSet<CloudFile> Files { get; set; } = null!;
 | 
			
		||||
    public DbSet<CloudFileReference> FileReferences { get; set; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
 | 
			
		||||
    {
 | 
			
		||||
        optionsBuilder.UseNpgsql(
 | 
			
		||||
            configuration.GetConnectionString("App"),
 | 
			
		||||
            opt => opt
 | 
			
		||||
                .ConfigureDataSource(optSource => optSource.EnableDynamicJson())
 | 
			
		||||
                .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
 | 
			
		||||
                .UseNodaTime()
 | 
			
		||||
        ).UseSnakeCaseNamingConvention();
 | 
			
		||||
 | 
			
		||||
        base.OnConfiguring(optionsBuilder);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override void OnModelCreating(ModelBuilder modelBuilder)
 | 
			
		||||
    {
 | 
			
		||||
        base.OnModelCreating(modelBuilder);
 | 
			
		||||
 | 
			
		||||
        // Automatically apply soft-delete filter to all entities inheriting BaseModel
 | 
			
		||||
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
 | 
			
		||||
        {
 | 
			
		||||
            if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue;
 | 
			
		||||
            var method = typeof(AppDatabase)
 | 
			
		||||
                .GetMethod(nameof(SetSoftDeleteFilter),
 | 
			
		||||
                    BindingFlags.NonPublic | BindingFlags.Static)!
 | 
			
		||||
                .MakeGenericMethod(entityType.ClrType);
 | 
			
		||||
 | 
			
		||||
            method.Invoke(null, [modelBuilder]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void SetSoftDeleteFilter<TEntity>(ModelBuilder modelBuilder)
 | 
			
		||||
        where TEntity : ModelBase
 | 
			
		||||
    {
 | 
			
		||||
        modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        var now = SystemClock.Instance.GetCurrentInstant();
 | 
			
		||||
 | 
			
		||||
        foreach (var entry in ChangeTracker.Entries<ModelBase>())
 | 
			
		||||
        {
 | 
			
		||||
            switch (entry.State)
 | 
			
		||||
            {
 | 
			
		||||
                case EntityState.Added:
 | 
			
		||||
                    entry.Entity.CreatedAt = now;
 | 
			
		||||
                    entry.Entity.UpdatedAt = now;
 | 
			
		||||
                    break;
 | 
			
		||||
                case EntityState.Modified:
 | 
			
		||||
                    entry.Entity.UpdatedAt = now;
 | 
			
		||||
                    break;
 | 
			
		||||
                case EntityState.Deleted:
 | 
			
		||||
                    entry.State = EntityState.Modified;
 | 
			
		||||
                    entry.Entity.DeletedAt = now;
 | 
			
		||||
                    break;
 | 
			
		||||
                case EntityState.Detached:
 | 
			
		||||
                case EntityState.Unchanged:
 | 
			
		||||
                default:
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await base.SaveChangesAsync(cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclingJob> logger) : IJob
 | 
			
		||||
{
 | 
			
		||||
    public async Task Execute(IJobExecutionContext context)
 | 
			
		||||
    {
 | 
			
		||||
        var now = SystemClock.Instance.GetCurrentInstant();
 | 
			
		||||
 | 
			
		||||
        logger.LogInformation("Deleting soft-deleted records...");
 | 
			
		||||
 | 
			
		||||
        var threshold = now - Duration.FromDays(7);
 | 
			
		||||
 | 
			
		||||
        var entityTypes = db.Model.GetEntityTypes()
 | 
			
		||||
            .Where(t => typeof(ModelBase).IsAssignableFrom(t.ClrType) && t.ClrType != typeof(ModelBase))
 | 
			
		||||
            .Select(t => t.ClrType);
 | 
			
		||||
 | 
			
		||||
        foreach (var entityType in entityTypes)
 | 
			
		||||
        {
 | 
			
		||||
            var set = (IQueryable)db.GetType().GetMethod(nameof(DbContext.Set), Type.EmptyTypes)!
 | 
			
		||||
                .MakeGenericMethod(entityType).Invoke(db, null)!;
 | 
			
		||||
            var parameter = Expression.Parameter(entityType, "e");
 | 
			
		||||
            var property = Expression.Property(parameter, nameof(ModelBase.DeletedAt));
 | 
			
		||||
            var condition = Expression.LessThan(property, Expression.Constant(threshold, typeof(Instant?)));
 | 
			
		||||
            var notNull = Expression.NotEqual(property, Expression.Constant(null, typeof(Instant?)));
 | 
			
		||||
            var finalCondition = Expression.AndAlso(notNull, condition);
 | 
			
		||||
            var lambda = Expression.Lambda(finalCondition, parameter);
 | 
			
		||||
 | 
			
		||||
            var queryable = set.Provider.CreateQuery(
 | 
			
		||||
                Expression.Call(
 | 
			
		||||
                    typeof(Queryable),
 | 
			
		||||
                    "Where",
 | 
			
		||||
                    [entityType],
 | 
			
		||||
                    set.Expression,
 | 
			
		||||
                    Expression.Quote(lambda)
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            var toListAsync = typeof(EntityFrameworkQueryableExtensions)
 | 
			
		||||
                .GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync))!
 | 
			
		||||
                .MakeGenericMethod(entityType);
 | 
			
		||||
 | 
			
		||||
            var items = await (dynamic)toListAsync.Invoke(null, [queryable, CancellationToken.None])!;
 | 
			
		||||
            db.RemoveRange(items);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
 | 
			
		||||
{
 | 
			
		||||
    public AppDatabase CreateDbContext(string[] args)
 | 
			
		||||
    {
 | 
			
		||||
        var configuration = new ConfigurationBuilder()
 | 
			
		||||
            .SetBasePath(Directory.GetCurrentDirectory())
 | 
			
		||||
            .AddJsonFile("appsettings.json")
 | 
			
		||||
            .Build();
 | 
			
		||||
 | 
			
		||||
        var optionsBuilder = new DbContextOptionsBuilder<AppDatabase>();
 | 
			
		||||
        return new AppDatabase(optionsBuilder.Options, configuration);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public static class OptionalQueryExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static IQueryable<T> If<T>(
 | 
			
		||||
        this IQueryable<T> source,
 | 
			
		||||
        bool condition,
 | 
			
		||||
        Func<IQueryable<T>, IQueryable<T>> transform
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        return condition ? transform(source) : source;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IQueryable<T> If<T, TP>(
 | 
			
		||||
        this IIncludableQueryable<T, TP> source,
 | 
			
		||||
        bool condition,
 | 
			
		||||
        Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform
 | 
			
		||||
    )
 | 
			
		||||
        where T : class
 | 
			
		||||
    {
 | 
			
		||||
        return condition ? transform(source) : source;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IQueryable<T> If<T, TP>(
 | 
			
		||||
        this IIncludableQueryable<T, IEnumerable<TP>> source,
 | 
			
		||||
        bool condition,
 | 
			
		||||
        Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform
 | 
			
		||||
    )
 | 
			
		||||
        where T : class
 | 
			
		||||
    {
 | 
			
		||||
        return condition ? transform(source) : source;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Billing;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// The quota record stands for the extra quota that a user has.
 | 
			
		||||
/// For normal users, the quota is 1GiB.
 | 
			
		||||
/// For stellar program t1 users, the quota is 5GiB
 | 
			
		||||
/// For stellar program t2 users, the quota is 10GiB
 | 
			
		||||
/// For stellar program t3 users, the quota is 15GiB
 | 
			
		||||
///
 | 
			
		||||
/// If users want to increase the quota, they need to pay for it.
 | 
			
		||||
/// Each 1NSD they paid for one GiB.
 | 
			
		||||
///
 | 
			
		||||
/// But the quota record unit is MiB, the minimal billable unit.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class QuotaRecord : ModelBase
 | 
			
		||||
{
 | 
			
		||||
    public Guid Id { get; set; } = Guid.NewGuid();
 | 
			
		||||
    public Guid AccountId { get; set; }
 | 
			
		||||
    public string Name { get; set; } = string.Empty;
 | 
			
		||||
    public string Description { get; set; } = string.Empty;
 | 
			
		||||
    
 | 
			
		||||
    public long Quota { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    public Instant? ExpiredAt { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,66 +0,0 @@
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Billing;
 | 
			
		||||
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Route("/api/billing/quota")]
 | 
			
		||||
public class QuotaController(AppDatabase db, QuotaService quota) : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    public class QuotaDetails
 | 
			
		||||
    {
 | 
			
		||||
        public long BasedQuota { get; set; }
 | 
			
		||||
        public long ExtraQuota { get; set; }
 | 
			
		||||
        public long TotalQuota { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<ActionResult<QuotaDetails>> GetQuota()
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
			
		||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
			
		||||
        
 | 
			
		||||
        var (based, extra) = await quota.GetQuotaVerbose(accountId);
 | 
			
		||||
        return Ok(new QuotaDetails
 | 
			
		||||
        {
 | 
			
		||||
            BasedQuota = based,
 | 
			
		||||
            ExtraQuota = extra,
 | 
			
		||||
            TotalQuota = based + extra
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    [HttpGet("records")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<ActionResult<List<QuotaRecord>>> GetQuotaRecords(
 | 
			
		||||
        [FromQuery] bool expired = false,
 | 
			
		||||
        [FromQuery] int offset = 0,
 | 
			
		||||
        [FromQuery] int take = 20
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
			
		||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
			
		||||
 | 
			
		||||
        var now = SystemClock.Instance.GetCurrentInstant();
 | 
			
		||||
        var query = db.QuotaRecords
 | 
			
		||||
            .Where(r => r.AccountId == accountId)
 | 
			
		||||
            .AsQueryable();
 | 
			
		||||
        if (!expired)
 | 
			
		||||
            query = query
 | 
			
		||||
                .Where(r => !r.ExpiredAt.HasValue || r.ExpiredAt > now);
 | 
			
		||||
 | 
			
		||||
        var total = await query.CountAsync();
 | 
			
		||||
        Response.Headers.Append("X-Total", total.ToString());
 | 
			
		||||
 | 
			
		||||
        var records = await query
 | 
			
		||||
            .OrderByDescending(r => r.CreatedAt)
 | 
			
		||||
            .Skip(offset)
 | 
			
		||||
            .Take(take)
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
 | 
			
		||||
        return Ok(records);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,69 +0,0 @@
 | 
			
		||||
using DysonNetwork.Shared.Auth;
 | 
			
		||||
using DysonNetwork.Shared.Cache;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Billing;
 | 
			
		||||
 | 
			
		||||
public class QuotaService(
 | 
			
		||||
    AppDatabase db,
 | 
			
		||||
    UsageService usage,
 | 
			
		||||
    AccountService.AccountServiceClient accounts,
 | 
			
		||||
    ICacheService cache
 | 
			
		||||
)
 | 
			
		||||
{
 | 
			
		||||
    public async Task<(bool ok, long billable, long quota)> IsFileAcceptable(Guid accountId, double costMultiplier, long newFileSize)
 | 
			
		||||
    {
 | 
			
		||||
        // The billable unit is MiB
 | 
			
		||||
        var billableUnit = (long)Math.Ceiling(newFileSize / 1024.0 / 1024.0 * costMultiplier);
 | 
			
		||||
        var totalBillableUsage = await usage.GetTotalBillableUsage(accountId);
 | 
			
		||||
        var quota = await GetQuota(accountId);
 | 
			
		||||
        return (totalBillableUsage + billableUnit <= quota, billableUnit, quota);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<long> GetQuota(Guid accountId)
 | 
			
		||||
    {
 | 
			
		||||
        var cacheKey = $"file:quota:{accountId}";
 | 
			
		||||
        var cachedResult = await cache.GetAsync<long?>(cacheKey);
 | 
			
		||||
        if (cachedResult.HasValue) return cachedResult.Value;
 | 
			
		||||
        
 | 
			
		||||
        var (based, extra) = await GetQuotaVerbose(accountId);
 | 
			
		||||
        var quota = based + extra;
 | 
			
		||||
        await cache.SetAsync(cacheKey, quota);
 | 
			
		||||
        return quota;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task<(long based, long extra)> GetQuotaVerbose(Guid accountId)
 | 
			
		||||
    {
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        var response = await accounts.GetAccountAsync(new GetAccountRequest { Id = accountId.ToString() });
 | 
			
		||||
        var perkSubscription = response.PerkSubscription;
 | 
			
		||||
 | 
			
		||||
        // The base quota is 1GiB, T1 is 5GiB, T2 is 10GiB, T3 is 15GiB
 | 
			
		||||
        var basedQuota = 1L;
 | 
			
		||||
        if (perkSubscription != null)
 | 
			
		||||
        {
 | 
			
		||||
            var privilege = PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(perkSubscription.Identifier);
 | 
			
		||||
            basedQuota = privilege switch
 | 
			
		||||
            {
 | 
			
		||||
                1 => 5L,
 | 
			
		||||
                2 => 10L,
 | 
			
		||||
                3 => 15L,
 | 
			
		||||
                _ => basedQuota
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // The based quota is in GiB, we need to convert it to MiB
 | 
			
		||||
        basedQuota *= 1024L;
 | 
			
		||||
        
 | 
			
		||||
        var now = SystemClock.Instance.GetCurrentInstant();
 | 
			
		||||
        var extraQuota = await db.QuotaRecords
 | 
			
		||||
            .Where(e => e.AccountId == accountId)
 | 
			
		||||
            .Where(e => !e.ExpiredAt.HasValue || e.ExpiredAt > now)
 | 
			
		||||
            .SumAsync(e => e.Quota);
 | 
			
		||||
        
 | 
			
		||||
        return (basedQuota, extraQuota);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,49 +0,0 @@
 | 
			
		||||
using DysonNetwork.Shared.Cache;
 | 
			
		||||
using DysonNetwork.Shared.Proto;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Billing;
 | 
			
		||||
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Route("api/billing/usage")]
 | 
			
		||||
public class UsageController(UsageService usage, QuotaService quota, ICacheService cache) : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    [HttpGet]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<ActionResult<TotalUsageDetails>> GetTotalUsage()
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
			
		||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
			
		||||
        
 | 
			
		||||
        var cacheKey = $"file:usage:{accountId}";
 | 
			
		||||
        
 | 
			
		||||
        // Try to get from cache first
 | 
			
		||||
        var (found, cachedResult) = await cache.GetAsyncWithStatus<TotalUsageDetails>(cacheKey);
 | 
			
		||||
        if (found && cachedResult != null)
 | 
			
		||||
            return Ok(cachedResult);
 | 
			
		||||
 | 
			
		||||
        // If not in cache, get from services
 | 
			
		||||
        var result = await usage.GetTotalUsage(accountId);
 | 
			
		||||
        var totalQuota = await quota.GetQuota(accountId);
 | 
			
		||||
        result.TotalQuota = totalQuota;
 | 
			
		||||
 | 
			
		||||
        // Cache the result for 5 minutes
 | 
			
		||||
        await cache.SetAsync(cacheKey, result, TimeSpan.FromMinutes(5));
 | 
			
		||||
 | 
			
		||||
        return Ok(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpGet("{poolId:guid}")]
 | 
			
		||||
    public async Task<ActionResult<UsageDetails>> GetPoolUsage(Guid poolId)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
			
		||||
        var accountId = Guid.Parse(currentUser.Id);
 | 
			
		||||
        
 | 
			
		||||
        var usageDetails = await usage.GetPoolUsage(poolId, accountId);
 | 
			
		||||
        if (usageDetails == null)
 | 
			
		||||
            return NotFound();
 | 
			
		||||
        return usageDetails;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,121 +0,0 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Billing;
 | 
			
		||||
 | 
			
		||||
public class UsageDetails
 | 
			
		||||
{
 | 
			
		||||
    public required Guid PoolId { get; set; }
 | 
			
		||||
    public required string PoolName { get; set; }
 | 
			
		||||
    public required long UsageBytes { get; set; }
 | 
			
		||||
    public required double Cost { get; set; }
 | 
			
		||||
    public required long FileCount { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class TotalUsageDetails
 | 
			
		||||
{
 | 
			
		||||
    public required List<UsageDetails> PoolUsages { get; set; }
 | 
			
		||||
    public required long TotalUsageBytes { get; set; }
 | 
			
		||||
    public required long TotalFileCount { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    // Quota, cannot be loaded in the service, cause circular dependency
 | 
			
		||||
    // Let the controller do the calculation
 | 
			
		||||
    public long? TotalQuota { get; set; }
 | 
			
		||||
    public long? UsedQuota { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class UsageService(AppDatabase db)
 | 
			
		||||
{
 | 
			
		||||
    public async Task<TotalUsageDetails> GetTotalUsage(Guid accountId)
 | 
			
		||||
    {
 | 
			
		||||
        var now = SystemClock.Instance.GetCurrentInstant();
 | 
			
		||||
        var fileQuery = db.Files
 | 
			
		||||
            .Where(f => !f.IsMarkedRecycle)
 | 
			
		||||
            .Where(f => !f.ExpiredAt.HasValue || f.ExpiredAt > now)
 | 
			
		||||
            .Where(f => f.AccountId == accountId)
 | 
			
		||||
            .AsQueryable();
 | 
			
		||||
        
 | 
			
		||||
        var poolUsages = await db.Pools
 | 
			
		||||
            .Select(p => new UsageDetails
 | 
			
		||||
            {
 | 
			
		||||
                PoolId = p.Id,
 | 
			
		||||
                PoolName = p.Name,
 | 
			
		||||
                UsageBytes = fileQuery
 | 
			
		||||
                    .Where(f => f.PoolId == p.Id)
 | 
			
		||||
                    .Sum(f => f.Size),
 | 
			
		||||
                Cost = fileQuery
 | 
			
		||||
                           .Where(f => f.PoolId == p.Id)
 | 
			
		||||
                           .Sum(f => f.Size) / 1024.0 / 1024.0 *
 | 
			
		||||
                       (p.BillingConfig.CostMultiplier ?? 1.0),
 | 
			
		||||
                FileCount = fileQuery
 | 
			
		||||
                    .Count(f => f.PoolId == p.Id)
 | 
			
		||||
            })
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
 | 
			
		||||
        var totalUsage = poolUsages.Sum(p => p.UsageBytes);
 | 
			
		||||
        var totalFileCount = poolUsages.Sum(p => p.FileCount);
 | 
			
		||||
 | 
			
		||||
        return new TotalUsageDetails
 | 
			
		||||
        {
 | 
			
		||||
            PoolUsages = poolUsages,
 | 
			
		||||
            TotalUsageBytes = totalUsage,
 | 
			
		||||
            TotalFileCount = totalFileCount,
 | 
			
		||||
            UsedQuota = await GetTotalBillableUsage(accountId)
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<UsageDetails?> GetPoolUsage(Guid poolId, Guid accountId)
 | 
			
		||||
    {
 | 
			
		||||
        var pool = await db.Pools.FindAsync(poolId);
 | 
			
		||||
        if (pool == null)
 | 
			
		||||
        {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        var now = SystemClock.Instance.GetCurrentInstant();
 | 
			
		||||
        var fileQuery = db.Files
 | 
			
		||||
            .Where(f => !f.IsMarkedRecycle)
 | 
			
		||||
            .Where(f => f.ExpiredAt.HasValue && f.ExpiredAt > now)
 | 
			
		||||
            .Where(f => f.AccountId == accountId)
 | 
			
		||||
            .AsQueryable();
 | 
			
		||||
 | 
			
		||||
        var usageBytes = await fileQuery
 | 
			
		||||
            .SumAsync(f => f.Size);
 | 
			
		||||
 | 
			
		||||
        var fileCount = await fileQuery
 | 
			
		||||
            .CountAsync();
 | 
			
		||||
 | 
			
		||||
        var cost = usageBytes / 1024.0 / 1024.0 *
 | 
			
		||||
                   (pool.BillingConfig.CostMultiplier ?? 1.0);
 | 
			
		||||
 | 
			
		||||
        return new UsageDetails
 | 
			
		||||
        {
 | 
			
		||||
            PoolId = pool.Id,
 | 
			
		||||
            PoolName = pool.Name,
 | 
			
		||||
            UsageBytes = usageBytes,
 | 
			
		||||
            Cost = cost,
 | 
			
		||||
            FileCount = fileCount
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<long> GetTotalBillableUsage(Guid accountId)
 | 
			
		||||
    {
 | 
			
		||||
        var now = SystemClock.Instance.GetCurrentInstant();
 | 
			
		||||
        var files = await db.Files
 | 
			
		||||
            .Where(f => f.AccountId == accountId)
 | 
			
		||||
            .Where(f => f.PoolId.HasValue)
 | 
			
		||||
            .Where(f => !f.IsMarkedRecycle)
 | 
			
		||||
            .Include(f => f.Pool)
 | 
			
		||||
            .Where(f => !f.ExpiredAt.HasValue || f.ExpiredAt > now)
 | 
			
		||||
            .Select(f => new
 | 
			
		||||
            {
 | 
			
		||||
                f.Size,
 | 
			
		||||
                Multiplier = f.Pool!.BillingConfig.CostMultiplier ?? 1.0
 | 
			
		||||
            })
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
 | 
			
		||||
        var totalCost = files.Sum(f => f.Size * f.Multiplier) / 1024.0 / 1024.0;
 | 
			
		||||
 | 
			
		||||
        return (long)Math.Ceiling(totalCost);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,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,
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
@@ -1,60 +0,0 @@
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
EXPOSE 8080
 | 
			
		||||
EXPOSE 8081
 | 
			
		||||
 | 
			
		||||
# Install only necessary dependencies
 | 
			
		||||
RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
			
		||||
  libfontconfig1 \
 | 
			
		||||
  libfreetype6 \
 | 
			
		||||
  libpng-dev \
 | 
			
		||||
  libharfbuzz0b \
 | 
			
		||||
  libgif7 \
 | 
			
		||||
  libvips \
 | 
			
		||||
  ffmpeg \
 | 
			
		||||
  && apt-get clean \
 | 
			
		||||
  && rm -rf /var/lib/apt/lists/* \
 | 
			
		||||
    
 | 
			
		||||
USER $APP_UID
 | 
			
		||||
 | 
			
		||||
# Stage 2: Build SPA
 | 
			
		||||
FROM node:22-alpine AS spa-builder
 | 
			
		||||
WORKDIR /src
 | 
			
		||||
 | 
			
		||||
# Copy package files for SPA
 | 
			
		||||
COPY ["DysonNetwork.Drive/Client/package.json", "DysonNetwork.Drive/Client/package-lock.json*", "./Client/"]
 | 
			
		||||
 | 
			
		||||
# Install SPA dependencies
 | 
			
		||||
WORKDIR /src/Client
 | 
			
		||||
RUN npm install
 | 
			
		||||
 | 
			
		||||
# Copy SPA source
 | 
			
		||||
COPY ["DysonNetwork.Drive/Client/", "./"]
 | 
			
		||||
 | 
			
		||||
# Build SPA
 | 
			
		||||
RUN npm run build
 | 
			
		||||
 | 
			
		||||
# Stage 3: Build .NET application
 | 
			
		||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
 | 
			
		||||
ARG BUILD_CONFIGURATION=Release
 | 
			
		||||
WORKDIR /src
 | 
			
		||||
COPY ["DysonNetwork.Drive/DysonNetwork.Drive.csproj", "DysonNetwork.Drive/"]
 | 
			
		||||
RUN dotnet restore "DysonNetwork.Drive/DysonNetwork.Drive.csproj"
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
# Copy built SPA to wwwroot
 | 
			
		||||
COPY --from=spa-builder /src/Client/dist /src/DysonNetwork.Drive/wwwroot/dist
 | 
			
		||||
 | 
			
		||||
WORKDIR "/src/DysonNetwork.Drive"
 | 
			
		||||
RUN dotnet build "./DysonNetwork.Drive.csproj" -c $BUILD_CONFIGURATION -o /app/build \
 | 
			
		||||
    -p:TypeScriptCompileBlocked=true \
 | 
			
		||||
    -p:UseRazorBuildServer=false
 | 
			
		||||
 | 
			
		||||
FROM build AS publish
 | 
			
		||||
ARG BUILD_CONFIGURATION=Release
 | 
			
		||||
RUN dotnet publish "./DysonNetwork.Drive.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
 | 
			
		||||
 | 
			
		||||
FROM base AS final
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
COPY --from=publish /app/publish .
 | 
			
		||||
ENTRYPOINT ["dotnet", "DysonNetwork.Drive.dll"]
 | 
			
		||||
@@ -1,81 +0,0 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk.Web">
 | 
			
		||||
 | 
			
		||||
    <PropertyGroup>
 | 
			
		||||
        <TargetFramework>net9.0</TargetFramework>
 | 
			
		||||
        <Nullable>enable</Nullable>
 | 
			
		||||
        <ImplicitUsings>enable</ImplicitUsings>
 | 
			
		||||
        <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
 | 
			
		||||
    </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
        <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
 | 
			
		||||
        <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
 | 
			
		||||
        <PackageReference Include="FFMpegCore" Version="5.2.0" />
 | 
			
		||||
        <PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
 | 
			
		||||
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
 | 
			
		||||
        <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
 | 
			
		||||
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
			
		||||
          <PrivateAssets>all</PrivateAssets>
 | 
			
		||||
        </PackageReference>
 | 
			
		||||
        <PackageReference Include="MimeTypes" Version="2.5.2">
 | 
			
		||||
          <PrivateAssets>all</PrivateAssets>
 | 
			
		||||
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
			
		||||
        </PackageReference>
 | 
			
		||||
        <PackageReference Include="Minio" Version="6.0.5" />
 | 
			
		||||
        <PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
 | 
			
		||||
          <PrivateAssets>all</PrivateAssets>
 | 
			
		||||
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
			
		||||
        </PackageReference>
 | 
			
		||||
        <PackageReference Include="NetVips" Version="3.1.0" />
 | 
			
		||||
        <PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
 | 
			
		||||
        <PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.1" />
 | 
			
		||||
        <PackageReference Include="NodaTime" Version="3.2.2" />
 | 
			
		||||
        <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
 | 
			
		||||
        <PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
 | 
			
		||||
        <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
 | 
			
		||||
        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
 | 
			
		||||
        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
 | 
			
		||||
        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
 | 
			
		||||
        <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
 | 
			
		||||
        <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
 | 
			
		||||
        <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
 | 
			
		||||
        <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
 | 
			
		||||
        <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
 | 
			
		||||
        <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
 | 
			
		||||
        <PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
 | 
			
		||||
        <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
 | 
			
		||||
        <PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5" />
 | 
			
		||||
        <PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0" />
 | 
			
		||||
        <PackageReference Include="Quartz" Version="3.14.0" />
 | 
			
		||||
        <PackageReference Include="Quartz.AspNetCore" Version="3.14.0" />
 | 
			
		||||
        <PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
 | 
			
		||||
        <PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
 | 
			
		||||
        <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" />
 | 
			
		||||
        <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
 | 
			
		||||
        <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
 | 
			
		||||
        <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" />
 | 
			
		||||
        <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
 | 
			
		||||
        <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
 | 
			
		||||
        <PackageReference Include="tusdotnet" Version="2.10.0" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
      <Content Include="..\.dockerignore">
 | 
			
		||||
        <Link>.dockerignore</Link>
 | 
			
		||||
      </Content>
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
      <ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" />
 | 
			
		||||
      <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
    <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>
 | 
			
		||||
@@ -1,190 +0,0 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using DysonNetwork.Drive;
 | 
			
		||||
using DysonNetwork.Drive.Storage;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(AppDatabase))]
 | 
			
		||||
    [Migration("20250713121317_InitialMigration")]
 | 
			
		||||
    partial class InitialMigration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<string>("Id")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("FileMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("file_meta");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasCompression")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_compression");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Hash")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("hash");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsMarkedRecycle")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_marked_recycle");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("MimeType")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("mime_type");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("sensitive_marks");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("Size")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("size");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageId")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("storage_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageUrl")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("storage_url");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("UploadedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("uploaded_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("UploadedTo")
 | 
			
		||||
                        .HasMaxLength(128)
 | 
			
		||||
                        .HasColumnType("character varying(128)")
 | 
			
		||||
                        .HasColumnName("uploaded_to");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("UserMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("user_meta");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_files");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("files", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("FileId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("ResourceId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("resource_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Usage")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("usage");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_file_references");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("FileId")
 | 
			
		||||
                        .HasDatabaseName("ix_file_references_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("file_references", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("FileId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_file_references_files_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("File");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,90 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using DysonNetwork.Drive.Storage;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class InitialMigration : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.AlterDatabase()
 | 
			
		||||
                .Annotation("Npgsql:PostgresExtension:postgis", ",,");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "files",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<string>(type: "character varying(32)", maxLength: 32, 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),
 | 
			
		||||
                    file_meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
 | 
			
		||||
                    user_meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
 | 
			
		||||
                    sensitive_marks = table.Column<List<ContentSensitiveMark>>(type: "jsonb", nullable: true),
 | 
			
		||||
                    mime_type = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
 | 
			
		||||
                    hash = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
 | 
			
		||||
                    size = table.Column<long>(type: "bigint", nullable: false),
 | 
			
		||||
                    uploaded_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
 | 
			
		||||
                    uploaded_to = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
 | 
			
		||||
                    has_compression = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
                    is_marked_recycle = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
                    storage_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
 | 
			
		||||
                    storage_url = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
 | 
			
		||||
                    account_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_files", x => x.id);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "file_references",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    file_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
 | 
			
		||||
                    usage = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    resource_id = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
 | 
			
		||||
                    created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_file_references", x => x.id);
 | 
			
		||||
                    table.ForeignKey(
 | 
			
		||||
                        name: "fk_file_references_files_file_id",
 | 
			
		||||
                        column: x => x.file_id,
 | 
			
		||||
                        principalTable: "files",
 | 
			
		||||
                        principalColumn: "id",
 | 
			
		||||
                        onDelete: ReferentialAction.Cascade);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_file_references_file_id",
 | 
			
		||||
                table: "file_references",
 | 
			
		||||
                column: "file_id");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "file_references");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "files");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,190 +0,0 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using DysonNetwork.Drive;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(AppDatabase))]
 | 
			
		||||
    [Migration("20250715080004_ReinitalMigration")]
 | 
			
		||||
    partial class ReinitalMigration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<string>("Id")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("FileMeta")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("file_meta");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasCompression")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_compression");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Hash")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("hash");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsMarkedRecycle")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_marked_recycle");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("MimeType")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("mime_type");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("sensitive_marks");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("Size")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("size");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageId")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("storage_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageUrl")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("storage_url");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("UploadedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("uploaded_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("UploadedTo")
 | 
			
		||||
                        .HasMaxLength(128)
 | 
			
		||||
                        .HasColumnType("character varying(128)")
 | 
			
		||||
                        .HasColumnName("uploaded_to");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("UserMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("user_meta");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_files");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("files", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("FileId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("ResourceId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("resource_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Usage")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("usage");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_file_references");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("FileId")
 | 
			
		||||
                        .HasDatabaseName("ix_file_references_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("file_references", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("FileId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_file_references_files_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("File");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class ReinitalMigration : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.AlterColumn<Dictionary<string, object>>(
 | 
			
		||||
                name: "file_meta",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                type: "jsonb",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(Dictionary<string, object>),
 | 
			
		||||
                oldType: "jsonb",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.AlterColumn<Dictionary<string, object>>(
 | 
			
		||||
                name: "file_meta",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                type: "jsonb",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(Dictionary<string, object>),
 | 
			
		||||
                oldType: "jsonb");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,265 +0,0 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using DysonNetwork.Drive;
 | 
			
		||||
using DysonNetwork.Drive.Storage;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(AppDatabase))]
 | 
			
		||||
    [Migration("20250726103203_AddCloudFilePool")]
 | 
			
		||||
    partial class AddCloudFilePool
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<string>("Id")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("FileMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("file_meta");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasCompression")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_compression");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasThumbnail")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_thumbnail");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Hash")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("hash");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsEncrypted")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_encrypted");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsMarkedRecycle")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_marked_recycle");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("MimeType")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("mime_type");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("PoolId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("sensitive_marks");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("Size")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("size");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageId")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("storage_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageUrl")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("storage_url");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("UploadedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("uploaded_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("UploadedTo")
 | 
			
		||||
                        .HasMaxLength(128)
 | 
			
		||||
                        .HasColumnType("character varying(128)")
 | 
			
		||||
                        .HasColumnName("uploaded_to");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("UserMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("user_meta");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_files");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PoolId")
 | 
			
		||||
                        .HasDatabaseName("ix_files_pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("files", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("FileId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("ResourceId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("resource_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Usage")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("usage");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_file_references");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("FileId")
 | 
			
		||||
                        .HasDatabaseName("ix_file_references_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("file_references", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<BillingConfig>("BillingConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("billing_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<PolicyConfig>("PolicyConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("policy_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<RemoteStorageConfig>("StorageConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("storage_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_pools");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("pools", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("PoolId")
 | 
			
		||||
                        .HasConstraintName("fk_files_pools_pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Pool");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("FileId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_file_references_files_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("File");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,113 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using DysonNetwork.Drive.Storage;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class AddCloudFilePool : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.AlterColumn<Dictionary<string, object>>(
 | 
			
		||||
                name: "file_meta",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                type: "jsonb",
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(Dictionary<string, object>),
 | 
			
		||||
                oldType: "jsonb");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddColumn<bool>(
 | 
			
		||||
                name: "has_thumbnail",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                type: "boolean",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: false);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddColumn<bool>(
 | 
			
		||||
                name: "is_encrypted",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                type: "boolean",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: false);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddColumn<Guid>(
 | 
			
		||||
                name: "pool_id",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                type: "uuid",
 | 
			
		||||
                nullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "pools",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    storage_config = table.Column<RemoteStorageConfig>(type: "jsonb", nullable: false),
 | 
			
		||||
                    billing_config = table.Column<BillingConfig>(type: "jsonb", nullable: false),
 | 
			
		||||
                    policy_config = table.Column<PolicyConfig>(type: "jsonb", nullable: false),
 | 
			
		||||
                    account_id = table.Column<Guid>(type: "uuid", nullable: true),
 | 
			
		||||
                    created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_pools", x => x.id);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_files_pool_id",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                column: "pool_id");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddForeignKey(
 | 
			
		||||
                name: "fk_files_pools_pool_id",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                column: "pool_id",
 | 
			
		||||
                principalTable: "pools",
 | 
			
		||||
                principalColumn: "id");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropForeignKey(
 | 
			
		||||
                name: "fk_files_pools_pool_id",
 | 
			
		||||
                table: "files");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "pools");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropIndex(
 | 
			
		||||
                name: "ix_files_pool_id",
 | 
			
		||||
                table: "files");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "has_thumbnail",
 | 
			
		||||
                table: "files");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "is_encrypted",
 | 
			
		||||
                table: "files");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "pool_id",
 | 
			
		||||
                table: "files");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<Dictionary<string, object>>(
 | 
			
		||||
                name: "file_meta",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                type: "jsonb",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(Dictionary<string, object>),
 | 
			
		||||
                oldType: "jsonb",
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,271 +0,0 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using DysonNetwork.Drive;
 | 
			
		||||
using DysonNetwork.Drive.Storage;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(AppDatabase))]
 | 
			
		||||
    [Migration("20250726120323_AddFilePoolDescription")]
 | 
			
		||||
    partial class AddFilePoolDescription
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<string>("Id")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("FileMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("file_meta");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasCompression")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_compression");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasThumbnail")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_thumbnail");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Hash")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("hash");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsEncrypted")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_encrypted");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsMarkedRecycle")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_marked_recycle");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("MimeType")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("mime_type");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("PoolId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("sensitive_marks");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("Size")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("size");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageId")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("storage_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageUrl")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("storage_url");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("UploadedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("uploaded_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("UploadedTo")
 | 
			
		||||
                        .HasMaxLength(128)
 | 
			
		||||
                        .HasColumnType("character varying(128)")
 | 
			
		||||
                        .HasColumnName("uploaded_to");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("UserMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("user_meta");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_files");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PoolId")
 | 
			
		||||
                        .HasDatabaseName("ix_files_pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("files", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("FileId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("ResourceId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("resource_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Usage")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("usage");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_file_references");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("FileId")
 | 
			
		||||
                        .HasDatabaseName("ix_file_references_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("file_references", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<BillingConfig>("BillingConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("billing_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(8192)
 | 
			
		||||
                        .HasColumnType("character varying(8192)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<PolicyConfig>("PolicyConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("policy_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<RemoteStorageConfig>("StorageConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("storage_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_pools");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("pools", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("PoolId")
 | 
			
		||||
                        .HasConstraintName("fk_files_pools_pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Pool");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("FileId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_file_references_files_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("File");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class AddFilePoolDescription : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.AddColumn<string>(
 | 
			
		||||
                name: "description",
 | 
			
		||||
                table: "pools",
 | 
			
		||||
                type: "character varying(8192)",
 | 
			
		||||
                maxLength: 8192,
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: "");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "description",
 | 
			
		||||
                table: "pools");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,275 +0,0 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using DysonNetwork.Drive;
 | 
			
		||||
using DysonNetwork.Drive.Storage;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(AppDatabase))]
 | 
			
		||||
    [Migration("20250726172039_AddCloudFileExpiration")]
 | 
			
		||||
    partial class AddCloudFileExpiration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<string>("Id")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("FileMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("file_meta");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasCompression")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_compression");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasThumbnail")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_thumbnail");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Hash")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("hash");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsEncrypted")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_encrypted");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsMarkedRecycle")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_marked_recycle");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("MimeType")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("mime_type");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("PoolId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("sensitive_marks");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("Size")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("size");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageId")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("storage_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageUrl")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("storage_url");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("UploadedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("uploaded_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("UploadedTo")
 | 
			
		||||
                        .HasMaxLength(128)
 | 
			
		||||
                        .HasColumnType("character varying(128)")
 | 
			
		||||
                        .HasColumnName("uploaded_to");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("UserMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("user_meta");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_files");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PoolId")
 | 
			
		||||
                        .HasDatabaseName("ix_files_pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("files", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("FileId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("ResourceId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("resource_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Usage")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("usage");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_file_references");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("FileId")
 | 
			
		||||
                        .HasDatabaseName("ix_file_references_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("file_references", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<BillingConfig>("BillingConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("billing_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(8192)
 | 
			
		||||
                        .HasColumnType("character varying(8192)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<PolicyConfig>("PolicyConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("policy_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<RemoteStorageConfig>("StorageConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("storage_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_pools");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("pools", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("PoolId")
 | 
			
		||||
                        .HasConstraintName("fk_files_pools_pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Pool");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("FileId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_file_references_files_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("File");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class AddCloudFileExpiration : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.AddColumn<Instant>(
 | 
			
		||||
                name: "expired_at",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                type: "timestamp with time zone",
 | 
			
		||||
                nullable: true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "expired_at",
 | 
			
		||||
                table: "files");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,322 +0,0 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using DysonNetwork.Drive;
 | 
			
		||||
using DysonNetwork.Drive.Storage;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(AppDatabase))]
 | 
			
		||||
    [Migration("20250727092028_AddQuotaRecord")]
 | 
			
		||||
    partial class AddQuotaRecord
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("text")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("text")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("Quota")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("quota");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_quota_records");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("quota_records", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<string>("Id")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("FileMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("file_meta");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasCompression")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_compression");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasThumbnail")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_thumbnail");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Hash")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("hash");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsEncrypted")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_encrypted");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsMarkedRecycle")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_marked_recycle");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("MimeType")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("mime_type");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("PoolId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("sensitive_marks");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("Size")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("size");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageId")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("storage_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageUrl")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("storage_url");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("UploadedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("uploaded_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("UploadedTo")
 | 
			
		||||
                        .HasMaxLength(128)
 | 
			
		||||
                        .HasColumnType("character varying(128)")
 | 
			
		||||
                        .HasColumnName("uploaded_to");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("UserMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("user_meta");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_files");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PoolId")
 | 
			
		||||
                        .HasDatabaseName("ix_files_pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("files", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("FileId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("ResourceId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("resource_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Usage")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("usage");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_file_references");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("FileId")
 | 
			
		||||
                        .HasDatabaseName("ix_file_references_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("file_references", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<BillingConfig>("BillingConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("billing_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(8192)
 | 
			
		||||
                        .HasColumnType("character varying(8192)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<PolicyConfig>("PolicyConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("policy_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<RemoteStorageConfig>("StorageConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("storage_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_pools");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("pools", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("PoolId")
 | 
			
		||||
                        .HasConstraintName("fk_files_pools_pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Pool");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("FileId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_file_references_files_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("File");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,42 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class AddQuotaRecord : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "quota_records",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    account_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    name = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
                    description = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
                    quota = table.Column<long>(type: "bigint", nullable: false),
 | 
			
		||||
                    expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
 | 
			
		||||
                    created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_quota_records", x => x.id);
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "quota_records");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,400 +0,0 @@
 | 
			
		||||
// <auto-generated />
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using DysonNetwork.Drive;
 | 
			
		||||
using DysonNetwork.Drive.Storage;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    [DbContext(typeof(AppDatabase))]
 | 
			
		||||
    [Migration("20250727130951_AddFileBundle")]
 | 
			
		||||
    partial class AddFileBundle
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void BuildTargetModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "9.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("text")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("text")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("Quota")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("quota");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_quota_records");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("quota_records", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<string>("Id")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("BundleId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("bundle_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("FileMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("file_meta");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasCompression")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_compression");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("HasThumbnail")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("has_thumbnail");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Hash")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("hash");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsEncrypted")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_encrypted");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsMarkedRecycle")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("is_marked_recycle");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("MimeType")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("mime_type");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("PoolId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("sensitive_marks");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("Size")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("size");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageId")
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("storage_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("StorageUrl")
 | 
			
		||||
                        .HasMaxLength(4096)
 | 
			
		||||
                        .HasColumnType("character varying(4096)")
 | 
			
		||||
                        .HasColumnName("storage_url");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("UploadedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("uploaded_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("UploadedTo")
 | 
			
		||||
                        .HasMaxLength(128)
 | 
			
		||||
                        .HasColumnType("character varying(128)")
 | 
			
		||||
                        .HasColumnName("uploaded_to");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, object>>("UserMeta")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("user_meta");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_files");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("BundleId")
 | 
			
		||||
                        .HasDatabaseName("ix_files_bundle_id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PoolId")
 | 
			
		||||
                        .HasDatabaseName("ix_files_pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("files", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("FileId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(32)
 | 
			
		||||
                        .HasColumnType("character varying(32)")
 | 
			
		||||
                        .HasColumnName("file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("ResourceId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("resource_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Usage")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("usage");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_file_references");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("FileId")
 | 
			
		||||
                        .HasDatabaseName("ix_file_references_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("file_references", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .HasMaxLength(8192)
 | 
			
		||||
                        .HasColumnType("character varying(8192)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("expired_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Passcode")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasColumnName("passcode");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Slug")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("slug");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_bundles");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("Slug")
 | 
			
		||||
                        .IsUnique()
 | 
			
		||||
                        .HasDatabaseName("ix_bundles_slug");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("bundles", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<BillingConfig>("BillingConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("billing_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Description")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(8192)
 | 
			
		||||
                        .HasColumnType("character varying(8192)")
 | 
			
		||||
                        .HasColumnName("description");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Name")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<PolicyConfig>("PolicyConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("policy_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<RemoteStorageConfig>("StorageConfig")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("storage_config");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_pools");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("pools", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.FileBundle", "Bundle")
 | 
			
		||||
                        .WithMany("Files")
 | 
			
		||||
                        .HasForeignKey("BundleId")
 | 
			
		||||
                        .HasConstraintName("fk_files_bundles_bundle_id");
 | 
			
		||||
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("PoolId")
 | 
			
		||||
                        .HasConstraintName("fk_files_pools_pool_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Bundle");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Pool");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("FileId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_file_references_files_file_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("File");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Navigation("Files");
 | 
			
		||||
                });
 | 
			
		||||
#pragma warning restore 612, 618
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,79 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Drive.Migrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class AddFileBundle : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.AddColumn<Guid>(
 | 
			
		||||
                name: "bundle_id",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                type: "uuid",
 | 
			
		||||
                nullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "bundles",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    description = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: true),
 | 
			
		||||
                    passcode = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
 | 
			
		||||
                    expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
 | 
			
		||||
                    account_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_bundles", x => x.id);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_files_bundle_id",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                column: "bundle_id");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_bundles_slug",
 | 
			
		||||
                table: "bundles",
 | 
			
		||||
                column: "slug",
 | 
			
		||||
                unique: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddForeignKey(
 | 
			
		||||
                name: "fk_files_bundles_bundle_id",
 | 
			
		||||
                table: "files",
 | 
			
		||||
                column: "bundle_id",
 | 
			
		||||
                principalTable: "bundles",
 | 
			
		||||
                principalColumn: "id");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropForeignKey(
 | 
			
		||||
                name: "fk_files_bundles_bundle_id",
 | 
			
		||||
                table: "files");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "bundles");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropIndex(
 | 
			
		||||
                name: "ix_files_bundle_id",
 | 
			
		||||
                table: "files");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "bundle_id",
 | 
			
		||||
                table: "files");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user