Compare commits
	
		
			43 Commits
		
	
	
		
			b6d416a3a8
			...
			4a4e7a302b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4a4e7a302b | |||
| f1a6d4ab90 | |||
| 609e30b67b | |||
| d22394230b | |||
| fc63a76eb2 | |||
| a37ca3c772 | |||
| 7b9150bd88 | |||
| 3380c8f688 | |||
| da5b3ac261 | |||
| 921a10f7ab | |||
| 4398984551 | |||
| e0e1eb76cd | |||
| 57f85ec341 | |||
| 086a12f971 | |||
| 651820e384 | |||
| 4e2a7ebbce | |||
| b14af43996 | |||
| 022f89c36e | |||
| e4dcf2517a | |||
| cd4af2e26f | |||
| 5549051ec5 | |||
| 3310487aba | |||
| 21b42b5b21 | |||
| 8fbc81cab9 | |||
| 3c11c4f3be | |||
| a03b8d1cac | |||
| cbfdb4aa60 | |||
| ef9175d27d | |||
| 06f1cc3ca1 | |||
| 92ab7a1a2a | |||
| 28067d18f6 | |||
| 387246a95c | |||
| 007da589bf | |||
| cde55eb237 | |||
| 03e26ef93c | |||
| afdbde951c | |||
| e66abe2e0c | |||
| 4a7f2e18b3 | |||
| e1b47bc7d1 | |||
| 33f56c4ef5 | |||
| 0318364bcf | |||
| ba49d1c7a7 | |||
| e76c80eead | 
							
								
								
									
										96
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										96
									
								
								.github/workflows/docker-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| name: Build and Push Dyson Sphere | ||||
| name: Build and Push Microservices | ||||
|  | ||||
| on: | ||||
|   push: | ||||
| @@ -7,23 +7,19 @@ on: | ||||
|   workflow_dispatch: | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest # x86_64 (default), avoids arm64 native module issues | ||||
|  | ||||
|   build-sphere: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Log in to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|  | ||||
|       - name: Build and push Docker image | ||||
|       - name: Build and push DysonNetwork.Sphere Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Sphere/Dockerfile | ||||
| @@ -31,3 +27,87 @@ jobs: | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-sphere:latest | ||||
|           platforms: linux/amd64 | ||||
|  | ||||
|   build-pass: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|       - name: Log in to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Pass Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Pass/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-pass:latest | ||||
|           platforms: linux/amd64 | ||||
|  | ||||
|   build-pusher: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|       - name: Log in to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Pusher Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Pusher/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-pusher:latest | ||||
|           platforms: linux/amd64 | ||||
|  | ||||
|   build-drive: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|       - name: Log in to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Drive Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Drive/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-drive:latest | ||||
|           platforms: linux/amd64 | ||||
|  | ||||
|   build-gateway: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|       - name: Log in to DockerHub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|       - name: Build and push DysonNetwork.Gateway Docker image | ||||
|         uses: docker/build-push-action@v6 | ||||
|         with: | ||||
|           file: DysonNetwork.Gateway/Dockerfile | ||||
|           context: . | ||||
|           push: true | ||||
|           tags: xsheep2010/dyson-gateway:latest | ||||
|           platforms: linux/amd64 | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| bin/ | ||||
| obj/ | ||||
| /packages/ | ||||
| /Certificates/ | ||||
| riderModule.iml | ||||
| /_ReSharper.Caches/ | ||||
| .idea | ||||
|   | ||||
							
								
								
									
										1
									
								
								DysonNetwork.Drive/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								DysonNetwork.Drive/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /Uploads/ | ||||
							
								
								
									
										179
									
								
								DysonNetwork.Drive/AppDatabase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								DysonNetwork.Drive/AppDatabase.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | ||||
| using System.Linq.Expressions; | ||||
| using System.Reflection; | ||||
| 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<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) | ||||
|                 .UseNetTopologySuite() | ||||
|                 .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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								DysonNetwork.Drive/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								DysonNetwork.Drive/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base | ||||
| USER $APP_UID | ||||
| WORKDIR /app | ||||
| EXPOSE 8080 | ||||
| EXPOSE 8081 | ||||
|  | ||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| WORKDIR /src | ||||
| COPY ["DysonNetwork.Drive/DysonNetwork.Drive.csproj", "DysonNetwork.Drive/"] | ||||
| RUN dotnet restore "DysonNetwork.Drive/DysonNetwork.Drive.csproj" | ||||
| COPY . . | ||||
| WORKDIR "/src/DysonNetwork.Drive" | ||||
| RUN dotnet build "./DysonNetwork.Drive.csproj" -c $BUILD_CONFIGURATION -o /app/build | ||||
|  | ||||
| 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"] | ||||
							
								
								
									
										71
									
								
								DysonNetwork.Drive/DysonNetwork.Drive.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								DysonNetwork.Drive/DysonNetwork.Drive.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||
|  | ||||
|     <PropertyGroup> | ||||
|         <TargetFramework>net9.0</TargetFramework> | ||||
|         <Nullable>enable</Nullable> | ||||
|         <ImplicitUsings>enable</ImplicitUsings> | ||||
|         <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <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.NetTopologySuite" Version="9.0.4" /> | ||||
|         <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" /> | ||||
|         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" /> | ||||
|         <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" /> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" /> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" /> | ||||
|         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" /> | ||||
|         <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" /> | ||||
|         <PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" /> | ||||
|         <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1" /> | ||||
|         <PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5" /> | ||||
|         <PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0" /> | ||||
|         <PackageReference Include="Quartz" Version="3.14.0" /> | ||||
|         <PackageReference Include="Quartz.AspNetCore" Version="3.14.0" /> | ||||
|         <PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" /> | ||||
|         <PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" /> | ||||
|         <PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" /> | ||||
|         <PackageReference Include="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.Shared\DysonNetwork.Shared.csproj" /> | ||||
|     </ItemGroup> | ||||
| </Project> | ||||
							
								
								
									
										190
									
								
								DysonNetwork.Drive/Migrations/20250713121317_InitialMigration.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								DysonNetwork.Drive/Migrations/20250713121317_InitialMigration.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,190 @@ | ||||
| // <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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,90 @@ | ||||
| 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"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										190
									
								
								DysonNetwork.Drive/Migrations/20250715080004_ReinitalMigration.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								DysonNetwork.Drive/Migrations/20250715080004_ReinitalMigration.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,190 @@ | ||||
| // <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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,36 @@ | ||||
| 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"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										187
									
								
								DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | ||||
| // <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.Storage.ValueConversion; | ||||
| using NodaTime; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Drive.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.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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										50
									
								
								DysonNetwork.Drive/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								DysonNetwork.Drive/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| using DysonNetwork.Drive; | ||||
| using DysonNetwork.Drive.Startup; | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Http; | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using tusdotnet.Stores; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| // Configure Kestrel and server options | ||||
| builder.ConfigureAppKestrel(); | ||||
|  | ||||
| // Add application services | ||||
| builder.Services.AddRegistryService(builder.Configuration); | ||||
| builder.Services.AddAppServices(builder.Configuration); | ||||
| builder.Services.AddAppRateLimiting(); | ||||
| builder.Services.AddAppAuthentication(); | ||||
| builder.Services.AddAppSwagger(); | ||||
| builder.Services.AddDysonAuth(); | ||||
|  | ||||
| builder.Services.AddAppFileStorage(builder.Configuration); | ||||
|  | ||||
| // Add flush handlers and websocket handlers | ||||
| builder.Services.AddAppFlushHandlers(); | ||||
|  | ||||
| // Add business services | ||||
| builder.Services.AddAppBusinessServices(); | ||||
|  | ||||
| // Add scheduled jobs | ||||
| builder.Services.AddAppScheduledJobs(); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| // Run database migrations | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
|     var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||
|     await db.Database.MigrateAsync(); | ||||
| } | ||||
|  | ||||
| var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>(); | ||||
|  | ||||
| // Configure application middleware pipeline | ||||
| app.ConfigureAppMiddleware(tusDiskStore); | ||||
|  | ||||
| // Configure gRPC | ||||
| app.ConfigureGrpcServices(); | ||||
|  | ||||
| app.Run(); | ||||
							
								
								
									
										23
									
								
								DysonNetwork.Drive/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								DysonNetwork.Drive/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|   "$schema": "https://json.schemastore.org/launchsettings.json", | ||||
|   "profiles": { | ||||
|     "http": { | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "http://localhost:5090", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
|     }, | ||||
|     "https": { | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "https://localhost:7092;http://localhost:5090", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										34
									
								
								DysonNetwork.Drive/Startup/ApplicationBuilderExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								DysonNetwork.Drive/Startup/ApplicationBuilderExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| using DysonNetwork.Drive.Storage; | ||||
| using tusdotnet; | ||||
| using tusdotnet.Interfaces; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Startup; | ||||
|  | ||||
| public static class ApplicationBuilderExtensions | ||||
| { | ||||
|     public static WebApplication ConfigureAppMiddleware(this WebApplication app, ITusStore tusStore) | ||||
|     { | ||||
|         // Configure the HTTP request pipeline. | ||||
|         if (app.Environment.IsDevelopment()) | ||||
|         { | ||||
|             app.UseSwagger(); | ||||
|             app.UseSwaggerUI(); | ||||
|         } | ||||
|  | ||||
|         app.UseAuthorization(); | ||||
|         app.MapControllers(); | ||||
|          | ||||
|         app.MapTus("/api/tus", _ => Task.FromResult(TusService.BuildConfiguration(tusStore, app.Configuration))); | ||||
|  | ||||
|         return app; | ||||
|     } | ||||
|  | ||||
|     public static WebApplication ConfigureGrpcServices(this WebApplication app) | ||||
|     { | ||||
|         // Map your gRPC services here | ||||
|         app.MapGrpcService<FileServiceGrpc>(); | ||||
|         app.MapGrpcService<FileReferenceServiceGrpc>(); | ||||
|  | ||||
|         return app; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										22
									
								
								DysonNetwork.Drive/Startup/ScheduledJobsConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								DysonNetwork.Drive/Startup/ScheduledJobsConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| using Quartz; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Startup; | ||||
|  | ||||
| public static class ScheduledJobsConfiguration | ||||
| { | ||||
|     public static IServiceCollection AddAppScheduledJobs(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddQuartz(q => | ||||
|         { | ||||
|             var appDatabaseRecyclingJob = new JobKey("AppDatabaseRecycling"); | ||||
|             q.AddJob<AppDatabaseRecyclingJob>(opts => opts.WithIdentity(appDatabaseRecyclingJob)); | ||||
|             q.AddTrigger(opts => opts | ||||
|                 .ForJob(appDatabaseRecyclingJob) | ||||
|                 .WithIdentity("AppDatabaseRecyclingTrigger") | ||||
|                 .WithCronSchedule("0 0 0 * * ?")); | ||||
|         }); | ||||
|         services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										144
									
								
								DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| using System.Text.Json; | ||||
| using System.Threading.RateLimiting; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.AspNetCore.RateLimiting; | ||||
| using Microsoft.OpenApi.Models; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.SystemTextJson; | ||||
| using StackExchange.Redis; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using tusdotnet.Stores; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Startup; | ||||
|  | ||||
| public static class ServiceCollectionExtensions | ||||
| { | ||||
|     public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services.AddDbContext<AppDatabase>(); // Assuming you'll have an AppDatabase | ||||
|         services.AddSingleton<IConnectionMultiplexer>(_ => | ||||
|         { | ||||
|             var connection = configuration.GetConnectionString("FastRetrieve")!; | ||||
|             return ConnectionMultiplexer.Connect(connection); | ||||
|         }); | ||||
|         services.AddSingleton<IClock>(SystemClock.Instance); | ||||
|         services.AddHttpContextAccessor(); | ||||
|         services.AddSingleton<ICacheService, CacheServiceRedis>(); // Uncomment if you have CacheServiceRedis | ||||
|  | ||||
|         services.AddHttpClient(); | ||||
|  | ||||
|         // Register gRPC services | ||||
|         services.AddGrpc(options => | ||||
|         { | ||||
|             options.EnableDetailedErrors = true; // Will be adjusted in Program.cs | ||||
|             options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB | ||||
|             options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB | ||||
|         }); | ||||
|  | ||||
|         // Register gRPC reflection for service discovery | ||||
|         services.AddGrpc(); | ||||
|  | ||||
|         services.AddControllers().AddJsonOptions(options => | ||||
|         { | ||||
|             options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|             options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; | ||||
|  | ||||
|             options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); | ||||
|         }); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppRateLimiting(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddRateLimiter(o => o.AddFixedWindowLimiter(policyName: "fixed", opts => | ||||
|         { | ||||
|             opts.Window = TimeSpan.FromMinutes(1); | ||||
|             opts.PermitLimit = 120; | ||||
|             opts.QueueLimit = 2; | ||||
|             opts.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; | ||||
|         })); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppAuthentication(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddCors(); | ||||
|         services.AddAuthorization(); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppFlushHandlers(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddSingleton<FlushBufferService>(); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppSwagger(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddEndpointsApiExplorer(); | ||||
|         services.AddSwaggerGen(options => | ||||
|         { | ||||
|             options.SwaggerDoc("v1", new OpenApiInfo | ||||
|             { | ||||
|                 Version = "v1", | ||||
|                 Title = "Dyson Drive", | ||||
|                 Description = | ||||
|                     "The file service of the Dyson Network. Mainly handling file storage and sharing. Also provide image processing and media analysis. Powered the Solar Network Drive as well.", | ||||
|                 TermsOfService = new Uri("https://solsynth.dev/terms"), // Update with actual terms | ||||
|                 License = new OpenApiLicense | ||||
|                 { | ||||
|                     Name = "APGLv3", // Update with actual license | ||||
|                     Url = new Uri("https://www.gnu.org/licenses/agpl-3.0.html") | ||||
|                 } | ||||
|             }); | ||||
|             options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme | ||||
|             { | ||||
|                 In = ParameterLocation.Header, | ||||
|                 Description = "Please enter a valid token", | ||||
|                 Name = "Authorization", | ||||
|                 Type = SecuritySchemeType.Http, | ||||
|                 BearerFormat = "JWT", | ||||
|                 Scheme = "Bearer" | ||||
|             }); | ||||
|             options.AddSecurityRequirement(new OpenApiSecurityRequirement | ||||
|             { | ||||
|                 { | ||||
|                     new OpenApiSecurityScheme | ||||
|                     { | ||||
|                         Reference = new OpenApiReference | ||||
|                         { | ||||
|                             Type = ReferenceType.SecurityScheme, | ||||
|                             Id = "Bearer" | ||||
|                         } | ||||
|                     }, | ||||
|                     [] | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|      | ||||
|     public static IServiceCollection AddAppFileStorage(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         var tusStorePath = configuration.GetSection("Tus").GetValue<string>("StorePath")!; | ||||
|         Directory.CreateDirectory(tusStorePath); | ||||
|         var tusDiskStore = new TusDiskStore(tusStorePath); | ||||
|  | ||||
|         services.AddSingleton(tusDiskStore); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
|  | ||||
|     public static IServiceCollection AddAppBusinessServices(this IServiceCollection services) | ||||
|     { | ||||
|         services.AddScoped<Storage.FileService>(); | ||||
|         services.AddScoped<Storage.FileReferenceService>(); | ||||
|          | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
| @@ -1,9 +1,13 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Google.Protobuf; | ||||
| using Newtonsoft.Json; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Storage; | ||||
| namespace DysonNetwork.Drive.Storage; | ||||
| 
 | ||||
| public class RemoteStorageConfig | ||||
| { | ||||
| @@ -28,7 +32,7 @@ public class CloudFileReferenceObject : ModelBase, ICloudFile | ||||
| { | ||||
|     public string Id { get; set; } = null!; | ||||
|     public string Name { get; set; } = string.Empty; | ||||
|     public Dictionary<string, object>? FileMeta { get; set; } = null!; | ||||
|     public Dictionary<string, object?> FileMeta { get; set; } = null!; | ||||
|     public Dictionary<string, object>? UserMeta { get; set; } = null!; | ||||
|     public string? MimeType { get; set; } | ||||
|     public string? Hash { get; set; } | ||||
| @@ -44,7 +48,7 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | ||||
| 
 | ||||
|     [MaxLength(1024)] public string Name { get; set; } = string.Empty; | ||||
|     [MaxLength(4096)] public string? Description { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object>? FileMeta { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object?> FileMeta { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object>? UserMeta { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] public List<ContentSensitiveMark>? SensitiveMarks { get; set; } = []; | ||||
|     [MaxLength(256)] public string? MimeType { get; set; } | ||||
| @@ -53,7 +57,7 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | ||||
|     public Instant? UploadedAt { get; set; } | ||||
|     [MaxLength(128)] public string? UploadedTo { get; set; } | ||||
|     public bool HasCompression { get; set; } = false; | ||||
|      | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The field is set to true if the recycling job plans to delete the file. | ||||
|     /// Due to the unstable of the recycling job, this doesn't really delete the file until a human verifies it. | ||||
| @@ -74,7 +78,6 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | ||||
|     [MaxLength(4096)] | ||||
|     public string? StorageUrl { get; set; } | ||||
| 
 | ||||
|     [JsonIgnore] public Account.Account Account { get; set; } = null!; | ||||
|     public Guid AccountId { get; set; } | ||||
| 
 | ||||
|     public CloudFileReferenceObject ToReferenceObject() | ||||
| @@ -95,24 +98,37 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     public string ResourceIdentifier => $"file/{Id}"; | ||||
| } | ||||
|     public string ResourceIdentifier => $"file:{Id}"; | ||||
| 
 | ||||
| public enum ContentSensitiveMark | ||||
| { | ||||
|     Language, | ||||
|     SexualContent, | ||||
|     Violence, | ||||
|     Profanity, | ||||
|     HateSpeech, | ||||
|     Racism, | ||||
|     AdultContent, | ||||
|     DrugAbuse, | ||||
|     AlcoholAbuse, | ||||
|     Gambling, | ||||
|     SelfHarm, | ||||
|     ChildAbuse, | ||||
|     Other | ||||
|     /// <summary> | ||||
|     /// Converts the CloudFile to a protobuf message | ||||
|     /// </summary> | ||||
|     /// <returns>The protobuf message representation of this object</returns> | ||||
|     public Shared.Proto.CloudFile ToProtoValue() | ||||
|     { | ||||
|         var proto = new Shared.Proto.CloudFile | ||||
|         { | ||||
|             Id = Id, | ||||
|             Name = Name ?? string.Empty, | ||||
|             MimeType = MimeType ?? string.Empty, | ||||
|             Hash = Hash ?? string.Empty, | ||||
|             Size = Size, | ||||
|             HasCompression = HasCompression, | ||||
|             Url = StorageUrl ?? string.Empty, | ||||
|             ContentType = MimeType ?? string.Empty, | ||||
|             UploadedAt = UploadedAt?.ToTimestamp(), | ||||
|             // Convert file metadata | ||||
|             FileMeta = ByteString.CopyFromUtf8( | ||||
|                 System.Text.Json.JsonSerializer.Serialize(FileMeta, GrpcTypeHelper.SystemTextSerializerOptions) | ||||
|             ), | ||||
|             // Convert user metadata | ||||
|             UserMeta = ByteString.CopyFromUtf8( | ||||
|                 System.Text.Json.JsonSerializer.Serialize(UserMeta, GrpcTypeHelper.SystemTextSerializerOptions) | ||||
|             ) | ||||
|         }; | ||||
| 
 | ||||
|         return proto; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public class CloudFileReference : ModelBase | ||||
| @@ -127,4 +143,21 @@ public class CloudFileReference : ModelBase | ||||
|     /// Optional expiration date for the file reference | ||||
|     /// </summary> | ||||
|     public Instant? ExpiredAt { get; set; } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Converts the CloudFileReference to a protobuf message | ||||
|     /// </summary> | ||||
|     /// <returns>The protobuf message representation of this object</returns> | ||||
|     public Shared.Proto.CloudFileReference ToProtoValue() | ||||
|     { | ||||
|         return new Shared.Proto.CloudFileReference | ||||
|         { | ||||
|             Id = Id.ToString(), | ||||
|             FileId = FileId, | ||||
|             File = File?.ToProtoValue(), | ||||
|             Usage = Usage, | ||||
|             ResourceId = ResourceId, | ||||
|             ExpiredAt = ExpiredAt?.ToTimestamp() | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using Quartz; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Storage; | ||||
| namespace DysonNetwork.Drive.Storage; | ||||
| 
 | ||||
| public class CloudFileUnusedRecyclingJob( | ||||
|     AppDatabase db, | ||||
| @@ -1,10 +1,10 @@ | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Minio.DataModel.Args; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Storage; | ||||
| namespace DysonNetwork.Drive.Storage; | ||||
| 
 | ||||
| [ApiController] | ||||
| [Route("/api/files")] | ||||
| @@ -12,8 +12,7 @@ public class FileController( | ||||
|     AppDatabase db, | ||||
|     FileService fs, | ||||
|     IConfiguration configuration, | ||||
|     IWebHostEnvironment env, | ||||
|     FileReferenceMigrationService rms | ||||
|     IWebHostEnvironment env | ||||
| ) : ControllerBase | ||||
| { | ||||
|     [HttpGet("{id}")] | ||||
| @@ -26,11 +25,11 @@ public class FileController( | ||||
|     { | ||||
|         // Support the file extension for client side data recognize | ||||
|         string? fileExtension = null; | ||||
|         if (id.Contains(".")) | ||||
|         if (id.Contains('.')) | ||||
|         { | ||||
|             var splitedId = id.Split('.'); | ||||
|             id = splitedId.First(); | ||||
|             fileExtension = splitedId.Last(); | ||||
|             var splitId = id.Split('.'); | ||||
|             id = splitId.First(); | ||||
|             fileExtension = splitId.Last(); | ||||
|         } | ||||
| 
 | ||||
|         var file = await fs.GetFileAsync(id); | ||||
| @@ -126,12 +125,12 @@ public class FileController( | ||||
|     [HttpDelete("{id}")] | ||||
|     public async Task<ActionResult> DeleteFile(string id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var userId = Guid.Parse(currentUser.Id); | ||||
| 
 | ||||
|         var file = await db.Files | ||||
|             .Where(e => e.Id == id) | ||||
|             .Where(e => e.Account.Id == userId) | ||||
|             .Where(e => e.AccountId == userId) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (file is null) return NotFound(); | ||||
| 
 | ||||
| @@ -142,13 +141,4 @@ public class FileController( | ||||
| 
 | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("/maintenance/migrateReferences")] | ||||
|     [Authorize] | ||||
|     [RequiredPermission("maintenance", "files.references")] | ||||
|     public async Task<ActionResult> MigrateFileReferences() | ||||
|     { | ||||
|         await rms.ScanAndMigrateReferences(); | ||||
|         return Ok(); | ||||
|     } | ||||
| } | ||||
| @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using Quartz; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Storage; | ||||
| namespace DysonNetwork.Drive.Storage; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Job responsible for cleaning up expired file references | ||||
| @@ -1,7 +1,9 @@ | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using EFCore.BulkExtensions; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Storage; | ||||
| namespace DysonNetwork.Drive.Storage; | ||||
| 
 | ||||
| public class FileReferenceService(AppDatabase db, FileService fileService, ICacheService cache) | ||||
| { | ||||
| @@ -18,11 +20,12 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach | ||||
|     /// <param name="duration">Optional duration after which the file expires (alternative to expiredAt)</param> | ||||
|     /// <returns>The created file reference</returns> | ||||
|     public async Task<CloudFileReference> CreateReferenceAsync( | ||||
|         string fileId,  | ||||
|         string usage,  | ||||
|         string resourceId,  | ||||
|         Instant? expiredAt = null,  | ||||
|         Duration? duration = null) | ||||
|         string fileId, | ||||
|         string usage, | ||||
|         string resourceId, | ||||
|         Instant? expiredAt = null, | ||||
|         Duration? duration = null | ||||
|     ) | ||||
|     { | ||||
|         // Calculate expiration time if needed | ||||
|         var finalExpiration = expiredAt; | ||||
| @@ -45,6 +48,25 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach | ||||
|         return reference; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<List<CloudFileReference>> CreateReferencesAsync( | ||||
|         List<string> fileId, | ||||
|         string usage, | ||||
|         string resourceId, | ||||
|         Instant? expiredAt = null, | ||||
|         Duration? duration = null | ||||
|     ) | ||||
|     { | ||||
|         var data = fileId.Select(id => new CloudFileReference | ||||
|         { | ||||
|             FileId = id, | ||||
|             Usage = usage, | ||||
|             ResourceId = resourceId, | ||||
|             ExpiredAt = expiredAt ?? SystemClock.Instance.GetCurrentInstant() + duration | ||||
|         }).ToList(); | ||||
|         await db.BulkInsertAsync(data); | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets all references to a file | ||||
|     /// </summary> | ||||
| @@ -273,8 +295,8 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach | ||||
| 
 | ||||
|             // Update newly added references with the expiration time | ||||
|             var referenceIds = await db.FileReferences | ||||
|                 .Where(r => toAdd.Select(a => a.FileId).Contains(r.FileId) &&  | ||||
|                             r.ResourceId == resourceId &&  | ||||
|                 .Where(r => toAdd.Select(a => a.FileId).Contains(r.FileId) && | ||||
|                             r.ResourceId == resourceId && | ||||
|                             r.Usage == usage) | ||||
|                 .Select(r => r.Id) | ||||
|                 .ToListAsync(); | ||||
| @@ -430,4 +452,4 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach | ||||
| 
 | ||||
|         return await SetReferenceExpirationAsync(referenceId, expiredAt); | ||||
|     } | ||||
| } | ||||
| } | ||||
							
								
								
									
										163
									
								
								DysonNetwork.Drive/Storage/FileReferenceServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								DysonNetwork.Drive/Storage/FileReferenceServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using NodaTime; | ||||
| using Duration = NodaTime.Duration; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Storage | ||||
| { | ||||
|     public class FileReferenceServiceGrpc(FileReferenceService fileReferenceService) | ||||
|         : Shared.Proto.FileReferenceService.FileReferenceServiceBase | ||||
|     { | ||||
|         public override async Task<Shared.Proto.CloudFileReference> CreateReference(CreateReferenceRequest request, | ||||
|             ServerCallContext context) | ||||
|         { | ||||
|             Instant? expiredAt = null; | ||||
|             if (request.ExpiredAt != null) | ||||
|                 expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds); | ||||
|             else if (request.Duration != null) | ||||
|                 expiredAt = SystemClock.Instance.GetCurrentInstant() + | ||||
|                             Duration.FromTimeSpan(request.Duration.ToTimeSpan()); | ||||
|  | ||||
|             var reference = await fileReferenceService.CreateReferenceAsync( | ||||
|                 request.FileId, | ||||
|                 request.Usage, | ||||
|                 request.ResourceId, | ||||
|                 expiredAt | ||||
|             ); | ||||
|             return reference.ToProtoValue(); | ||||
|         } | ||||
|  | ||||
|         public override async Task<CreateReferenceBatchResponse> CreateReferenceBatch(CreateReferenceBatchRequest request, | ||||
|             ServerCallContext context) | ||||
|         { | ||||
|             Instant? expiredAt = null; | ||||
|             if (request.ExpiredAt != null) | ||||
|                 expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds); | ||||
|             else if (request.Duration != null) | ||||
|                 expiredAt = SystemClock.Instance.GetCurrentInstant() + | ||||
|                             Duration.FromTimeSpan(request.Duration.ToTimeSpan()); | ||||
|  | ||||
|             var references = await fileReferenceService.CreateReferencesAsync( | ||||
|                 request.FilesId.ToList(), | ||||
|                 request.Usage, | ||||
|                 request.ResourceId, | ||||
|                 expiredAt | ||||
|             ); | ||||
|             var response = new CreateReferenceBatchResponse(); | ||||
|             response.References.AddRange(references.Select(r => r.ToProtoValue())); | ||||
|             return response; | ||||
|         } | ||||
|  | ||||
|         public override async Task<GetReferencesResponse> GetReferences(GetReferencesRequest request, | ||||
|             ServerCallContext context) | ||||
|         { | ||||
|             var references = await fileReferenceService.GetReferencesAsync(request.FileId); | ||||
|             var response = new GetReferencesResponse(); | ||||
|             response.References.AddRange(references.Select(r => r.ToProtoValue())); | ||||
|             return response; | ||||
|         } | ||||
|  | ||||
|         public override async Task<GetReferenceCountResponse> GetReferenceCount(GetReferenceCountRequest request, | ||||
|             ServerCallContext context) | ||||
|         { | ||||
|             var count = await fileReferenceService.GetReferenceCountAsync(request.FileId); | ||||
|             return new GetReferenceCountResponse { Count = count }; | ||||
|         } | ||||
|  | ||||
|         public override async Task<GetReferencesResponse> GetResourceReferences(GetResourceReferencesRequest request, | ||||
|             ServerCallContext context) | ||||
|         { | ||||
|             var references = await fileReferenceService.GetResourceReferencesAsync(request.ResourceId, request.Usage); | ||||
|             var response = new GetReferencesResponse(); | ||||
|             response.References.AddRange(references.Select(r => r.ToProtoValue())); | ||||
|             return response; | ||||
|         } | ||||
|  | ||||
|         public override async Task<GetResourceFilesResponse> GetResourceFiles(GetResourceFilesRequest request, | ||||
|             ServerCallContext context) | ||||
|         { | ||||
|             var files = await fileReferenceService.GetResourceFilesAsync(request.ResourceId, request.Usage); | ||||
|             var response = new GetResourceFilesResponse(); | ||||
|             response.Files.AddRange(files.Select(f => f.ToProtoValue())); | ||||
|             return response; | ||||
|         } | ||||
|  | ||||
|         public override async Task<DeleteResourceReferencesResponse> DeleteResourceReferences( | ||||
|             DeleteResourceReferencesRequest request, ServerCallContext context) | ||||
|         { | ||||
|             var deletedCount = 0; | ||||
|             if (request.Usage is null) | ||||
|                 deletedCount = await fileReferenceService.DeleteResourceReferencesAsync(request.ResourceId); | ||||
|             else | ||||
|                 deletedCount = | ||||
|                     await fileReferenceService.DeleteResourceReferencesAsync(request.ResourceId, request.Usage!); | ||||
|             return new DeleteResourceReferencesResponse { DeletedCount = deletedCount }; | ||||
|         } | ||||
|  | ||||
|         public override async Task<DeleteReferenceResponse> DeleteReference(DeleteReferenceRequest request, | ||||
|             ServerCallContext context) | ||||
|         { | ||||
|             var success = await fileReferenceService.DeleteReferenceAsync(Guid.Parse(request.ReferenceId)); | ||||
|             return new DeleteReferenceResponse { Success = success }; | ||||
|         } | ||||
|  | ||||
|         public override async Task<UpdateResourceFilesResponse> UpdateResourceFiles(UpdateResourceFilesRequest request, | ||||
|             ServerCallContext context) | ||||
|         { | ||||
|             Instant? expiredAt = null; | ||||
|             if (request.ExpiredAt != null) | ||||
|             { | ||||
|                 expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds); | ||||
|             } | ||||
|             else if (request.Duration != null) | ||||
|             { | ||||
|                 expiredAt = SystemClock.Instance.GetCurrentInstant() + | ||||
|                             Duration.FromTimeSpan(request.Duration.ToTimeSpan()); | ||||
|             } | ||||
|  | ||||
|             var references = await fileReferenceService.UpdateResourceFilesAsync( | ||||
|                 request.ResourceId, | ||||
|                 request.FileIds, | ||||
|                 request.Usage, | ||||
|                 expiredAt | ||||
|             ); | ||||
|             var response = new UpdateResourceFilesResponse(); | ||||
|             response.References.AddRange(references.Select(r => r.ToProtoValue())); | ||||
|             return response; | ||||
|         } | ||||
|  | ||||
|         public override async Task<SetReferenceExpirationResponse> SetReferenceExpiration( | ||||
|             SetReferenceExpirationRequest request, ServerCallContext context) | ||||
|         { | ||||
|             Instant? expiredAt = null; | ||||
|             if (request.ExpiredAt != null) | ||||
|             { | ||||
|                 expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds); | ||||
|             } | ||||
|             else if (request.Duration != null) | ||||
|             { | ||||
|                 expiredAt = SystemClock.Instance.GetCurrentInstant() + | ||||
|                             Duration.FromTimeSpan(request.Duration.ToTimeSpan()); | ||||
|             } | ||||
|  | ||||
|             var success = | ||||
|                 await fileReferenceService.SetReferenceExpirationAsync(Guid.Parse(request.ReferenceId), expiredAt); | ||||
|             return new SetReferenceExpirationResponse { Success = success }; | ||||
|         } | ||||
|  | ||||
|         public override async Task<SetFileReferencesExpirationResponse> SetFileReferencesExpiration( | ||||
|             SetFileReferencesExpirationRequest request, ServerCallContext context) | ||||
|         { | ||||
|             var expiredAt = Instant.FromUnixTimeSeconds(request.ExpiredAt.Seconds); | ||||
|             var updatedCount = await fileReferenceService.SetFileReferencesExpirationAsync(request.FileId, expiredAt); | ||||
|             return new SetFileReferencesExpirationResponse { UpdatedCount = updatedCount }; | ||||
|         } | ||||
|  | ||||
|         public override async Task<HasFileReferencesResponse> HasFileReferences(HasFileReferencesRequest request, | ||||
|             ServerCallContext context) | ||||
|         { | ||||
|             var hasReferences = await fileReferenceService.HasFileReferencesAsync(request.FileId); | ||||
|             return new HasFileReferencesResponse { HasReferences = hasReferences }; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,7 +1,9 @@ | ||||
| using System.Globalization; | ||||
| using FFMpegCore; | ||||
| using System.Security.Cryptography; | ||||
| using AngleSharp.Text; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Google.Protobuf.WellKnownTypes; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Minio; | ||||
| using Minio.DataModel.Args; | ||||
| @@ -9,7 +11,7 @@ using NetVips; | ||||
| using NodaTime; | ||||
| using tusdotnet.Stores; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Storage; | ||||
| namespace DysonNetwork.Drive.Storage; | ||||
| 
 | ||||
| public class FileService( | ||||
|     AppDatabase db, | ||||
| @@ -40,7 +42,6 @@ public class FileService( | ||||
|             return cachedFile; | ||||
| 
 | ||||
|         var file = await db.Files | ||||
|             .Include(f => f.Account) | ||||
|             .Where(f => f.Id == fileId) | ||||
|             .FirstOrDefaultAsync(); | ||||
| 
 | ||||
| @@ -50,6 +51,47 @@ public class FileService( | ||||
|         return file; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<List<CloudFile>> GetFilesAsync(List<string> fileIds) | ||||
|     { | ||||
|         var cachedFiles = new Dictionary<string, CloudFile>(); | ||||
|         var uncachedIds = new List<string>(); | ||||
| 
 | ||||
|         // Check cache first | ||||
|         foreach (var fileId in fileIds) | ||||
|         { | ||||
|             var cacheKey = $"{CacheKeyPrefix}{fileId}"; | ||||
|             var cachedFile = await cache.GetAsync<CloudFile>(cacheKey); | ||||
| 
 | ||||
|             if (cachedFile != null) | ||||
|                 cachedFiles[fileId] = cachedFile; | ||||
|             else | ||||
|                 uncachedIds.Add(fileId); | ||||
|         } | ||||
| 
 | ||||
|         // Load uncached files from database | ||||
|         if (uncachedIds.Count > 0) | ||||
|         { | ||||
|             var dbFiles = await db.Files | ||||
|                 .Where(f => uncachedIds.Contains(f.Id)) | ||||
|                 .ToListAsync(); | ||||
| 
 | ||||
|             // Add to cache | ||||
|             foreach (var file in dbFiles) | ||||
|             { | ||||
|                 var cacheKey = $"{CacheKeyPrefix}{file.Id}"; | ||||
|                 await cache.SetAsync(cacheKey, file, CacheDuration); | ||||
|                 cachedFiles[file.Id] = file; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Preserve original order | ||||
|         return fileIds | ||||
|             .Select(f => cachedFiles.GetValueOrDefault(f)) | ||||
|             .Where(f => f != null) | ||||
|             .Cast<CloudFile>() | ||||
|             .ToList(); | ||||
|     } | ||||
| 
 | ||||
|     private static readonly string TempFilePrefix = "dyn-cloudfile"; | ||||
| 
 | ||||
|     private static readonly string[] AnimatedImageTypes = | ||||
| @@ -58,7 +100,7 @@ public class FileService( | ||||
|     // The analysis file method no longer will remove the GPS EXIF data | ||||
|     // It should be handled on the client side, and for some specific cases it should be keep | ||||
|     public async Task<CloudFile> ProcessNewFileAsync( | ||||
|         Account.Account account, | ||||
|         Account account, | ||||
|         string fileId, | ||||
|         Stream stream, | ||||
|         string fileName, | ||||
| @@ -79,7 +121,7 @@ public class FileService( | ||||
|             MimeType = contentType, | ||||
|             Size = fileSize, | ||||
|             Hash = hash, | ||||
|             AccountId = account.Id | ||||
|             AccountId = Guid.Parse(account.Id) | ||||
|         }; | ||||
| 
 | ||||
|         var existingFile = await db.Files.FirstOrDefaultAsync(f => f.Hash == hash); | ||||
| @@ -100,7 +142,11 @@ public class FileService( | ||||
|         { | ||||
|             case "image": | ||||
|                 var blurhash = | ||||
|                     BlurHashSharp.SkiaSharp.BlurHashEncoder.Encode(xComponent: 3, yComponent: 3, filename: ogFilePath); | ||||
|                     BlurHashSharp.SkiaSharp.BlurHashEncoder.Encode( | ||||
|                         xComponent: 3, | ||||
|                         yComponent: 3, | ||||
|                         filename: ogFilePath | ||||
|                     ); | ||||
| 
 | ||||
|                 // Rewind stream | ||||
|                 stream.Position = 0; | ||||
| @@ -114,7 +160,7 @@ public class FileService( | ||||
| 
 | ||||
|                     // Try to get orientation from exif data | ||||
|                     var orientation = 1; | ||||
|                     var meta = new Dictionary<string, object> | ||||
|                     var meta = new Dictionary<string, object?> | ||||
|                     { | ||||
|                         ["blur"] = blurhash, | ||||
|                         ["format"] = format, | ||||
| @@ -134,7 +180,7 @@ public class FileService( | ||||
| 
 | ||||
|                         if (field.StartsWith("exif-")) exif[field.Replace("exif-", "")] = value; | ||||
|                         else meta[field] = value; | ||||
|                          | ||||
| 
 | ||||
|                         if (field == "orientation") orientation = (int)value; | ||||
|                     } | ||||
| 
 | ||||
| @@ -154,7 +200,7 @@ public class FileService( | ||||
|                 try | ||||
|                 { | ||||
|                     var mediaInfo = await FFProbe.AnalyseAsync(ogFilePath); | ||||
|                     file.FileMeta = new Dictionary<string, object> | ||||
|                     file.FileMeta = new Dictionary<string, object?> | ||||
|                     { | ||||
|                         ["duration"] = mediaInfo.Duration.TotalSeconds, | ||||
|                         ["format_name"] = mediaInfo.Format.FormatName, | ||||
| @@ -165,7 +211,8 @@ public class FileService( | ||||
|                         ["chapters"] = mediaInfo.Chapters, | ||||
|                     }; | ||||
|                     if (mediaInfo.PrimaryVideoStream is not null) | ||||
|                         file.FileMeta["ratio"] = mediaInfo.PrimaryVideoStream.Width / mediaInfo.PrimaryVideoStream.Height; | ||||
|                         file.FileMeta["ratio"] = | ||||
|                             mediaInfo.PrimaryVideoStream.Width / mediaInfo.PrimaryVideoStream.Height; | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
| @@ -343,6 +390,68 @@ public class FileService( | ||||
|         return file; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<CloudFile> UpdateFileAsync(CloudFile file, FieldMask updateMask) | ||||
|     { | ||||
|         var existingFile = await db.Files.FirstOrDefaultAsync(f => f.Id == file.Id); | ||||
|         if (existingFile == null) | ||||
|         { | ||||
|             throw new InvalidOperationException($"File with ID {file.Id} not found."); | ||||
|         } | ||||
| 
 | ||||
|         foreach (var path in updateMask.Paths) | ||||
|         { | ||||
|             switch (path) | ||||
|             { | ||||
|                 case "name": | ||||
|                     existingFile.Name = file.Name; | ||||
|                     break; | ||||
|                 case "description": | ||||
|                     existingFile.Description = file.Description; | ||||
|                     break; | ||||
|                 case "file_meta": | ||||
|                     existingFile.FileMeta = file.FileMeta; | ||||
|                     break; | ||||
|                 case "user_meta": | ||||
|                     existingFile.UserMeta = file.UserMeta; | ||||
|                     break; | ||||
|                 case "mime_type": | ||||
|                     existingFile.MimeType = file.MimeType; | ||||
|                     break; | ||||
|                 case "hash": | ||||
|                     existingFile.Hash = file.Hash; | ||||
|                     break; | ||||
|                 case "size": | ||||
|                     existingFile.Size = file.Size; | ||||
|                     break; | ||||
|                 case "uploaded_at": | ||||
|                     existingFile.UploadedAt = file.UploadedAt; | ||||
|                     break; | ||||
|                 case "uploaded_to": | ||||
|                     existingFile.UploadedTo = file.UploadedTo; | ||||
|                     break; | ||||
|                 case "has_compression": | ||||
|                     existingFile.HasCompression = file.HasCompression; | ||||
|                     break; | ||||
|                 case "is_marked_recycle": | ||||
|                     existingFile.IsMarkedRecycle = file.IsMarkedRecycle; | ||||
|                     break; | ||||
|                 case "storage_id": | ||||
|                     existingFile.StorageId = file.StorageId; | ||||
|                     break; | ||||
|                 case "storage_url": | ||||
|                     existingFile.StorageUrl = file.StorageUrl; | ||||
|                     break; | ||||
|                 default: | ||||
|                     logger.LogWarning("Attempted to update unknown field: {Field}", path); | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         await db.SaveChangesAsync(); | ||||
|         await _PurgeCacheAsync(file.Id); | ||||
|         return existingFile; | ||||
|     } | ||||
| 
 | ||||
|     public async Task DeleteFileAsync(CloudFile file) | ||||
|     { | ||||
|         await DeleteFileDataAsync(file); | ||||
| @@ -465,7 +574,6 @@ public class FileService( | ||||
|         if (uncachedIds.Count > 0) | ||||
|         { | ||||
|             var dbFiles = await db.Files | ||||
|                 .Include(f => f.Account) | ||||
|                 .Where(f => uncachedIds.Contains(f.Id)) | ||||
|                 .ToListAsync(); | ||||
| 
 | ||||
							
								
								
									
										70
									
								
								DysonNetwork.Drive/Storage/FileServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								DysonNetwork.Drive/Storage/FileServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Google.Protobuf.WellKnownTypes; | ||||
| using Grpc.Core; | ||||
|  | ||||
| namespace DysonNetwork.Drive.Storage | ||||
| { | ||||
|     public class FileServiceGrpc(FileService fileService) : Shared.Proto.FileService.FileServiceBase | ||||
|     { | ||||
|         public override async Task<Shared.Proto.CloudFile> GetFile(GetFileRequest request, ServerCallContext context) | ||||
|         { | ||||
|             var file = await fileService.GetFileAsync(request.Id); | ||||
|             return file?.ToProtoValue() ?? throw new RpcException(new Status(StatusCode.NotFound, "File not found")); | ||||
|         } | ||||
|  | ||||
|         public override async Task<GetFileBatchResponse> GetFileBatch(GetFileBatchRequest request, ServerCallContext context) | ||||
|         { | ||||
|             var files = await fileService.GetFilesAsync(request.Ids.ToList()); | ||||
|             return new GetFileBatchResponse { Files = { files.Select(f => f.ToProtoValue()) } }; | ||||
|         } | ||||
|  | ||||
|         public override async Task<Shared.Proto.CloudFile> UpdateFile(UpdateFileRequest request, | ||||
|             ServerCallContext context) | ||||
|         { | ||||
|             var file = await fileService.GetFileAsync(request.File.Id); | ||||
|             if (file == null) | ||||
|                 throw new RpcException(new Status(StatusCode.NotFound, "File not found")); | ||||
|             var updatedFile = await fileService.UpdateFileAsync(file, request.UpdateMask); | ||||
|             return updatedFile.ToProtoValue(); | ||||
|         } | ||||
|  | ||||
|         public override async Task<Empty> DeleteFile(DeleteFileRequest request, ServerCallContext context) | ||||
|         { | ||||
|             var file = await fileService.GetFileAsync(request.Id); | ||||
|             if (file == null) | ||||
|             { | ||||
|                 throw new RpcException(new Status(StatusCode.NotFound, "File not found")); | ||||
|             } | ||||
|  | ||||
|             await fileService.DeleteFileAsync(file); | ||||
|             return new Empty(); | ||||
|         } | ||||
|  | ||||
|         public override async Task<LoadFromReferenceResponse> LoadFromReference( | ||||
|             LoadFromReferenceRequest request, | ||||
|             ServerCallContext context | ||||
|         ) | ||||
|         { | ||||
|             // Assuming CloudFileReferenceObject is a simple class/struct that holds an ID | ||||
|             // You might need to define this or adjust the LoadFromReference method in FileService | ||||
|             var references = request.ReferenceIds.Select(id => new CloudFileReferenceObject { Id = id }).ToList(); | ||||
|             var files = await fileService.LoadFromReference(references); | ||||
|             var response = new LoadFromReferenceResponse(); | ||||
|             response.Files.AddRange(files.Where(f => f != null).Select(f => f!.ToProtoValue())); | ||||
|             return response; | ||||
|         } | ||||
|  | ||||
|         public override async Task<IsReferencedResponse> IsReferenced(IsReferencedRequest request, | ||||
|             ServerCallContext context) | ||||
|         { | ||||
|             var isReferenced = await fileService.IsReferencedAsync(request.FileId); | ||||
|             return new IsReferencedResponse { IsReferenced = isReferenced }; | ||||
|         } | ||||
|  | ||||
|         public override async Task<Empty> PurgeCache(PurgeCacheRequest request, ServerCallContext context) | ||||
|         { | ||||
|             await fileService._PurgeCacheAsync(request.FileId); | ||||
|             return new Empty(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,18 +1,18 @@ | ||||
| using System.Net; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Options; | ||||
| using tusdotnet.Interfaces; | ||||
| using tusdotnet.Models; | ||||
| using tusdotnet.Models.Configuration; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Storage; | ||||
| namespace DysonNetwork.Drive.Storage; | ||||
| 
 | ||||
| public abstract class TusService | ||||
| { | ||||
|     public static DefaultTusConfiguration BuildConfiguration(ITusStore store) => new() | ||||
|     public static DefaultTusConfiguration BuildConfiguration(ITusStore store, IConfiguration configuration) => new() | ||||
|     { | ||||
|         Store = store, | ||||
|         Events = new Events | ||||
| @@ -29,7 +29,7 @@ public abstract class TusService | ||||
|                 } | ||||
| 
 | ||||
|                 var httpContext = eventContext.HttpContext; | ||||
|                 if (httpContext.Items["CurrentUser"] is not Account.Account user) | ||||
|                 if (httpContext.Items["CurrentUser"] is not Account user) | ||||
|                 { | ||||
|                     eventContext.FailRequest(HttpStatusCode.Unauthorized); | ||||
|                     return; | ||||
| @@ -38,9 +38,10 @@ public abstract class TusService | ||||
|                 if (!user.IsSuperuser) | ||||
|                 { | ||||
|                     using var scope = httpContext.RequestServices.CreateScope(); | ||||
|                     var pm = scope.ServiceProvider.GetRequiredService<PermissionService>(); | ||||
|                     var allowed = await pm.HasPermissionAsync($"user:{user.Id}", "global", "files.create"); | ||||
|                     if (!allowed) | ||||
|                     var pm = scope.ServiceProvider.GetRequiredService<PermissionService.PermissionServiceClient>(); | ||||
|                     var allowed = await pm.HasPermissionAsync(new HasPermissionRequest | ||||
|                         { Actor = $"user:{user.Id}", Area = "global", Key = "files.create" }); | ||||
|                     if (!allowed.HasPermission) | ||||
|                         eventContext.FailRequest(HttpStatusCode.Forbidden); | ||||
|                 } | ||||
|             }, | ||||
| @@ -50,7 +51,7 @@ public abstract class TusService | ||||
|                 var services = scope.ServiceProvider; | ||||
| 
 | ||||
|                 var httpContext = eventContext.HttpContext; | ||||
|                 if (httpContext.Items["CurrentUser"] is not Account.Account user) return; | ||||
|                 if (httpContext.Items["CurrentUser"] is not Account user) return; | ||||
| 
 | ||||
|                 var file = await eventContext.GetFileAsync(); | ||||
|                 var metadata = await file.GetMetadataAsync(eventContext.CancellationToken); | ||||
| @@ -72,6 +73,13 @@ public abstract class TusService | ||||
| 
 | ||||
|                 // Dispose the stream after all processing is complete | ||||
|                 await fileStream.DisposeAsync(); | ||||
|             }, | ||||
|             OnCreateCompleteAsync = eventContext => | ||||
|             { | ||||
|                 var gatewayUrl = configuration["GatewayUrl"]; | ||||
|                 if (gatewayUrl is not null) | ||||
|                     eventContext.SetUploadUrl(new Uri(gatewayUrl + "/drive/tus/" + eventContext.FileId)); | ||||
|                 return Task.CompletedTask; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
							
								
								
									
										20
									
								
								DysonNetwork.Drive/VersionController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								DysonNetwork.Drive/VersionController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| namespace DysonNetwork.Drive; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/version")] | ||||
| public class VersionController : ControllerBase | ||||
| { | ||||
|     [HttpGet] | ||||
|     public IActionResult Get() | ||||
|     { | ||||
|         return Ok(new AppVersion | ||||
|         { | ||||
|             Version = ThisAssembly.AssemblyVersion, | ||||
|             Commit = ThisAssembly.GitCommitId, | ||||
|             UpdateDate = ThisAssembly.GitCommitDate | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										137
									
								
								DysonNetwork.Drive/appsettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								DysonNetwork.Drive/appsettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| { | ||||
|   "Debug": true, | ||||
|   "BaseUrl": "http://localhost:5071", | ||||
|   "GatewayUrl": "http://10.126.126.1:5094", | ||||
|   "Logging": { | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|       "Microsoft.AspNetCore": "Warning" | ||||
|     } | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_drive;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60", | ||||
|     "FastRetrieve": "localhost:6379", | ||||
|     "Etcd": "etcd.orb.local:2379" | ||||
|   }, | ||||
|   "Authentication": { | ||||
|     "Schemes": { | ||||
|       "Bearer": { | ||||
|         "ValidAudiences": [ | ||||
|           "http://localhost:5071", | ||||
|           "https://localhost:7099" | ||||
|         ], | ||||
|         "ValidIssuer": "solar-network" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "AuthToken": { | ||||
|     "PublicKeyPath": "Keys/PublicKey.pem", | ||||
|     "PrivateKeyPath": "Keys/PrivateKey.pem" | ||||
|   }, | ||||
|   "OidcProvider": { | ||||
|     "IssuerUri": "https://nt.solian.app", | ||||
|     "PublicKeyPath": "Keys/PublicKey.pem", | ||||
|     "PrivateKeyPath": "Keys/PrivateKey.pem", | ||||
|     "AccessTokenLifetime": "01:00:00", | ||||
|     "RefreshTokenLifetime": "30.00:00:00", | ||||
|     "AuthorizationCodeLifetime": "00:30:00", | ||||
|     "RequireHttpsMetadata": true | ||||
|   }, | ||||
|   "Tus": { | ||||
|     "StorePath": "Uploads" | ||||
|   }, | ||||
|   "Storage": { | ||||
|     "PreferredRemote": "minio", | ||||
|     "Remote": [ | ||||
|       { | ||||
|         "Id": "minio", | ||||
|         "Label": "Minio", | ||||
|         "Region": "auto", | ||||
|         "Bucket": "solar-network-development", | ||||
|         "Endpoint": "localhost:9000", | ||||
|         "SecretId": "littlesheep", | ||||
|         "SecretKey": "password", | ||||
|         "EnabledSigned": true, | ||||
|         "EnableSsl": false | ||||
|       }, | ||||
|       { | ||||
|         "Id": "cloudflare", | ||||
|         "Label": "Cloudflare R2", | ||||
|         "Region": "auto", | ||||
|         "Bucket": "solar-network", | ||||
|         "Endpoint": "0a70a6d1b7128888c823359d0008f4e1.r2.cloudflarestorage.com", | ||||
|         "SecretId": "8ff5d06c7b1639829d60bc6838a542e6", | ||||
|         "SecretKey": "fd58158c5201be16d1872c9209d9cf199421dae3c2f9972f94b2305976580d67", | ||||
|         "EnableSigned": true, | ||||
|         "EnableSsl": true | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "Captcha": { | ||||
|     "Provider": "cloudflare", | ||||
|     "ApiKey": "0x4AAAAAABCDUdOujj4feOb_", | ||||
|     "ApiSecret": "0x4AAAAAABCDUWABiJQweqlB7tYq-IqIm8U" | ||||
|   }, | ||||
|   "Notifications": { | ||||
|     "Topic": "dev.solsynth.solian", | ||||
|     "Endpoint": "http://localhost:8088" | ||||
|   }, | ||||
|   "Email": { | ||||
|     "Server": "smtp4dev.orb.local", | ||||
|     "Port": 25, | ||||
|     "UseSsl": false, | ||||
|     "Username": "no-reply@mail.solsynth.dev", | ||||
|     "Password": "password", | ||||
|     "FromAddress": "no-reply@mail.solsynth.dev", | ||||
|     "FromName": "Alphabot", | ||||
|     "SubjectPrefix": "Solar Network" | ||||
|   }, | ||||
|   "RealtimeChat": { | ||||
|     "Endpoint": "https://solar-network-im44o8gq.livekit.cloud", | ||||
|     "ApiKey": "APIs6TiL8wj3A4j", | ||||
|     "ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE" | ||||
|   }, | ||||
|   "GeoIp": { | ||||
|     "DatabasePath": "./Keys/GeoLite2-City.mmdb" | ||||
|   }, | ||||
|   "Oidc": { | ||||
|     "Google": { | ||||
|       "ClientId": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com", | ||||
|       "ClientSecret": "" | ||||
|     }, | ||||
|     "Apple": { | ||||
|       "ClientId": "dev.solsynth.solian", | ||||
|       "TeamId": "W7HPZ53V6B", | ||||
|       "KeyId": "B668YP4KBG", | ||||
|       "PrivateKeyPath": "./Keys/Solarpass.p8" | ||||
|     }, | ||||
|     "Microsoft": { | ||||
|       "ClientId": "YOUR_MICROSOFT_CLIENT_ID", | ||||
|       "ClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET", | ||||
|       "DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT" | ||||
|     } | ||||
|   }, | ||||
|   "Payment": { | ||||
|     "Auth": { | ||||
|       "Afdian": "<token here>" | ||||
|     }, | ||||
|     "Subscriptions": { | ||||
|       "Afdian": { | ||||
|         "7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary", | ||||
|         "7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova", | ||||
|         "141713ee3d6211f085b352540025c377": "solian.stellar.supernova" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "KnownProxies": [ | ||||
|     "127.0.0.1", | ||||
|     "::1" | ||||
|   ], | ||||
|   "Service": { | ||||
|     "Name": "DysonNetwork.Drive", | ||||
|     "Url": "https://localhost:7092", | ||||
|     "ClientCert": "../Certificates/client.crt", | ||||
|     "ClientKey": "../Certificates/client.key" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										7
									
								
								DysonNetwork.Drive/version.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								DysonNetwork.Drive/version.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "version": "1.0", | ||||
|   "publicReleaseRefSpec": ["^refs/heads/main$"], | ||||
|   "cloudBuild": { | ||||
|     "setVersionVariables": true | ||||
|   } | ||||
| } | ||||
							
								
								
									
										78
									
								
								DysonNetwork.Gateway/Controllers/WellKnownController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								DysonNetwork.Gateway/Controllers/WellKnownController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| using System.Text; | ||||
| using dotnet_etcd.interfaces; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Yarp.ReverseProxy.Configuration; | ||||
|  | ||||
| namespace DysonNetwork.Gateway.Controllers; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/.well-known")] | ||||
| public class WellKnownController( | ||||
|     IConfiguration configuration, | ||||
|     IProxyConfigProvider proxyConfigProvider, | ||||
|     IEtcdClient etcdClient) | ||||
|     : ControllerBase | ||||
| { | ||||
|     [HttpGet("domains")] | ||||
|     public IActionResult GetDomainMappings() | ||||
|     { | ||||
|         var domainMappings = configuration.GetSection("DomainMappings").GetChildren() | ||||
|             .ToDictionary(x => x.Key, x => x.Value); | ||||
|         return Ok(domainMappings); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("services")] | ||||
|     public IActionResult GetServices() | ||||
|     { | ||||
|         var local = configuration.GetValue<bool>("LocalMode"); | ||||
|         var response = etcdClient.GetRange("/services/"); | ||||
|         var kvs = response.Kvs; | ||||
|  | ||||
|         var serviceMap = kvs.ToDictionary( | ||||
|             kv => Encoding.UTF8.GetString(kv.Key.ToByteArray()).Replace("/services/", ""), | ||||
|             kv => Encoding.UTF8.GetString(kv.Value.ToByteArray()) | ||||
|         ); | ||||
|  | ||||
|         if (local) return Ok(serviceMap); | ||||
|          | ||||
|         var domainMappings = configuration.GetSection("DomainMappings").GetChildren() | ||||
|             .ToDictionary(x => x.Key, x => x.Value); | ||||
|         foreach (var (key, _) in serviceMap.ToList()) | ||||
|         { | ||||
|             if (!domainMappings.TryGetValue(key, out var domain)) continue; | ||||
|             if (domain is not null) | ||||
|                 serviceMap[key] = domain; | ||||
|         } | ||||
|  | ||||
|         return Ok(serviceMap); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("routes")] | ||||
|     public IActionResult GetProxyRules() | ||||
|     { | ||||
|         var config = proxyConfigProvider.GetConfig(); | ||||
|         var rules = config.Routes.Select(r => new | ||||
|         { | ||||
|             r.RouteId, | ||||
|             r.ClusterId, | ||||
|             Match = new | ||||
|             { | ||||
|                 r.Match.Path, | ||||
|                 Hosts = r.Match.Hosts != null ? string.Join(", ", r.Match.Hosts) : null | ||||
|             }, | ||||
|             Transforms = r.Transforms?.Select(t => t.Select(kv => $"{kv.Key}: {kv.Value}").ToList()) | ||||
|         }).ToList(); | ||||
|  | ||||
|         var clusters = config.Clusters.Select(c => new | ||||
|         { | ||||
|             c.ClusterId, | ||||
|             Destinations = c.Destinations?.Select(d => new | ||||
|             { | ||||
|                 d.Key, | ||||
|                 d.Value.Address | ||||
|             }).ToList() | ||||
|         }).ToList(); | ||||
|  | ||||
|         return Ok(new { Rules = rules, Clusters = clusters }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								DysonNetwork.Gateway/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								DysonNetwork.Gateway/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base | ||||
| USER $APP_UID | ||||
| WORKDIR /app | ||||
| EXPOSE 8080 | ||||
| EXPOSE 8081 | ||||
|  | ||||
| FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| WORKDIR /src | ||||
| COPY ["DysonNetwork.Gateway/DysonNetwork.Gateway.csproj", "DysonNetwork.Gateway/"] | ||||
| RUN dotnet restore "DysonNetwork.Gateway/DysonNetwork.Gateway.csproj" | ||||
| COPY . . | ||||
| WORKDIR "/src/DysonNetwork.Gateway" | ||||
| RUN dotnet build "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/build | ||||
|  | ||||
| FROM build AS publish | ||||
| ARG BUILD_CONFIGURATION=Release | ||||
| RUN dotnet publish "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false | ||||
|  | ||||
| FROM base AS final | ||||
| WORKDIR /app | ||||
| COPY --from=publish /app/publish . | ||||
| ENTRYPOINT ["dotnet", "DysonNetwork.Gateway.dll"] | ||||
							
								
								
									
										23
									
								
								DysonNetwork.Gateway/DysonNetwork.Gateway.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								DysonNetwork.Gateway/DysonNetwork.Gateway.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net9.0</TargetFramework> | ||||
|     <Nullable>enable</Nullable> | ||||
|     <ImplicitUsings>enable</ImplicitUsings> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="dotnet-etcd" Version="8.0.1" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" /> | ||||
|     <PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115"> | ||||
|       <PrivateAssets>all</PrivateAssets> | ||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|     </PackageReference> | ||||
|     <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										23
									
								
								DysonNetwork.Gateway/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								DysonNetwork.Gateway/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| using DysonNetwork.Gateway.Startup; | ||||
|  | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| // Add services to the container. | ||||
| builder.Services.AddGateway(builder.Configuration); | ||||
| builder.Services.AddControllers(); | ||||
|  | ||||
| var app = builder.Build(); | ||||
|  | ||||
| app.UseCors(opts => | ||||
|     opts.SetIsOriginAllowed(_ => true) | ||||
|         .WithExposedHeaders("*") | ||||
|         .WithHeaders() | ||||
|         .AllowCredentials() | ||||
|         .AllowAnyHeader() | ||||
|         .AllowAnyMethod() | ||||
| ); | ||||
|  | ||||
| app.MapControllers(); | ||||
| app.MapReverseProxy(); | ||||
|  | ||||
| app.Run(); | ||||
							
								
								
									
										23
									
								
								DysonNetwork.Gateway/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								DysonNetwork.Gateway/Properties/launchSettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|   "$schema": "https://json.schemastore.org/launchsettings.json", | ||||
|   "profiles": { | ||||
|     "http": { | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "http://localhost:5094", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
|     }, | ||||
|     "https": { | ||||
|       "commandName": "Project", | ||||
|       "dotnetRunMessages": true, | ||||
|       "launchBrowser": false, | ||||
|       "applicationUrl": "https://localhost:7034;http://0.0.0.0:5094", | ||||
|       "environmentVariables": { | ||||
|         "ASPNETCORE_ENVIRONMENT": "Development" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										245
									
								
								DysonNetwork.Gateway/RegistryProxyConfigProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								DysonNetwork.Gateway/RegistryProxyConfigProvider.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,245 @@ | ||||
| using System.Text; | ||||
| using dotnet_etcd.interfaces; | ||||
| using Yarp.ReverseProxy.Configuration; | ||||
| using Yarp.ReverseProxy.Forwarder; | ||||
|  | ||||
| namespace DysonNetwork.Gateway; | ||||
|  | ||||
| public class RegistryProxyConfigProvider : IProxyConfigProvider, IDisposable | ||||
| { | ||||
|     private readonly object _lock = new(); | ||||
|     private readonly IEtcdClient _etcdClient; | ||||
|     private readonly IConfiguration _configuration; | ||||
|     private readonly ILogger<RegistryProxyConfigProvider> _logger; | ||||
|     private readonly CancellationTokenSource _watchCts = new(); | ||||
|     private CancellationTokenSource _cts; | ||||
|     private IProxyConfig _config; | ||||
|  | ||||
|     public RegistryProxyConfigProvider( | ||||
|         IEtcdClient etcdClient, | ||||
|         IConfiguration configuration, | ||||
|         ILogger<RegistryProxyConfigProvider> logger | ||||
|     ) | ||||
|     { | ||||
|         _etcdClient = etcdClient; | ||||
|         _configuration = configuration; | ||||
|         _logger = logger; | ||||
|         _cts = new CancellationTokenSource(); | ||||
|         _config = LoadConfig(); | ||||
|  | ||||
|         // Watch for changes in etcd | ||||
|         _etcdClient.WatchRange("/services/", _ => | ||||
|         { | ||||
|             _logger.LogInformation("Etcd configuration changed. Reloading proxy config."); | ||||
|             ReloadConfig(); | ||||
|         }, cancellationToken: _watchCts.Token); | ||||
|     } | ||||
|  | ||||
|     public IProxyConfig GetConfig() => _config; | ||||
|  | ||||
|     private void ReloadConfig() | ||||
|     { | ||||
|         lock (_lock) | ||||
|         { | ||||
|             var oldCts = _cts; | ||||
|             _cts = new CancellationTokenSource(); | ||||
|             _config = LoadConfig(); | ||||
|             oldCts.Cancel(); | ||||
|             oldCts.Dispose(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private IProxyConfig LoadConfig() | ||||
|     { | ||||
|         _logger.LogInformation("Generating new proxy config."); | ||||
|         var response = _etcdClient.GetRange("/services/"); | ||||
|         var kvs = response.Kvs; | ||||
|  | ||||
|         var serviceMap = kvs.ToDictionary( | ||||
|             kv => Encoding.UTF8.GetString(kv.Key.ToByteArray()).Replace("/services/", ""), | ||||
|             kv => Encoding.UTF8.GetString(kv.Value.ToByteArray()) | ||||
|         ); | ||||
|  | ||||
|         var clusters = new List<ClusterConfig>(); | ||||
|         var routes = new List<RouteConfig>(); | ||||
|  | ||||
|         var domainMappings = _configuration.GetSection("DomainMappings").GetChildren() | ||||
|             .ToDictionary(x => x.Key, x => x.Value); | ||||
|  | ||||
|         var pathAliases = _configuration.GetSection("PathAliases").GetChildren() | ||||
|             .ToDictionary(x => x.Key, x => x.Value); | ||||
|  | ||||
|         var directRoutes = _configuration.GetSection("DirectRoutes").Get<List<DirectRouteConfig>>() ?? | ||||
|                            []; | ||||
|  | ||||
|         _logger.LogInformation("Indexing {ServiceCount} services from Etcd.", kvs.Count); | ||||
|  | ||||
|         var gatewayServiceName = _configuration["Service:Name"]; | ||||
|  | ||||
|         // Add direct routes | ||||
|         foreach (var directRoute in directRoutes) | ||||
|         { | ||||
|             if (serviceMap.TryGetValue(directRoute.Service, out var serviceUrl)) | ||||
|             { | ||||
|                 var existingCluster = clusters.FirstOrDefault(c => c.ClusterId == directRoute.Service); | ||||
|                 if (existingCluster is null) | ||||
|                 { | ||||
|                     var cluster = new ClusterConfig | ||||
|                     { | ||||
|                         ClusterId = directRoute.Service, | ||||
|                         Destinations = new Dictionary<string, DestinationConfig> | ||||
|                         { | ||||
|                             { "destination1", new DestinationConfig { Address = serviceUrl } } | ||||
|                         }, | ||||
|                         HttpRequest = new ForwarderRequestConfig | ||||
|                         { | ||||
|                             ActivityTimeout = directRoute.IsWebsocket ? TimeSpan.FromHours(24) : TimeSpan.FromMinutes(2) | ||||
|                         } | ||||
|                     }; | ||||
|                     clusters.Add(cluster); | ||||
|                 } | ||||
|  | ||||
|                 var route = new RouteConfig | ||||
|                 { | ||||
|                     RouteId = $"direct-{directRoute.Service}-{directRoute.Path.Replace("/", "-")}", | ||||
|                     ClusterId = directRoute.Service, | ||||
|                     Match = new RouteMatch { Path = directRoute.Path }, | ||||
|                     Timeout = directRoute.IsWebsocket ? null : TimeSpan.FromSeconds(5), | ||||
|                 }; | ||||
|                 routes.Add(route); | ||||
|                 _logger.LogInformation("    Added Direct Route: {Path} -> {Service}", directRoute.Path, | ||||
|                     directRoute.Service); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _logger.LogWarning("    Direct route service {Service} not found in Etcd.", directRoute.Service); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         foreach (var serviceName in serviceMap.Keys) | ||||
|         { | ||||
|             if (serviceName == gatewayServiceName) | ||||
|             { | ||||
|                 _logger.LogInformation("Skipping gateway service: {ServiceName}", serviceName); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             var serviceUrl = serviceMap[serviceName]; | ||||
|  | ||||
|             // Determine the path alias | ||||
|             string? pathAlias; | ||||
|             pathAlias = pathAliases.TryGetValue(serviceName, out var alias) | ||||
|                 ? alias | ||||
|                 : serviceName.Split('.').Last().ToLowerInvariant(); | ||||
|  | ||||
|             _logger.LogInformation("  Service: {ServiceName}, URL: {ServiceUrl}, Path Alias: {PathAlias}", serviceName, | ||||
|                 serviceUrl, pathAlias); | ||||
|  | ||||
|             // Check if the cluster already exists | ||||
|             var existingCluster = clusters.FirstOrDefault(c => c.ClusterId == serviceName); | ||||
|             if (existingCluster == null) | ||||
|             { | ||||
|                 var cluster = new ClusterConfig | ||||
|                 { | ||||
|                     ClusterId = serviceName, | ||||
|                     Destinations = new Dictionary<string, DestinationConfig> | ||||
|                     { | ||||
|                         { "destination1", new DestinationConfig { Address = serviceUrl } } | ||||
|                     } | ||||
|                 }; | ||||
|                 clusters.Add(cluster); | ||||
|                 _logger.LogInformation("  Added Cluster: {ServiceName}", serviceName); | ||||
|             } | ||||
|             else if (existingCluster.Destinations is not null) | ||||
|             { | ||||
|                 // Create a new cluster with merged destinations | ||||
|                 var newDestinations = new Dictionary<string, DestinationConfig>(existingCluster.Destinations) | ||||
|                 { | ||||
|                     { | ||||
|                         $"destination{existingCluster.Destinations.Count + 1}", | ||||
|                         new DestinationConfig { Address = serviceUrl } | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 var mergedCluster = new ClusterConfig | ||||
|                 { | ||||
|                     ClusterId = serviceName, | ||||
|                     Destinations = newDestinations | ||||
|                 }; | ||||
|  | ||||
|                 // Replace the existing cluster with the merged one | ||||
|                 var index = clusters.IndexOf(existingCluster); | ||||
|                 clusters[index] = mergedCluster; | ||||
|  | ||||
|                 _logger.LogInformation("  Updated Cluster {ServiceName} with {DestinationCount} destinations", | ||||
|                     serviceName, mergedCluster.Destinations.Count); | ||||
|             } | ||||
|  | ||||
|             // Host-based routing | ||||
|             if (domainMappings.TryGetValue(serviceName, out var domain)) | ||||
|             { | ||||
|                 var hostRoute = new RouteConfig | ||||
|                 { | ||||
|                     RouteId = $"{serviceName}-host", | ||||
|                     ClusterId = serviceName, | ||||
|                     Match = new RouteMatch | ||||
|                     { | ||||
|                         Hosts = [domain], | ||||
|                         Path = "/{**catch-all}" | ||||
|                     } | ||||
|                 }; | ||||
|                 routes.Add(hostRoute); | ||||
|                 _logger.LogInformation("    Added Host-based Route: {Host}", domain); | ||||
|             } | ||||
|  | ||||
|             // Path-based routing | ||||
|             var pathRoute = new RouteConfig | ||||
|             { | ||||
|                 RouteId = $"{serviceName}-path", | ||||
|                 ClusterId = serviceName, | ||||
|                 Match = new RouteMatch { Path = $"/{pathAlias}/{{**catch-all}}" }, | ||||
|                 Transforms = new List<Dictionary<string, string>> | ||||
|                 { | ||||
|                     new() { { "PathRemovePrefix", $"/{pathAlias}" } }, | ||||
|                     new() { { "PathPrefix", "/api" } } | ||||
|                 }, | ||||
|                 Timeout = TimeSpan.FromSeconds(5) | ||||
|             }; | ||||
|             routes.Add(pathRoute); | ||||
|             _logger.LogInformation("    Added Path-based Route: {Path}", pathRoute.Match.Path); | ||||
|         } | ||||
|  | ||||
|         return new CustomProxyConfig( | ||||
|             routes, | ||||
|             clusters, | ||||
|             new Microsoft.Extensions.Primitives.CancellationChangeToken(_cts.Token) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private class CustomProxyConfig( | ||||
|         IReadOnlyList<RouteConfig> routes, | ||||
|         IReadOnlyList<ClusterConfig> clusters, | ||||
|         Microsoft.Extensions.Primitives.IChangeToken changeToken | ||||
|     ) | ||||
|         : IProxyConfig | ||||
|     { | ||||
|         public IReadOnlyList<RouteConfig> Routes { get; } = routes; | ||||
|         public IReadOnlyList<ClusterConfig> Clusters { get; } = clusters; | ||||
|         public Microsoft.Extensions.Primitives.IChangeToken ChangeToken { get; } = changeToken; | ||||
|     } | ||||
|  | ||||
|     public record DirectRouteConfig | ||||
|     { | ||||
|         public required string Path { get; set; } | ||||
|         public required string Service { get; set; } | ||||
|         public bool IsWebsocket { get; set; } = false; | ||||
|     } | ||||
|  | ||||
|     public virtual void Dispose() | ||||
|     { | ||||
|         _cts.Cancel(); | ||||
|         _cts.Dispose(); | ||||
|         _watchCts.Cancel(); | ||||
|         _watchCts.Dispose(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										22
									
								
								DysonNetwork.Gateway/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								DysonNetwork.Gateway/Startup/ServiceCollectionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| using DysonNetwork.Shared.Registry; | ||||
| using Microsoft.AspNetCore.Http.Timeouts; | ||||
| using Yarp.ReverseProxy.Configuration; | ||||
|  | ||||
| namespace DysonNetwork.Gateway.Startup; | ||||
|  | ||||
| public static class ServiceCollectionExtensions | ||||
| { | ||||
|     public static IServiceCollection AddGateway(this IServiceCollection services, IConfiguration configuration) | ||||
|     { | ||||
|         services | ||||
|             .AddReverseProxy() | ||||
|             .ConfigureHttpClient((context, handler) => | ||||
|             { | ||||
|             }); | ||||
|  | ||||
|         services.AddRegistryService(configuration); | ||||
|         services.AddSingleton<IProxyConfigProvider, RegistryProxyConfigProvider>(); | ||||
|  | ||||
|         return services; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										20
									
								
								DysonNetwork.Gateway/VersionController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								DysonNetwork.Gateway/VersionController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| namespace DysonNetwork.Gateway; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/version")] | ||||
| public class VersionController : ControllerBase | ||||
| { | ||||
|     [HttpGet] | ||||
|     public IActionResult Get() | ||||
|     { | ||||
|         return Ok(new AppVersion | ||||
|         { | ||||
|             Version = ThisAssembly.AssemblyVersion, | ||||
|             Commit = ThisAssembly.GitCommitId, | ||||
|             UpdateDate = ThisAssembly.GitCommitDate | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										45
									
								
								DysonNetwork.Gateway/appsettings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								DysonNetwork.Gateway/appsettings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| { | ||||
|   "LocalMode": true, | ||||
|   "Logging": { | ||||
|     "LogLevel": { | ||||
|       "Default": "Information", | ||||
|       "Microsoft.AspNetCore": "Warning" | ||||
|     } | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "Etcd": "etcd.orb.local:2379" | ||||
|   }, | ||||
|   "Etcd": { | ||||
|     "Insecure": true | ||||
|   }, | ||||
|   "Service": { | ||||
|     "Name": "DysonNetwork.Gateway", | ||||
|     "Url": "https://localhost:7034" | ||||
|   }, | ||||
|   "DomainMappings": { | ||||
|     "DysonNetwork.Pass": "id.solsynth.dev", | ||||
|     "DysonNetwork.Drive": "drive.solsynth.dev", | ||||
|     "DysonNetwork.Pusher": "push.solsynth.dev", | ||||
|     "DysonNetwork.Sphere": "sphere.solsynth.dev" | ||||
|   }, | ||||
|   "PathAliases": { | ||||
|     "DysonNetwork.Pass": "id", | ||||
|     "DysonNetwork.Drive": "drive" | ||||
|   }, | ||||
|   "DirectRoutes": [ | ||||
|     { | ||||
|       "Path": "/ws", | ||||
|       "Service": "DysonNetwork.Pusher", | ||||
|       "IsWebsocket": true | ||||
|     }, | ||||
|     { | ||||
|       "Path": "/.well-known/openid-configuration", | ||||
|       "Service": "DysonNetwork.Pass" | ||||
|     }, | ||||
|     { | ||||
|       "Path": "/.well-known/jwks", | ||||
|       "Service": "DysonNetwork.Pass" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										7
									
								
								DysonNetwork.Gateway/version.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								DysonNetwork.Gateway/version.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "version": "1.0", | ||||
|   "publicReleaseRefSpec": ["^refs/heads/main$"], | ||||
|   "cloudBuild": { | ||||
|     "setVersionVariables": true | ||||
|   } | ||||
| } | ||||
							
								
								
									
										2
									
								
								DysonNetwork.Pass/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								DysonNetwork.Pass/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| /wwwroot/dist | ||||
| /Keys | ||||
| @@ -1,7 +1,8 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public enum AbuseReportType | ||||
| { | ||||
| @@ -1,38 +1,88 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Sphere.Wallet; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
| using OtpNet; | ||||
| using VerificationMark = DysonNetwork.Shared.Data.VerificationMark; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| [Index(nameof(Name), IsUnique = true)] | ||||
| public class Account : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(256)] public string Name { get; set; } = string.Empty; | ||||
|     [MaxLength(256)] public string Nick { get; set; } = string.Empty; | ||||
|     [MaxLength(32)] public string Language { get; set; } = string.Empty; | ||||
|     public Instant? ActivatedAt { get; set; } | ||||
|     public bool IsSuperuser { get; set; } = false; | ||||
| 
 | ||||
|     public Profile Profile { get; set; } = null!; | ||||
|     public AccountProfile Profile { get; set; } = null!; | ||||
|     public ICollection<AccountContact> Contacts { get; set; } = new List<AccountContact>(); | ||||
|     public ICollection<Badge> Badges { get; set; } = new List<Badge>(); | ||||
|     public ICollection<AccountBadge> Badges { get; set; } = new List<AccountBadge>(); | ||||
| 
 | ||||
|     [JsonIgnore] public ICollection<AccountAuthFactor> AuthFactors { get; set; } = new List<AccountAuthFactor>(); | ||||
|     [JsonIgnore] public ICollection<AccountConnection> Connections { get; set; } = new List<AccountConnection>(); | ||||
|     [JsonIgnore] public ICollection<Auth.Session> Sessions { get; set; } = new List<Auth.Session>(); | ||||
|     [JsonIgnore] public ICollection<Auth.Challenge> Challenges { get; set; } = new List<Auth.Challenge>(); | ||||
|     [JsonIgnore] public ICollection<Auth.AuthSession> Sessions { get; set; } = new List<Auth.AuthSession>(); | ||||
|     [JsonIgnore] public ICollection<Auth.AuthChallenge> Challenges { get; set; } = new List<Auth.AuthChallenge>(); | ||||
| 
 | ||||
|     [JsonIgnore] public ICollection<Relationship> OutgoingRelationships { get; set; } = new List<Relationship>(); | ||||
|     [JsonIgnore] public ICollection<Relationship> IncomingRelationships { get; set; } = new List<Relationship>(); | ||||
| 
 | ||||
|     [JsonIgnore] public ICollection<Subscription> Subscriptions { get; set; } = new List<Subscription>(); | ||||
|     public Shared.Proto.Account ToProtoValue() | ||||
|     { | ||||
|         var proto = new Shared.Proto.Account | ||||
|         { | ||||
|             Id = Id.ToString(), | ||||
|             Name = Name, | ||||
|             Nick = Nick, | ||||
|             Language = Language, | ||||
|             ActivatedAt = ActivatedAt?.ToTimestamp(), | ||||
|             IsSuperuser = IsSuperuser, | ||||
|             Profile = Profile.ToProtoValue(), | ||||
|             CreatedAt = CreatedAt.ToTimestamp(), | ||||
|             UpdatedAt = UpdatedAt.ToTimestamp() | ||||
|         }; | ||||
| 
 | ||||
|         // Add contacts | ||||
|         foreach (var contact in Contacts) | ||||
|             proto.Contacts.Add(contact.ToProtoValue()); | ||||
| 
 | ||||
|         // Add badges | ||||
|         foreach (var badge in Badges) | ||||
|             proto.Badges.Add(badge.ToProtoValue()); | ||||
| 
 | ||||
|         return proto; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static Account FromProtoValue(Shared.Proto.Account proto) | ||||
|     { | ||||
|         var account = new Account | ||||
|         { | ||||
|             Id = Guid.Parse(proto.Id), | ||||
|             Name = proto.Name, | ||||
|             Nick = proto.Nick, | ||||
|             Language = proto.Language, | ||||
|             ActivatedAt = proto.ActivatedAt?.ToInstant(), | ||||
|             IsSuperuser = proto.IsSuperuser, | ||||
|             CreatedAt = proto.CreatedAt.ToInstant(), | ||||
|             UpdatedAt = proto.UpdatedAt.ToInstant(), | ||||
|         }; | ||||
| 
 | ||||
|         account.Profile = AccountProfile.FromProtoValue(proto.Profile); | ||||
| 
 | ||||
|         foreach (var contactProto in proto.Contacts) | ||||
|             account.Contacts.Add(AccountContact.FromProtoValue(contactProto)); | ||||
| 
 | ||||
|         foreach (var badgeProto in proto.Badges) | ||||
|             account.Badges.Add(AccountBadge.FromProtoValue(badgeProto)); | ||||
| 
 | ||||
|         return account; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public abstract class Leveling | ||||
| @@ -57,7 +107,7 @@ public abstract class Leveling | ||||
|     ]; | ||||
| } | ||||
| 
 | ||||
| public class Profile : ModelBase | ||||
| public class AccountProfile : ModelBase, IIdentifiedResource | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     [MaxLength(256)] public string? FirstName { get; set; } | ||||
| @@ -73,7 +123,6 @@ public class Profile : ModelBase | ||||
| 
 | ||||
|     [Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public BadgeReferenceObject? ActiveBadge { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public SubscriptionReferenceObject? StellarMembership { get; set; } | ||||
| 
 | ||||
|     public int Experience { get; set; } = 0; | ||||
|     [NotMapped] public int Level => Leveling.ExperiencePerLevel.Count(xp => Experience >= xp) - 1; | ||||
| @@ -84,15 +133,71 @@ public class Profile : ModelBase | ||||
|         : (Experience - Leveling.ExperiencePerLevel[Level]) * 100.0 / | ||||
|           (Leveling.ExperiencePerLevel[Level + 1] - Leveling.ExperiencePerLevel[Level]); | ||||
| 
 | ||||
|     // Outdated fields, for backward compability | ||||
|     [MaxLength(32)] public string? PictureId { get; set; } | ||||
|     [MaxLength(32)] public string? BackgroundId { get; set; } | ||||
| 
 | ||||
|     [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; } | ||||
| 
 | ||||
|     public Guid AccountId { get; set; } | ||||
|     [JsonIgnore] public Account Account { get; set; } = null!; | ||||
| 
 | ||||
|     public Shared.Proto.AccountProfile ToProtoValue() | ||||
|     { | ||||
|         var proto = new Shared.Proto.AccountProfile | ||||
|         { | ||||
|             Id = Id.ToString(), | ||||
|             FirstName = FirstName ?? string.Empty, | ||||
|             MiddleName = MiddleName ?? string.Empty, | ||||
|             LastName = LastName ?? string.Empty, | ||||
|             Bio = Bio ?? string.Empty, | ||||
|             Gender = Gender ?? string.Empty, | ||||
|             Pronouns = Pronouns ?? string.Empty, | ||||
|             TimeZone = TimeZone ?? string.Empty, | ||||
|             Location = Location ?? string.Empty, | ||||
|             Birthday = Birthday?.ToTimestamp(), | ||||
|             LastSeenAt = LastSeenAt?.ToTimestamp(), | ||||
|             Experience = Experience, | ||||
|             Level = Level, | ||||
|             LevelingProgress = LevelingProgress, | ||||
|             Picture = Picture?.ToProtoValue(), | ||||
|             Background = Background?.ToProtoValue(), | ||||
|             AccountId = AccountId.ToString(), | ||||
|             Verification = Verification?.ToProtoValue(), | ||||
|             ActiveBadge = ActiveBadge?.ToProtoValue(), | ||||
|             CreatedAt = CreatedAt.ToTimestamp(), | ||||
|             UpdatedAt = UpdatedAt.ToTimestamp() | ||||
|         }; | ||||
| 
 | ||||
|         return proto; | ||||
|     } | ||||
| 
 | ||||
|     public static AccountProfile FromProtoValue(Shared.Proto.AccountProfile proto) | ||||
|     { | ||||
|         var profile = new AccountProfile | ||||
|         { | ||||
|             Id = Guid.Parse(proto.Id), | ||||
|             FirstName = proto.FirstName, | ||||
|             LastName = proto.LastName, | ||||
|             MiddleName = proto.MiddleName, | ||||
|             Bio = proto.Bio, | ||||
|             Gender = proto.Gender, | ||||
|             Pronouns = proto.Pronouns, | ||||
|             TimeZone = proto.TimeZone, | ||||
|             Location = proto.Location, | ||||
|             Birthday = proto.Birthday?.ToInstant(), | ||||
|             LastSeenAt = proto.LastSeenAt?.ToInstant(), | ||||
|             Verification = proto.Verification is null ? null : VerificationMark.FromProtoValue(proto.Verification), | ||||
|             ActiveBadge = proto.ActiveBadge is null ? null : BadgeReferenceObject.FromProtoValue(proto.ActiveBadge), | ||||
|             Experience = proto.Experience, | ||||
|             Picture = proto.Picture is null ? null : CloudFileReferenceObject.FromProtoValue(proto.Picture), | ||||
|             Background = proto.Background is null ? null : CloudFileReferenceObject.FromProtoValue(proto.Background), | ||||
|             AccountId = Guid.Parse(proto.AccountId), | ||||
|             CreatedAt = proto.CreatedAt.ToInstant(), | ||||
|             UpdatedAt = proto.UpdatedAt.ToInstant() | ||||
|         }; | ||||
| 
 | ||||
|         return profile; | ||||
|     } | ||||
| 
 | ||||
|     public string ResourceIdentifier => $"account:profile:{Id}"; | ||||
| } | ||||
| 
 | ||||
| public class AccountContact : ModelBase | ||||
| @@ -105,6 +210,52 @@ public class AccountContact : ModelBase | ||||
| 
 | ||||
|     public Guid AccountId { get; set; } | ||||
|     [JsonIgnore] public Account Account { get; set; } = null!; | ||||
| 
 | ||||
|     public Shared.Proto.AccountContact ToProtoValue() | ||||
|     { | ||||
|         var proto = new Shared.Proto.AccountContact | ||||
|         { | ||||
|             Id = Id.ToString(), | ||||
|             Type = Type switch | ||||
|             { | ||||
|                 AccountContactType.Email => Shared.Proto.AccountContactType.Email, | ||||
|                 AccountContactType.PhoneNumber => Shared.Proto.AccountContactType.PhoneNumber, | ||||
|                 AccountContactType.Address => Shared.Proto.AccountContactType.Address, | ||||
|                 _ => Shared.Proto.AccountContactType.Unspecified | ||||
|             }, | ||||
|             Content = Content, | ||||
|             IsPrimary = IsPrimary, | ||||
|             VerifiedAt = VerifiedAt?.ToTimestamp(), | ||||
|             AccountId = AccountId.ToString(), | ||||
|             CreatedAt = CreatedAt.ToTimestamp(), | ||||
|             UpdatedAt = UpdatedAt.ToTimestamp() | ||||
|         }; | ||||
| 
 | ||||
|         return proto; | ||||
|     } | ||||
| 
 | ||||
|     public static AccountContact FromProtoValue(Shared.Proto.AccountContact proto) | ||||
|     { | ||||
|         var contact = new AccountContact | ||||
|         { | ||||
|             Id = Guid.Parse(proto.Id), | ||||
|             AccountId = Guid.Parse(proto.AccountId), | ||||
|             Type = proto.Type switch | ||||
|             { | ||||
|                 Shared.Proto.AccountContactType.Email => AccountContactType.Email, | ||||
|                 Shared.Proto.AccountContactType.PhoneNumber => AccountContactType.PhoneNumber, | ||||
|                 Shared.Proto.AccountContactType.Address => AccountContactType.Address, | ||||
|                 _ => AccountContactType.Email | ||||
|             }, | ||||
|             Content = proto.Content, | ||||
|             IsPrimary = proto.IsPrimary, | ||||
|             VerifiedAt = proto.VerifiedAt?.ToInstant(), | ||||
|             CreatedAt = proto.CreatedAt.ToInstant(), | ||||
|             UpdatedAt = proto.UpdatedAt.ToInstant() | ||||
|         }; | ||||
| 
 | ||||
|         return contact; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public enum AccountContactType | ||||
| @@ -1,14 +1,10 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Sphere.Auth; | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using DysonNetwork.Pass.Auth; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using NodaTime.Extensions; | ||||
| using System.Collections.Generic; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| [ApiController] | ||||
| [Route("/api/accounts")] | ||||
| @@ -33,9 +29,9 @@ public class AccountController( | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet("{name}/badges")] | ||||
|     [ProducesResponseType<List<Badge>>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<List<AccountBadge>>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult<List<Badge>>> GetBadgesByName(string name) | ||||
|     public async Task<ActionResult<List<AccountBadge>>> GetBadgesByName(string name) | ||||
|     { | ||||
|         var account = await db.Accounts | ||||
|             .Include(e => e.Badges) | ||||
| @@ -1,14 +1,16 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Sphere.Auth; | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Pass.Permission; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using Org.BouncyCastle.Utilities; | ||||
| using AuthService = DysonNetwork.Pass.Auth.AuthService; | ||||
| using AuthSession = DysonNetwork.Pass.Auth.AuthSession; | ||||
| using ChallengePlatform = DysonNetwork.Pass.Auth.ChallengePlatform; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| [Authorize] | ||||
| [ApiController] | ||||
| @@ -16,9 +18,10 @@ namespace DysonNetwork.Sphere.Account; | ||||
| public class AccountCurrentController( | ||||
|     AppDatabase db, | ||||
|     AccountService accounts, | ||||
|     FileReferenceService fileRefService, | ||||
|     AccountEventService events, | ||||
|     AuthService auth | ||||
|     AuthService auth, | ||||
|     FileService.FileServiceClient files, | ||||
|     FileReferenceService.FileReferenceServiceClient fileRefs | ||||
| ) : ControllerBase | ||||
| { | ||||
|     [HttpGet] | ||||
| @@ -75,7 +78,7 @@ public class AccountCurrentController( | ||||
|     } | ||||
| 
 | ||||
|     [HttpPatch("profile")] | ||||
|     public async Task<ActionResult<Profile>> UpdateProfile([FromBody] ProfileRequest request) | ||||
|     public async Task<ActionResult<AccountProfile>> UpdateProfile([FromBody] ProfileRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
| @@ -97,58 +100,37 @@ public class AccountCurrentController( | ||||
| 
 | ||||
|         if (request.PictureId is not null) | ||||
|         { | ||||
|             var picture = await db.Files.Where(f => f.Id == request.PictureId).FirstOrDefaultAsync(); | ||||
|             if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud."); | ||||
| 
 | ||||
|             var profileResourceId = $"profile:{profile.Id}"; | ||||
| 
 | ||||
|             // Remove old references for the profile picture | ||||
|             var file = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId }); | ||||
|             if (profile.Picture is not null) | ||||
|             { | ||||
|                 var oldPictureRefs = | ||||
|                     await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.picture"); | ||||
|                 foreach (var oldRef in oldPictureRefs) | ||||
|                 await fileRefs.DeleteResourceReferencesAsync( | ||||
|                     new DeleteResourceReferencesRequest { ResourceId = profile.ResourceIdentifier } | ||||
|                 ); | ||||
|             await fileRefs.CreateReferenceAsync( | ||||
|                 new CreateReferenceRequest | ||||
|                 { | ||||
|                     await fileRefService.DeleteReferenceAsync(oldRef.Id); | ||||
|                     ResourceId = profile.ResourceIdentifier, | ||||
|                     FileId = request.PictureId, | ||||
|                     Usage = "profile.picture" | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             profile.Picture = picture.ToReferenceObject(); | ||||
| 
 | ||||
|             // Create new reference | ||||
|             await fileRefService.CreateReferenceAsync( | ||||
|                 picture.Id, | ||||
|                 "profile.picture", | ||||
|                 profileResourceId | ||||
|             ); | ||||
|             profile.Picture = CloudFileReferenceObject.FromProtoValue(file); | ||||
|         } | ||||
| 
 | ||||
|         if (request.BackgroundId is not null) | ||||
|         { | ||||
|             var background = await db.Files.Where(f => f.Id == request.BackgroundId).FirstOrDefaultAsync(); | ||||
|             if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud."); | ||||
| 
 | ||||
|             var profileResourceId = $"profile:{profile.Id}"; | ||||
| 
 | ||||
|             // Remove old references for the profile background | ||||
|             var file = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId }); | ||||
|             if (profile.Background is not null) | ||||
|             { | ||||
|                 var oldBackgroundRefs = | ||||
|                     await fileRefService.GetResourceReferencesAsync(profileResourceId, "profile.background"); | ||||
|                 foreach (var oldRef in oldBackgroundRefs) | ||||
|                 await fileRefs.DeleteResourceReferencesAsync( | ||||
|                     new DeleteResourceReferencesRequest { ResourceId = profile.ResourceIdentifier } | ||||
|                 ); | ||||
|             await fileRefs.CreateReferenceAsync( | ||||
|                 new CreateReferenceRequest | ||||
|                 { | ||||
|                     await fileRefService.DeleteReferenceAsync(oldRef.Id); | ||||
|                     ResourceId = profile.ResourceIdentifier, | ||||
|                     FileId = request.BackgroundId, | ||||
|                     Usage = "profile.background" | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             profile.Background = background.ToReferenceObject(); | ||||
| 
 | ||||
|             // Create new reference | ||||
|             await fileRefService.CreateReferenceAsync( | ||||
|                 background.Id, | ||||
|                 "profile.background", | ||||
|                 profileResourceId | ||||
|             ); | ||||
|             profile.Background = CloudFileReferenceObject.FromProtoValue(file); | ||||
|         } | ||||
| 
 | ||||
|         db.Update(profile); | ||||
| @@ -438,7 +420,7 @@ public class AccountCurrentController( | ||||
|         public string UserAgent { get; set; } = null!; | ||||
|         public string DeviceId { get; set; } = null!; | ||||
|         public ChallengePlatform Platform { get; set; } | ||||
|         public List<Session> Sessions { get; set; } = []; | ||||
|         public List<AuthSession> Sessions { get; set; } = []; | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet("devices")] | ||||
| @@ -446,7 +428,7 @@ public class AccountCurrentController( | ||||
|     public async Task<ActionResult<List<AuthorizedDevice>>> GetDevices() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); | ||||
| 
 | ||||
|         Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString()); | ||||
| 
 | ||||
| @@ -475,13 +457,13 @@ public class AccountCurrentController( | ||||
| 
 | ||||
|     [HttpGet("sessions")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<Session>>> GetSessions( | ||||
|     public async Task<ActionResult<List<AuthSession>>> GetSessions( | ||||
|         [FromQuery] int take = 20, | ||||
|         [FromQuery] int offset = 0 | ||||
|     ) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); | ||||
| 
 | ||||
|         var query = db.AuthSessions | ||||
|             .Include(session => session.Account) | ||||
| @@ -503,7 +485,7 @@ public class AccountCurrentController( | ||||
| 
 | ||||
|     [HttpDelete("sessions/{id:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Session>> DeleteSession(Guid id) | ||||
|     public async Task<ActionResult<AuthSession>> DeleteSession(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
| @@ -520,10 +502,10 @@ public class AccountCurrentController( | ||||
| 
 | ||||
|     [HttpDelete("sessions/current")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Session>> DeleteCurrentSession() | ||||
|     public async Task<ActionResult<AuthSession>> DeleteCurrentSession() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
| @@ -537,7 +519,7 @@ public class AccountCurrentController( | ||||
|     } | ||||
| 
 | ||||
|     [HttpPatch("sessions/{id:guid}/label")] | ||||
|     public async Task<ActionResult<Session>> UpdateSessionLabel(Guid id, [FromBody] string label) | ||||
|     public async Task<ActionResult<AuthSession>> UpdateSessionLabel(Guid id, [FromBody] string label) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
| @@ -553,10 +535,10 @@ public class AccountCurrentController( | ||||
|     } | ||||
| 
 | ||||
|     [HttpPatch("sessions/current/label")] | ||||
|     public async Task<ActionResult<Session>> UpdateCurrentSessionLabel([FromBody] string label) | ||||
|     public async Task<ActionResult<AuthSession>> UpdateCurrentSessionLabel([FromBody] string label) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
| @@ -672,9 +654,9 @@ public class AccountCurrentController( | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet("badges")] | ||||
|     [ProducesResponseType<List<Badge>>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<List<AccountBadge>>(StatusCodes.Status200OK)] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<Badge>>> GetBadges() | ||||
|     public async Task<ActionResult<List<AccountBadge>>> GetBadges() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
| @@ -686,7 +668,7 @@ public class AccountCurrentController( | ||||
| 
 | ||||
|     [HttpPost("badges/{id:guid}/active")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Badge>> ActivateBadge(Guid id) | ||||
|     public async Task<ActionResult<AccountBadge>> ActivateBadge(Guid id) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
| 
 | ||||
| @@ -1,26 +1,31 @@ | ||||
| using System.Globalization; | ||||
| using DysonNetwork.Sphere.Activity; | ||||
| using DysonNetwork.Sphere.Connection; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Sphere.Wallet; | ||||
| using DysonNetwork.Pass.Wallet; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Caching.Distributed; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NodaTime; | ||||
| using Org.BouncyCastle.Asn1.X509; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public class AccountEventService( | ||||
|     AppDatabase db, | ||||
|     WebSocketService ws, | ||||
|     ICacheService cache, | ||||
|     PaymentService payment, | ||||
|     IStringLocalizer<Localization.AccountEventResource> localizer | ||||
|     ICacheService cache, | ||||
|     IStringLocalizer<Localization.AccountEventResource> localizer, | ||||
|     PusherService.PusherServiceClient pusher | ||||
| ) | ||||
| { | ||||
|     private static readonly Random Random = new(); | ||||
|     private const string StatusCacheKey = "AccountStatus_"; | ||||
|     private const string StatusCacheKey = "account:status:"; | ||||
| 
 | ||||
|     private async Task<bool> GetAccountIsConnected(Guid userId) | ||||
|     { | ||||
|         var resp = await pusher.GetWebsocketConnectionStatusAsync( | ||||
|             new GetWebsocketConnectionStatusRequest { UserId = userId.ToString() } | ||||
|         ); | ||||
|         return resp.IsConnected; | ||||
|     } | ||||
| 
 | ||||
|     public void PurgeStatusCache(Guid userId) | ||||
|     { | ||||
| @@ -34,7 +39,7 @@ public class AccountEventService( | ||||
|         var cachedStatus = await cache.GetAsync<Status>(cacheKey); | ||||
|         if (cachedStatus is not null) | ||||
|         { | ||||
|             cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId); | ||||
|             cachedStatus!.IsOnline = !cachedStatus.IsInvisible && await GetAccountIsConnected(userId); | ||||
|             return cachedStatus; | ||||
|         } | ||||
| 
 | ||||
| @@ -44,7 +49,7 @@ public class AccountEventService( | ||||
|             .Where(e => e.ClearedAt == null || e.ClearedAt > now) | ||||
|             .OrderByDescending(e => e.CreatedAt) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         var isOnline = ws.GetAccountIsConnected(userId); | ||||
|         var isOnline = await GetAccountIsConnected(userId); | ||||
|         if (status is not null) | ||||
|         { | ||||
|             status.IsOnline = !status.IsInvisible && isOnline; | ||||
| @@ -65,7 +70,7 @@ public class AccountEventService( | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|                 return new Status | ||||
|         return new Status | ||||
|         { | ||||
|             Attitude = StatusAttitude.Neutral, | ||||
|             IsOnline = false, | ||||
| @@ -86,7 +91,7 @@ public class AccountEventService( | ||||
|             var cachedStatus = await cache.GetAsync<Status>(cacheKey); | ||||
|             if (cachedStatus != null) | ||||
|             { | ||||
|                 cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId); | ||||
|                 cachedStatus.IsOnline = !cachedStatus.IsInvisible && await GetAccountIsConnected(userId); | ||||
|                 results[userId] = cachedStatus; | ||||
|             } | ||||
|             else | ||||
| @@ -95,7 +100,7 @@ public class AccountEventService( | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (cacheMissUserIds.Any()) | ||||
|         if (cacheMissUserIds.Count != 0) | ||||
|         { | ||||
|             var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|             var statusesFromDb = await db.AccountStatuses | ||||
| @@ -109,7 +114,7 @@ public class AccountEventService( | ||||
| 
 | ||||
|             foreach (var status in statusesFromDb) | ||||
|             { | ||||
|                 var isOnline = ws.GetAccountIsConnected(status.AccountId); | ||||
|                 var isOnline = await GetAccountIsConnected(status.AccountId); | ||||
|                 status.IsOnline = !status.IsInvisible && isOnline; | ||||
|                 results[status.AccountId] = status; | ||||
|                 var cacheKey = $"{StatusCacheKey}{status.AccountId}"; | ||||
| @@ -122,7 +127,7 @@ public class AccountEventService( | ||||
|             { | ||||
|                 foreach (var userId in usersWithoutStatus) | ||||
|                 { | ||||
|                     var isOnline = ws.GetAccountIsConnected(userId); | ||||
|                     var isOnline = await GetAccountIsConnected(userId); | ||||
|                     var defaultStatus = new Status | ||||
|                     { | ||||
|                         Attitude = StatusAttitude.Neutral, | ||||
| @@ -198,11 +203,11 @@ public class AccountEventService( | ||||
|     public async Task<CheckInResult> CheckInDaily(Account user) | ||||
|     { | ||||
|         var lockKey = $"{CheckInLockKey}{user.Id}"; | ||||
|          | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             var lk = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromMilliseconds(100)); | ||||
|              | ||||
| 
 | ||||
|             if (lk != null) | ||||
|                 await lk.ReleaseAsync(); | ||||
|         } | ||||
| @@ -210,9 +215,10 @@ public class AccountEventService( | ||||
|         { | ||||
|             // Ignore errors from this pre-check | ||||
|         } | ||||
|          | ||||
| 
 | ||||
|         // Now try to acquire the lock properly | ||||
|         await using var lockObj = await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5)); | ||||
|         await using var lockObj = | ||||
|             await cache.AcquireLockAsync(lockKey, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5)); | ||||
|         if (lockObj is null) throw new InvalidOperationException("Check-in was in progress."); | ||||
| 
 | ||||
|         var cultureInfo = new CultureInfo(user.Language, false); | ||||
| @@ -274,7 +280,7 @@ public class AccountEventService( | ||||
|                 s.SetProperty(b => b.Experience, b => b.Experience + result.RewardExperience) | ||||
|             ); | ||||
|         db.AccountCheckInResults.Add(result); | ||||
|         await db.SaveChangesAsync();  // Don't forget to save changes to the database | ||||
|         await db.SaveChangesAsync(); // Don't forget to save changes to the database | ||||
| 
 | ||||
|         // The lock will be automatically released by the await using statement | ||||
|         return result; | ||||
| @@ -1,26 +1,26 @@ | ||||
| using System.Globalization; | ||||
| using DysonNetwork.Sphere.Auth; | ||||
| using DysonNetwork.Sphere.Auth.OpenId; | ||||
| using DysonNetwork.Sphere.Email; | ||||
| 
 | ||||
| using DysonNetwork.Sphere.Localization; | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Pass.Auth; | ||||
| using DysonNetwork.Pass.Auth.OpenId; | ||||
| using DysonNetwork.Pass.Email; | ||||
| using DysonNetwork.Pass.Localization; | ||||
| using DysonNetwork.Pass.Permission; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using EFCore.BulkExtensions; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NodaTime; | ||||
| using Org.BouncyCastle.Utilities; | ||||
| using OtpNet; | ||||
| using AuthSession = DysonNetwork.Pass.Auth.AuthSession; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public class AccountService( | ||||
|     AppDatabase db, | ||||
|     MagicSpellService spells, | ||||
|     AccountUsernameService uname, | ||||
|     NotificationService nty, | ||||
|     EmailService mailer, | ||||
|     PusherService.PusherServiceClient pusher, | ||||
|     IStringLocalizer<NotificationResource> localizer, | ||||
|     ICacheService cache, | ||||
|     ILogger<AccountService> logger | ||||
| @@ -84,79 +84,69 @@ public class AccountService( | ||||
|         bool isActivated = false | ||||
|     ) | ||||
|     { | ||||
|         await using var transaction = await db.Database.BeginTransactionAsync(); | ||||
|         try | ||||
|         var dupeNameCount = await db.Accounts.Where(a => a.Name == name).CountAsync(); | ||||
|         if (dupeNameCount > 0) | ||||
|             throw new InvalidOperationException("Account name has already been taken."); | ||||
| 
 | ||||
|         var account = new Account | ||||
|         { | ||||
|             var dupeNameCount = await db.Accounts.Where(a => a.Name == name).CountAsync(); | ||||
|             if (dupeNameCount > 0) | ||||
|                 throw new InvalidOperationException("Account name has already been taken."); | ||||
| 
 | ||||
|             var account = new Account | ||||
|             Name = name, | ||||
|             Nick = nick, | ||||
|             Language = language, | ||||
|             Contacts = new List<AccountContact> | ||||
|             { | ||||
|                 Name = name, | ||||
|                 Nick = nick, | ||||
|                 Language = language, | ||||
|                 Contacts = new List<AccountContact> | ||||
|                 new() | ||||
|                 { | ||||
|                     new() | ||||
|                     { | ||||
|                         Type = AccountContactType.Email, | ||||
|                         Content = email, | ||||
|                         VerifiedAt = isEmailVerified ? SystemClock.Instance.GetCurrentInstant() : null, | ||||
|                         IsPrimary = true | ||||
|                     } | ||||
|                 }, | ||||
|                 AuthFactors = password is not null | ||||
|                     ? new List<AccountAuthFactor> | ||||
|                     { | ||||
|                         new AccountAuthFactor | ||||
|                         { | ||||
|                             Type = AccountAuthFactorType.Password, | ||||
|                             Secret = password, | ||||
|                             EnabledAt = SystemClock.Instance.GetCurrentInstant() | ||||
|                         }.HashSecret() | ||||
|                     } | ||||
|                     : [], | ||||
|                 Profile = new Profile() | ||||
|             }; | ||||
| 
 | ||||
|             if (isActivated) | ||||
|             { | ||||
|                 account.ActivatedAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|                 var defaultGroup = await db.PermissionGroups.FirstOrDefaultAsync(g => g.Key == "default"); | ||||
|                 if (defaultGroup is not null) | ||||
|                 { | ||||
|                     db.PermissionGroupMembers.Add(new PermissionGroupMember | ||||
|                     { | ||||
|                         Actor = $"user:{account.Id}", | ||||
|                         Group = defaultGroup | ||||
|                     }); | ||||
|                     Type = AccountContactType.Email, | ||||
|                     Content = email, | ||||
|                     VerifiedAt = isEmailVerified ? SystemClock.Instance.GetCurrentInstant() : null, | ||||
|                     IsPrimary = true | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var spell = await spells.CreateMagicSpell( | ||||
|                     account, | ||||
|                     MagicSpellType.AccountActivation, | ||||
|                     new Dictionary<string, object> | ||||
|             }, | ||||
|             AuthFactors = password is not null | ||||
|                 ? new List<AccountAuthFactor> | ||||
|                 { | ||||
|                     new AccountAuthFactor | ||||
|                     { | ||||
|                         { "contact_method", account.Contacts.First().Content } | ||||
|                     } | ||||
|                 ); | ||||
|                 await spells.NotifyMagicSpell(spell, true); | ||||
|             } | ||||
|                         Type = AccountAuthFactorType.Password, | ||||
|                         Secret = password, | ||||
|                         EnabledAt = SystemClock.Instance.GetCurrentInstant() | ||||
|                     }.HashSecret() | ||||
|                 } | ||||
|                 : [], | ||||
|             Profile = new AccountProfile() | ||||
|         }; | ||||
| 
 | ||||
|             db.Accounts.Add(account); | ||||
|             await db.SaveChangesAsync(); | ||||
| 
 | ||||
|             await transaction.CommitAsync(); | ||||
|             return account; | ||||
|         } | ||||
|         catch | ||||
|         if (isActivated) | ||||
|         { | ||||
|             await transaction.RollbackAsync(); | ||||
|             throw; | ||||
|             account.ActivatedAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|             var defaultGroup = await db.PermissionGroups.FirstOrDefaultAsync(g => g.Key == "default"); | ||||
|             if (defaultGroup is not null) | ||||
|             { | ||||
|                 db.PermissionGroupMembers.Add(new PermissionGroupMember | ||||
|                 { | ||||
|                     Actor = $"user:{account.Id}", | ||||
|                     Group = defaultGroup | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         db.Accounts.Add(account); | ||||
|         await db.SaveChangesAsync(); | ||||
| 
 | ||||
|         if (isActivated) return account; | ||||
|          | ||||
|         var spell = await spells.CreateMagicSpell( | ||||
|             account, | ||||
|             MagicSpellType.AccountActivation, | ||||
|             new Dictionary<string, object> | ||||
|             { | ||||
|                 { "contact_method", account.Contacts.First().Content } | ||||
|             } | ||||
|         ); | ||||
|         await spells.NotifyMagicSpell(spell, true); | ||||
| 
 | ||||
|         return account; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<Account> CreateAccount(OidcUserInfo userInfo) | ||||
| @@ -355,13 +345,18 @@ public class AccountService( | ||||
|                 if (await _GetFactorCode(factor) is not null) | ||||
|                     throw new InvalidOperationException("A factor code has been sent and in active duration."); | ||||
| 
 | ||||
|                 await nty.SendNotification( | ||||
|                     account, | ||||
|                     "auth.verification", | ||||
|                     localizer["AuthCodeTitle"], | ||||
|                     null, | ||||
|                     localizer["AuthCodeBody", code], | ||||
|                     save: true | ||||
|                 await pusher.SendPushNotificationToUserAsync( | ||||
|                     new SendPushNotificationToUserRequest | ||||
|                     { | ||||
|                         UserId = account.Id.ToString(), | ||||
|                         Notification = new PushNotification | ||||
|                         { | ||||
|                             Topic = "auth.verification", | ||||
|                             Title = localizer["AuthCodeTitle"], | ||||
|                             Body = localizer["AuthCodeBody", code], | ||||
|                             IsSavable = false | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
|                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(5)); | ||||
|                 break; | ||||
| @@ -397,16 +392,17 @@ public class AccountService( | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                                 await mailer.SendTemplatedEmailAsync<DysonNetwork.Sphere.Pages.Emails.VerificationEmail, VerificationEmailModel>( | ||||
|                     account.Nick, | ||||
|                     contact.Content, | ||||
|                     localizer["VerificationEmail"], | ||||
|                     new VerificationEmailModel | ||||
|                     { | ||||
|                         Name = account.Name, | ||||
|                         Code = code | ||||
|                     } | ||||
|                 ); | ||||
|                 await mailer | ||||
|                     .SendTemplatedEmailAsync<Pages.Emails.VerificationEmail, VerificationEmailModel>( | ||||
|                         account.Nick, | ||||
|                         contact.Content, | ||||
|                         localizer["VerificationEmail"], | ||||
|                         new VerificationEmailModel | ||||
|                         { | ||||
|                             Name = account.Name, | ||||
|                             Code = code | ||||
|                         } | ||||
|                     ); | ||||
| 
 | ||||
|                 await _SetFactorCode(factor, code, TimeSpan.FromMinutes(30)); | ||||
|                 break; | ||||
| @@ -454,7 +450,7 @@ public class AccountService( | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<Session> UpdateSessionLabel(Account account, Guid sessionId, string label) | ||||
|     public async Task<AuthSession> UpdateSessionLabel(Account account, Guid sessionId, string label) | ||||
|     { | ||||
|         var session = await db.AuthSessions | ||||
|             .Include(s => s.Challenge) | ||||
| @@ -491,7 +487,10 @@ public class AccountService( | ||||
|             .ToListAsync(); | ||||
| 
 | ||||
|         if (session.Challenge.DeviceId is not null) | ||||
|             await nty.UnsubscribePushNotifications(session.Challenge.DeviceId); | ||||
|             await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest() | ||||
|             { | ||||
|                 DeviceId = session.Challenge.DeviceId | ||||
|             }); | ||||
| 
 | ||||
|         // The current session should be included in the sessions' list | ||||
|         await db.AuthSessions | ||||
| @@ -574,7 +573,7 @@ public class AccountService( | ||||
|     /// This method will grant a badge to the account. | ||||
|     /// Shouldn't be exposed to normal user and the user itself. | ||||
|     /// </summary> | ||||
|     public async Task<Badge> GrantBadge(Account account, Badge badge) | ||||
|     public async Task<AccountBadge> GrantBadge(Account account, AccountBadge badge) | ||||
|     { | ||||
|         badge.AccountId = account.Id; | ||||
|         db.Badges.Add(badge); | ||||
| @@ -650,7 +649,8 @@ public class AccountService( | ||||
| 
 | ||||
|         if (missingId.Count != 0) | ||||
|         { | ||||
|             var newProfiles = missingId.Select(id => new Profile { Id = Guid.NewGuid(), AccountId = id }).ToList(); | ||||
|             var newProfiles = missingId.Select(id => new AccountProfile { Id = Guid.NewGuid(), AccountId = id }) | ||||
|                 .ToList(); | ||||
|             await db.BulkInsertAsync(newProfiles); | ||||
|         } | ||||
|     } | ||||
							
								
								
									
										153
									
								
								DysonNetwork.Pass/Account/AccountServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								DysonNetwork.Pass/Account/AccountServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Google.Protobuf.WellKnownTypes; | ||||
| using Grpc.Core; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class AccountServiceGrpc( | ||||
|     AppDatabase db, | ||||
|     RelationshipService relationships, | ||||
|     IClock clock, | ||||
|     ILogger<AccountServiceGrpc> logger | ||||
| ) | ||||
|     : Shared.Proto.AccountService.AccountServiceBase | ||||
| { | ||||
|     private readonly AppDatabase _db = db ?? throw new ArgumentNullException(nameof(db)); | ||||
|     private readonly IClock _clock = clock ?? throw new ArgumentNullException(nameof(clock)); | ||||
|  | ||||
|     private readonly ILogger<AccountServiceGrpc> | ||||
|         _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||
|  | ||||
|     public override async Task<Shared.Proto.Account> GetAccount(GetAccountRequest request, ServerCallContext context) | ||||
|     { | ||||
|         if (!Guid.TryParse(request.Id, out var accountId)) | ||||
|             throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); | ||||
|  | ||||
|         var account = await _db.Accounts | ||||
|             .AsNoTracking() | ||||
|             .Include(a => a.Profile) | ||||
|             .FirstOrDefaultAsync(a => a.Id == accountId); | ||||
|  | ||||
|         if (account == null) | ||||
|             throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, $"Account {request.Id} not found")); | ||||
|  | ||||
|         return account.ToProtoValue(); | ||||
|     } | ||||
|  | ||||
|     public override async Task<GetAccountBatchResponse> GetAccountBatch(GetAccountBatchRequest request, | ||||
|         ServerCallContext context) | ||||
|     { | ||||
|         var accountIds = request.Id | ||||
|             .Select(id => Guid.TryParse(id, out var accountId) ? accountId : (Guid?)null) | ||||
|             .Where(id => id.HasValue) | ||||
|             .Select(id => id!.Value) | ||||
|             .ToList(); | ||||
|  | ||||
|         var accounts = await _db.Accounts | ||||
|             .AsNoTracking() | ||||
|             .Where(a => accountIds.Contains(a.Id)) | ||||
|             .Include(a => a.Profile) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var response = new GetAccountBatchResponse(); | ||||
|         response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue())); | ||||
|         return response; | ||||
|     } | ||||
|  | ||||
|     public override async Task<ListAccountsResponse> ListAccounts(ListAccountsRequest request, | ||||
|         ServerCallContext context) | ||||
|     { | ||||
|         var query = _db.Accounts.AsNoTracking(); | ||||
|  | ||||
|         // Apply filters if provided | ||||
|         if (!string.IsNullOrEmpty(request.Filter)) | ||||
|         { | ||||
|             // Implement filtering logic based on request.Filter | ||||
|             // This is a simplified example | ||||
|             query = query.Where(a => a.Name.Contains(request.Filter) || a.Nick.Contains(request.Filter)); | ||||
|         } | ||||
|  | ||||
|         // Apply ordering | ||||
|         query = request.OrderBy switch | ||||
|         { | ||||
|             "name" => query.OrderBy(a => a.Name), | ||||
|             "name_desc" => query.OrderByDescending(a => a.Name), | ||||
|             _ => query.OrderBy(a => a.Id) | ||||
|         }; | ||||
|  | ||||
|         // Get total count for pagination | ||||
|         var totalCount = await query.CountAsync(); | ||||
|  | ||||
|         // Apply pagination | ||||
|         var accounts = await query | ||||
|             .Skip(request.PageSize * (request.PageToken != null ? int.Parse(request.PageToken) : 0)) | ||||
|             .Take(request.PageSize) | ||||
|             .Include(a => a.Profile) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         var response = new ListAccountsResponse | ||||
|         { | ||||
|             TotalSize = totalCount, | ||||
|             NextPageToken = (accounts.Count == request.PageSize) | ||||
|                 ? ((request.PageToken != null ? int.Parse(request.PageToken) : 0) + 1).ToString() | ||||
|                 : "" | ||||
|         }; | ||||
|  | ||||
|         response.Accounts.AddRange(accounts.Select(x => x.ToProtoValue())); | ||||
|         return response; | ||||
|     } | ||||
|  | ||||
|     public override async Task<ListRelationshipSimpleResponse> ListFriends( | ||||
|         ListRelationshipSimpleRequest request, ServerCallContext context) | ||||
|     { | ||||
|         var accountId = Guid.Parse(request.AccountId); | ||||
|         var relationship = await relationships.ListAccountFriends(accountId); | ||||
|         var resp = new ListRelationshipSimpleResponse(); | ||||
|         resp.AccountsId.AddRange(relationship.Select(x => x.ToString())); | ||||
|         return resp; | ||||
|     } | ||||
|  | ||||
|     public override async Task<ListRelationshipSimpleResponse> ListBlocked( | ||||
|         ListRelationshipSimpleRequest request, ServerCallContext context) | ||||
|     { | ||||
|         var accountId = Guid.Parse(request.AccountId); | ||||
|         var relationship = await relationships.ListAccountBlocked(accountId); | ||||
|         var resp = new ListRelationshipSimpleResponse(); | ||||
|         resp.AccountsId.AddRange(relationship.Select(x => x.ToString())); | ||||
|         return resp; | ||||
|     } | ||||
|  | ||||
|     public override async Task<GetRelationshipResponse> GetRelationship(GetRelationshipRequest request, | ||||
|         ServerCallContext context) | ||||
|     { | ||||
|         var relationship = await relationships.GetRelationship( | ||||
|             Guid.Parse(request.AccountId), | ||||
|             Guid.Parse(request.RelatedId), | ||||
|             status: (RelationshipStatus?)request.Status | ||||
|         ); | ||||
|         return new GetRelationshipResponse | ||||
|         { | ||||
|             Relationship = relationship?.ToProtoValue() | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public override async Task<BoolValue> HasRelationship(GetRelationshipRequest request, ServerCallContext context) | ||||
|     { | ||||
|         var hasRelationship = false; | ||||
|         if (!request.HasStatus) | ||||
|             hasRelationship = await relationships.HasExistingRelationship( | ||||
|                 Guid.Parse(request.AccountId), | ||||
|                 Guid.Parse(request.RelatedId) | ||||
|             ); | ||||
|         else | ||||
|             hasRelationship = await relationships.HasRelationshipWithStatus( | ||||
|                 Guid.Parse(request.AccountId), | ||||
|                 Guid.Parse(request.RelatedId), | ||||
|                 (RelationshipStatus)request.Status | ||||
|             ); | ||||
|         return new BoolValue { Value = hasRelationship }; | ||||
|     } | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| using System.Text.RegularExpressions; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Service for handling username generation and validation | ||||
							
								
								
									
										44
									
								
								DysonNetwork.Pass/Account/ActionLog.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								DysonNetwork.Pass/Account/ActionLog.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
| using Point = NetTopologySuite.Geometries.Point; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class ActionLog : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(4096)] public string Action { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new(); | ||||
|     [MaxLength(512)] public string? UserAgent { get; set; } | ||||
|     [MaxLength(128)] public string? IpAddress { get; set; } | ||||
|     public Point? Location { get; set; } | ||||
|  | ||||
|     public Guid AccountId { get; set; } | ||||
|     public Account Account { get; set; } = null!; | ||||
|     public Guid? SessionId { get; set; } | ||||
|  | ||||
|     public Shared.Proto.ActionLog ToProtoValue() | ||||
|     { | ||||
|         var protoLog = new Shared.Proto.ActionLog | ||||
|         { | ||||
|             Id = Id.ToString(), | ||||
|             Action = Action, | ||||
|             UserAgent = UserAgent ?? string.Empty, | ||||
|             IpAddress = IpAddress ?? string.Empty, | ||||
|             Location = Location?.ToString() ?? string.Empty, | ||||
|             AccountId = AccountId.ToString(), | ||||
|             CreatedAt = CreatedAt.ToTimestamp() | ||||
|         }; | ||||
|  | ||||
|         // Convert Meta dictionary to Struct | ||||
|         protoLog.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta)); | ||||
|  | ||||
|         if (SessionId.HasValue) | ||||
|             protoLog.SessionId = SessionId.Value.ToString(); | ||||
|  | ||||
|         return protoLog; | ||||
|     } | ||||
| } | ||||
| @@ -1,13 +1,11 @@ | ||||
| using Quartz; | ||||
| using DysonNetwork.Sphere.Connection; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Sphere.Storage.Handlers; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.GeoIp; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | ||||
| { | ||||
|     public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta) | ||||
|     public void CreateActionLog(Guid accountId, string action, Dictionary<string, object?> meta) | ||||
|     { | ||||
|         var log = new ActionLog | ||||
|         { | ||||
| @@ -38,7 +36,7 @@ public class ActionLogService(GeoIpService geo, FlushBufferService fbs) | ||||
|         else | ||||
|             throw new ArgumentException("No user context was found"); | ||||
|          | ||||
|         if (request.HttpContext.Items["CurrentSession"] is Auth.Session currentSession) | ||||
|         if (request.HttpContext.Items["CurrentSession"] is Auth.AuthSession currentSession) | ||||
|             log.SessionId = currentSession.Id; | ||||
| 
 | ||||
|         fbs.Enqueue(log); | ||||
							
								
								
									
										114
									
								
								DysonNetwork.Pass/Account/ActionLogServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								DysonNetwork.Pass/Account/ActionLogServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class ActionLogServiceGrpc : Shared.Proto.ActionLogService.ActionLogServiceBase | ||||
| { | ||||
|     private readonly ActionLogService _actionLogService; | ||||
|     private readonly AppDatabase _db; | ||||
|     private readonly ILogger<ActionLogServiceGrpc> _logger; | ||||
|  | ||||
|     public ActionLogServiceGrpc( | ||||
|         ActionLogService actionLogService, | ||||
|         AppDatabase db, | ||||
|         ILogger<ActionLogServiceGrpc> logger) | ||||
|     { | ||||
|         _actionLogService = actionLogService ?? throw new ArgumentNullException(nameof(actionLogService)); | ||||
|         _db = db ?? throw new ArgumentNullException(nameof(db)); | ||||
|         _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||
|     } | ||||
|  | ||||
|     public override async Task<CreateActionLogResponse> CreateActionLog(CreateActionLogRequest request, | ||||
|         ServerCallContext context) | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(request.AccountId) || !Guid.TryParse(request.AccountId, out var accountId)) | ||||
|         { | ||||
|             throw new RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.InvalidArgument, "Invalid account ID")); | ||||
|         } | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var meta = request.Meta | ||||
|                 ?.Select(x => new KeyValuePair<string, object?>(x.Key, GrpcTypeHelper.ConvertValueToObject(x.Value))) | ||||
|                 .ToDictionary() ?? new Dictionary<string, object?>(); | ||||
|  | ||||
|             _actionLogService.CreateActionLog( | ||||
|                 accountId, | ||||
|                 request.Action, | ||||
|                 meta | ||||
|             ); | ||||
|  | ||||
|             return new CreateActionLogResponse(); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             _logger.LogError(ex, "Error creating action log"); | ||||
|             throw new RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.Internal, "Failed to create action log")); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override async Task<ListActionLogsResponse> ListActionLogs(ListActionLogsRequest request, | ||||
|         ServerCallContext context) | ||||
|     { | ||||
|         if (string.IsNullOrEmpty(request.AccountId) || !Guid.TryParse(request.AccountId, out var accountId)) | ||||
|         { | ||||
|             throw new RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.InvalidArgument, "Invalid account ID")); | ||||
|         } | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var query = _db.ActionLogs | ||||
|                 .AsNoTracking() | ||||
|                 .Where(log => log.AccountId == accountId); | ||||
|  | ||||
|             if (!string.IsNullOrEmpty(request.Action)) | ||||
|             { | ||||
|                 query = query.Where(log => log.Action == request.Action); | ||||
|             } | ||||
|  | ||||
|             // Apply ordering (default to newest first) | ||||
|             query = (request.OrderBy?.ToLower() ?? "createdat desc") switch | ||||
|             { | ||||
|                 "createdat" => query.OrderBy(log => log.CreatedAt), | ||||
|                 "createdat desc" => query.OrderByDescending(log => log.CreatedAt), | ||||
|                 _ => query.OrderByDescending(log => log.CreatedAt) | ||||
|             }; | ||||
|  | ||||
|             // Apply pagination | ||||
|             var pageSize = request.PageSize == 0 ? 50 : Math.Min(request.PageSize, 1000); | ||||
|             var logs = await query | ||||
|                 .Take(pageSize + 1) // Fetch one extra to determine if there are more pages | ||||
|                 .ToListAsync(); | ||||
|  | ||||
|             var hasMore = logs.Count > pageSize; | ||||
|             if (hasMore) | ||||
|             { | ||||
|                 logs.RemoveAt(logs.Count - 1); | ||||
|             } | ||||
|  | ||||
|             var response = new ListActionLogsResponse | ||||
|             { | ||||
|                 TotalSize = await query.CountAsync() | ||||
|             }; | ||||
|  | ||||
|             if (hasMore) | ||||
|             { | ||||
|                 // In a real implementation, you'd generate a proper page token | ||||
|                 response.NextPageToken = (logs.LastOrDefault()?.CreatedAt ?? SystemClock.Instance.GetCurrentInstant()) | ||||
|                     .ToString(); | ||||
|             } | ||||
|  | ||||
|             response.ActionLogs.AddRange(logs.Select(log => log.ToProtoValue())); | ||||
|  | ||||
|             return response; | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             _logger.LogError(ex, "Error listing action logs"); | ||||
|             throw new RpcException(new Grpc.Core.Status(Grpc.Core.StatusCode.Internal, "Failed to list action logs")); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										123
									
								
								DysonNetwork.Pass/Account/Badge.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								DysonNetwork.Pass/Account/Badge.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Google.Protobuf.WellKnownTypes; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public class AccountBadge : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(1024)] public string Type { get; set; } = null!; | ||||
|     [MaxLength(1024)] public string? Label { get; set; } | ||||
|     [MaxLength(4096)] public string? Caption { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object?> Meta { get; set; } = new(); | ||||
|     public Instant? ActivatedAt { get; set; } | ||||
|     public Instant? ExpiredAt { get; set; } | ||||
|  | ||||
|     public Guid AccountId { get; set; } | ||||
|     [JsonIgnore] public Account Account { get; set; } = null!; | ||||
|  | ||||
|     public BadgeReferenceObject ToReference() | ||||
|     { | ||||
|         return new BadgeReferenceObject | ||||
|         { | ||||
|             Id = Id, | ||||
|             Type = Type, | ||||
|             Label = Label, | ||||
|             Caption = Caption, | ||||
|             Meta = Meta, | ||||
|             ActivatedAt = ActivatedAt, | ||||
|             ExpiredAt = ExpiredAt, | ||||
|             AccountId = AccountId, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public Shared.Proto.AccountBadge ToProtoValue() | ||||
|     { | ||||
|         var proto = new Shared.Proto.AccountBadge | ||||
|         { | ||||
|             Id = Id.ToString(), | ||||
|             Type = Type, | ||||
|             Label = Label ?? string.Empty, | ||||
|             Caption = Caption ?? string.Empty, | ||||
|             ActivatedAt = ActivatedAt?.ToTimestamp(), | ||||
|             ExpiredAt = ExpiredAt?.ToTimestamp(), | ||||
|             AccountId = AccountId.ToString(), | ||||
|             CreatedAt = CreatedAt.ToTimestamp(), | ||||
|             UpdatedAt = UpdatedAt.ToTimestamp() | ||||
|         }; | ||||
|         proto.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta)); | ||||
|  | ||||
|         return proto; | ||||
|     } | ||||
|      | ||||
|     public static AccountBadge FromProtoValue(Shared.Proto.AccountBadge proto) | ||||
|     { | ||||
|         var badge = new AccountBadge | ||||
|         { | ||||
|             Id = Guid.Parse(proto.Id), | ||||
|             AccountId = Guid.Parse(proto.AccountId), | ||||
|             Type = proto.Type, | ||||
|             Label = proto.Label, | ||||
|             Caption = proto.Caption, | ||||
|             ActivatedAt = proto.ActivatedAt?.ToInstant(), | ||||
|             ExpiredAt = proto.ExpiredAt?.ToInstant(), | ||||
|             CreatedAt = proto.CreatedAt.ToInstant(), | ||||
|             UpdatedAt = proto.UpdatedAt.ToInstant() | ||||
|         }; | ||||
|  | ||||
|         return badge; | ||||
|     } | ||||
| } | ||||
|  | ||||
| public class BadgeReferenceObject : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     public string Type { get; set; } = null!; | ||||
|     public string? Label { get; set; } | ||||
|     public string? Caption { get; set; } | ||||
|     public Dictionary<string, object?> Meta { get; set; } | ||||
|     public Instant? ActivatedAt { get; set; } | ||||
|     public Instant? ExpiredAt { get; set; } | ||||
|     public Guid AccountId { get; set; } | ||||
|  | ||||
|     public Shared.Proto.BadgeReferenceObject ToProtoValue() | ||||
|     { | ||||
|         var proto = new Shared.Proto.BadgeReferenceObject | ||||
|         { | ||||
|             Id = Id.ToString(), | ||||
|             Type = Type, | ||||
|             Label = Label ?? string.Empty, | ||||
|             Caption = Caption ?? string.Empty, | ||||
|             ActivatedAt = ActivatedAt?.ToTimestamp(), | ||||
|             ExpiredAt = ExpiredAt?.ToTimestamp(), | ||||
|             AccountId = AccountId.ToString() | ||||
|         }; | ||||
|         proto.Meta.Add(GrpcTypeHelper.ConvertToValueMap(Meta!)); | ||||
|  | ||||
|         return proto; | ||||
|     } | ||||
|      | ||||
|      | ||||
|     public static BadgeReferenceObject FromProtoValue(Shared.Proto.BadgeReferenceObject proto) | ||||
|     { | ||||
|         var badge = new BadgeReferenceObject | ||||
|         { | ||||
|             Id = Guid.Parse(proto.Id), | ||||
|             Type = proto.Type, | ||||
|             Label = proto.Label, | ||||
|             Caption = proto.Caption, | ||||
|             Meta = GrpcTypeHelper.ConvertFromValueMap(proto.Meta).ToDictionary(), | ||||
|             ActivatedAt = proto.ActivatedAt?.ToInstant(), | ||||
|             ExpiredAt = proto.ExpiredAt?.ToInstant(), | ||||
|             AccountId = Guid.Parse(proto.AccountId) | ||||
|         }; | ||||
|          | ||||
|         return badge; | ||||
|     } | ||||
| } | ||||
| @@ -1,8 +1,9 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public enum StatusAttitude | ||||
| { | ||||
| @@ -1,10 +1,11 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public enum MagicSpellType | ||||
| { | ||||
							
								
								
									
										64
									
								
								DysonNetwork.Pass/Account/MagicSpellController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								DysonNetwork.Pass/Account/MagicSpellController.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/api/spells")] | ||||
| public class MagicSpellController(AppDatabase db, MagicSpellService sp) : ControllerBase | ||||
| { | ||||
|     [HttpPost("{spellId:guid}/resend")] | ||||
|     public async Task<ActionResult> ResendMagicSpell(Guid spellId) | ||||
|     { | ||||
|         var spell = db.MagicSpells.FirstOrDefault(x => x.Id == spellId); | ||||
|         if (spell == null) | ||||
|             return NotFound(); | ||||
|      | ||||
|         await sp.NotifyMagicSpell(spell, true); | ||||
|         return Ok(); | ||||
|     } | ||||
|      | ||||
|     [HttpGet("{spellWord}")] | ||||
|     public async Task<ActionResult> GetMagicSpell(string spellWord) | ||||
|     { | ||||
|         var word = Uri.UnescapeDataString(spellWord); | ||||
|         var spell = await db.MagicSpells | ||||
|             .Where(x => x.Spell == word) | ||||
|             .Include(x => x.Account) | ||||
|             .ThenInclude(x => x.Profile) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (spell is null) | ||||
|             return NotFound(); | ||||
|         return Ok(spell); | ||||
|     } | ||||
|  | ||||
|     public record class MagicSpellApplyRequest | ||||
|     { | ||||
|         public string? NewPassword { get; set; } | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{spellWord}/apply")] | ||||
|     public async Task<ActionResult> ApplyMagicSpell([FromRoute] string spellWord, [FromBody] MagicSpellApplyRequest? request) | ||||
|     { | ||||
|         var word = Uri.UnescapeDataString(spellWord); | ||||
|         var spell = await db.MagicSpells | ||||
|             .Where(x => x.Spell == word) | ||||
|             .Include(x => x.Account) | ||||
|             .ThenInclude(x => x.Profile) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (spell is null) | ||||
|             return NotFound(); | ||||
|         try | ||||
|         { | ||||
|             if (spell.Type == MagicSpellType.AuthPasswordReset && request?.NewPassword is not null) | ||||
|                 await sp.ApplyPasswordReset(spell, request.NewPassword); | ||||
|             else | ||||
|                 await sp.ApplyMagicSpell(spell); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             return BadRequest(ex.Message); | ||||
|         } | ||||
|         return Ok(); | ||||
|     } | ||||
| } | ||||
| @@ -1,23 +1,21 @@ | ||||
| using System.Globalization; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Email; | ||||
| using DysonNetwork.Sphere.Pages.Emails; | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| using DysonNetwork.Sphere.Resources.Localization; | ||||
| using DysonNetwork.Sphere.Resources.Pages.Emails; | ||||
| using DysonNetwork.Pass.Email; | ||||
| using DysonNetwork.Pass.Pages.Emails; | ||||
| using DysonNetwork.Pass.Permission; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NodaTime; | ||||
| using EmailResource = DysonNetwork.Pass.Localization.EmailResource; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public class MagicSpellService( | ||||
|     AppDatabase db, | ||||
|     EmailService email, | ||||
|     IConfiguration configuration, | ||||
|     ILogger<MagicSpellService> logger, | ||||
|     IStringLocalizer<Localization.EmailResource> localizer | ||||
|     IStringLocalizer<EmailResource> localizer, | ||||
|     EmailService email | ||||
| ) | ||||
| { | ||||
|     public async Task<MagicSpell> CreateMagicSpell( | ||||
| @@ -136,6 +134,7 @@ public class MagicSpellService( | ||||
|                         } | ||||
|                     ); | ||||
|                     break; | ||||
|                 case MagicSpellType.AccountDeactivation: | ||||
|                 default: | ||||
|                     throw new ArgumentOutOfRangeException(); | ||||
|             } | ||||
| @@ -1,10 +1,11 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public class Notification : ModelBase | ||||
| { | ||||
							
								
								
									
										36
									
								
								DysonNetwork.Pass/Account/Relationship.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								DysonNetwork.Pass/Account/Relationship.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Account; | ||||
|  | ||||
| public enum RelationshipStatus : short | ||||
| { | ||||
|     Friends = 100, | ||||
|     Pending = 0, | ||||
|     Blocked = -100 | ||||
| } | ||||
|  | ||||
| public class Relationship : ModelBase | ||||
| { | ||||
|     public Guid AccountId { get; set; } | ||||
|     public Account Account { get; set; } = null!; | ||||
|     public Guid RelatedId { get; set; } | ||||
|     public Account Related { get; set; } = null!; | ||||
|  | ||||
|     public Instant? ExpiredAt { get; set; } | ||||
|  | ||||
|     public RelationshipStatus Status { get; set; } = RelationshipStatus.Pending; | ||||
|      | ||||
|     public Shared.Proto.Relationship ToProtoValue() => new() | ||||
|     { | ||||
|         AccountId = AccountId.ToString(), | ||||
|         RelatedId = RelatedId.ToString(), | ||||
|         Account = Account.ToProtoValue(), | ||||
|         Related = Related.ToProtoValue(), | ||||
|         Status = (int)Status, | ||||
|         CreatedAt = CreatedAt.ToTimestamp(), | ||||
|         UpdatedAt = UpdatedAt.ToTimestamp() | ||||
|     }; | ||||
| } | ||||
| @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| [ApiController] | ||||
| [Route("/api/relationships")] | ||||
| @@ -1,8 +1,8 @@ | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
| namespace DysonNetwork.Pass.Account; | ||||
| 
 | ||||
| public class RelationshipService(AppDatabase db, ICacheService cache) | ||||
| { | ||||
| @@ -154,13 +154,18 @@ public class RelationshipService(AppDatabase db, ICacheService cache) | ||||
| 
 | ||||
|     public async Task<List<Guid>> ListAccountFriends(Account account) | ||||
|     { | ||||
|         var cacheKey = $"{UserFriendsCacheKeyPrefix}{account.Id}"; | ||||
|         return await ListAccountFriends(account.Id); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<List<Guid>> ListAccountFriends(Guid accountId) | ||||
|     { | ||||
|         var cacheKey = $"{UserFriendsCacheKeyPrefix}{accountId}"; | ||||
|         var friends = await cache.GetAsync<List<Guid>>(cacheKey); | ||||
|          | ||||
|         if (friends == null) | ||||
|         { | ||||
|             friends = await db.AccountRelationships | ||||
|                 .Where(r => r.RelatedId == account.Id) | ||||
|                 .Where(r => r.RelatedId == accountId) | ||||
|                 .Where(r => r.Status == RelationshipStatus.Friends) | ||||
|                 .Select(r => r.AccountId) | ||||
|                 .ToListAsync(); | ||||
| @@ -173,13 +178,18 @@ public class RelationshipService(AppDatabase db, ICacheService cache) | ||||
|      | ||||
|     public async Task<List<Guid>> ListAccountBlocked(Account account) | ||||
|     { | ||||
|         var cacheKey = $"{UserBlockedCacheKeyPrefix}{account.Id}"; | ||||
|         return await ListAccountBlocked(account.Id); | ||||
|     } | ||||
|      | ||||
|     public async Task<List<Guid>> ListAccountBlocked(Guid accountId) | ||||
|     { | ||||
|         var cacheKey = $"{UserBlockedCacheKeyPrefix}{accountId}"; | ||||
|         var blocked = await cache.GetAsync<List<Guid>>(cacheKey); | ||||
|          | ||||
|         if (blocked == null) | ||||
|         { | ||||
|             blocked = await db.AccountRelationships | ||||
|                 .Where(r => r.RelatedId == account.Id) | ||||
|                 .Where(r => r.RelatedId == accountId) | ||||
|                 .Where(r => r.Status == RelationshipStatus.Blocked) | ||||
|                 .Select(r => r.AccountId) | ||||
|                 .ToListAsync(); | ||||
							
								
								
									
										276
									
								
								DysonNetwork.Pass/AppDatabase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										276
									
								
								DysonNetwork.Pass/AppDatabase.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,276 @@ | ||||
| using System.Linq.Expressions; | ||||
| using System.Reflection; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Pass.Auth; | ||||
| using DysonNetwork.Pass.Developer; | ||||
| using DysonNetwork.Pass.Permission; | ||||
| using DysonNetwork.Pass.Wallet; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| using Microsoft.EntityFrameworkCore.Query; | ||||
| using NodaTime; | ||||
| using Quartz; | ||||
|  | ||||
| namespace DysonNetwork.Pass; | ||||
|  | ||||
| public class AppDatabase( | ||||
|     DbContextOptions<AppDatabase> options, | ||||
|     IConfiguration configuration | ||||
| ) : DbContext(options) | ||||
| { | ||||
|     public DbSet<PermissionNode> PermissionNodes { get; set; } | ||||
|     public DbSet<PermissionGroup> PermissionGroups { get; set; } | ||||
|     public DbSet<PermissionGroupMember> PermissionGroupMembers { get; set; } | ||||
|  | ||||
|     public DbSet<MagicSpell> MagicSpells { get; set; } | ||||
|     public DbSet<Account.Account> Accounts { get; set; } | ||||
|     public DbSet<AccountConnection> AccountConnections { get; set; } | ||||
|     public DbSet<AccountProfile> AccountProfiles { get; set; } | ||||
|     public DbSet<AccountContact> AccountContacts { get; set; } | ||||
|     public DbSet<AccountAuthFactor> AccountAuthFactors { get; set; } | ||||
|     public DbSet<Relationship> AccountRelationships { get; set; } | ||||
|     public DbSet<Status> AccountStatuses { get; set; } | ||||
|     public DbSet<CheckInResult> AccountCheckInResults { get; set; } | ||||
|     public DbSet<Notification> Notifications { get; set; } | ||||
|     public DbSet<NotificationPushSubscription> NotificationPushSubscriptions { get; set; } | ||||
|     public DbSet<AccountBadge> Badges { get; set; } | ||||
|     public DbSet<ActionLog> ActionLogs { get; set; } | ||||
|     public DbSet<AbuseReport> AbuseReports { get; set; } | ||||
|  | ||||
|     public DbSet<AuthSession> AuthSessions { get; set; } | ||||
|     public DbSet<AuthChallenge> AuthChallenges { get; set; } | ||||
|      | ||||
|     public DbSet<Wallet.Wallet> Wallets { get; set; } | ||||
|     public DbSet<WalletPocket> WalletPockets { get; set; } | ||||
|     public DbSet<Order> PaymentOrders { get; set; } | ||||
|     public DbSet<Transaction> PaymentTransactions { get; set; } | ||||
|     public DbSet<Subscription> WalletSubscriptions { get; set; } | ||||
|     public DbSet<Coupon> WalletCoupons { get; set; } | ||||
|      | ||||
|     public DbSet<CustomApp> CustomApps { get; set; } | ||||
|     public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } | ||||
|  | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|     { | ||||
|         optionsBuilder.UseNpgsql( | ||||
|             configuration.GetConnectionString("App"), | ||||
|             opt => opt | ||||
|                 .ConfigureDataSource(optSource => optSource.EnableDynamicJson()) | ||||
|                 .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) | ||||
|                 .UseNetTopologySuite() | ||||
|                 .UseNodaTime() | ||||
|         ).UseSnakeCaseNamingConvention(); | ||||
|  | ||||
|         optionsBuilder.UseAsyncSeeding(async (context, _, cancellationToken) => | ||||
|         { | ||||
|             var defaultPermissionGroup = await context.Set<PermissionGroup>() | ||||
|                 .FirstOrDefaultAsync(g => g.Key == "default", cancellationToken); | ||||
|             if (defaultPermissionGroup is null) | ||||
|             { | ||||
|                 context.Set<PermissionGroup>().Add(new PermissionGroup | ||||
|                 { | ||||
|                     Key = "default", | ||||
|                     Nodes = new List<string> | ||||
|                         { | ||||
|                             "posts.create", | ||||
|                             "posts.react", | ||||
|                             "publishers.create", | ||||
|                             "files.create", | ||||
|                             "chat.create", | ||||
|                             "chat.messages.create", | ||||
|                             "chat.realtime.create", | ||||
|                             "accounts.statuses.create", | ||||
|                             "accounts.statuses.update", | ||||
|                             "stickers.packs.create", | ||||
|                             "stickers.create" | ||||
|                         }.Select(permission => | ||||
|                             PermissionService.NewPermissionNode("group:default", "global", permission, true)) | ||||
|                         .ToList() | ||||
|                 }); | ||||
|                 await context.SaveChangesAsync(cancellationToken); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         optionsBuilder.UseSeeding((context, _) => {}); | ||||
|  | ||||
|         base.OnConfiguring(optionsBuilder); | ||||
|     } | ||||
|  | ||||
|     protected override void OnModelCreating(ModelBuilder modelBuilder) | ||||
|     { | ||||
|         base.OnModelCreating(modelBuilder); | ||||
|  | ||||
|         modelBuilder.Entity<PermissionGroupMember>() | ||||
|             .HasKey(pg => new { pg.GroupId, pg.Actor }); | ||||
|         modelBuilder.Entity<PermissionGroupMember>() | ||||
|             .HasOne(pg => pg.Group) | ||||
|             .WithMany(g => g.Members) | ||||
|             .HasForeignKey(pg => pg.GroupId) | ||||
|             .OnDelete(DeleteBehavior.Cascade); | ||||
|  | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|             .HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId }); | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|             .HasOne(r => r.Account) | ||||
|             .WithMany(a => a.OutgoingRelationships) | ||||
|             .HasForeignKey(r => r.AccountId); | ||||
|         modelBuilder.Entity<Relationship>() | ||||
|             .HasOne(r => r.Related) | ||||
|             .WithMany(a => a.IncomingRelationships) | ||||
|             .HasForeignKey(r => r.RelatedId); | ||||
|  | ||||
|         // 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("Cleaning up expired records..."); | ||||
|  | ||||
|         // Expired relationships | ||||
|         var affectedRows = await db.AccountRelationships | ||||
|             .Where(x => x.ExpiredAt != null && x.ExpiredAt <= now) | ||||
|             .ExecuteDeleteAsync(); | ||||
|         logger.LogDebug("Removed {Count} records of expired relationships.", affectedRows); | ||||
|         // Expired permission group members | ||||
|         affectedRows = await db.PermissionGroupMembers | ||||
|             .Where(x => x.ExpiredAt != null && x.ExpiredAt <= now) | ||||
|             .ExecuteDeleteAsync(); | ||||
|         logger.LogDebug("Removed {Count} records of expired permission group members.", affectedRows); | ||||
|  | ||||
|         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,22 +1,16 @@ | ||||
| using System.IdentityModel.Tokens.Jwt; | ||||
| using System.Security.Claims; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text.Encodings.Web; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Options; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Sphere.Storage.Handlers; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Options; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using NodaTime; | ||||
| using System.Text; | ||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Controllers; | ||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Services; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Services; | ||||
| using DysonNetwork.Pass.Handlers; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using SystemClock = NodaTime.SystemClock; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth; | ||||
| namespace DysonNetwork.Pass.Auth; | ||||
| 
 | ||||
| public static class AuthConstants | ||||
| { | ||||
| @@ -71,7 +65,7 @@ public class DysonTokenAuthHandler( | ||||
|                 return AuthenticateResult.Fail("Invalid token."); | ||||
| 
 | ||||
|             // Try to get session from cache first | ||||
|             var session = await cache.GetAsync<Session>($"{AuthCachePrefix}{sessionId}"); | ||||
|             var session = await cache.GetAsync<AuthSession>($"{AuthCachePrefix}{sessionId}"); | ||||
| 
 | ||||
|             // If not in cache, load from database | ||||
|             if (session is null) | ||||
| @@ -1,14 +1,12 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using NodaTime; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using System.IdentityModel.Tokens.Jwt; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Connection; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using DysonNetwork.Shared.GeoIp; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth; | ||||
| namespace DysonNetwork.Pass.Auth; | ||||
| 
 | ||||
| [ApiController] | ||||
| [Route("/api/auth")] | ||||
| @@ -30,7 +28,7 @@ public class AuthController( | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("challenge")] | ||||
|     public async Task<ActionResult<Challenge>> StartChallenge([FromBody] ChallengeRequest request) | ||||
|     public async Task<ActionResult<AuthChallenge>> StartChallenge([FromBody] ChallengeRequest request) | ||||
|     { | ||||
|         var account = await accounts.LookupAccount(request.Account); | ||||
|         if (account is null) return NotFound("Account was not found."); | ||||
| @@ -50,7 +48,7 @@ public class AuthController( | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (existingChallenge is not null) return existingChallenge; | ||||
| 
 | ||||
|         var challenge = new Challenge | ||||
|         var challenge = new AuthChallenge | ||||
|         { | ||||
|             ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)), | ||||
|             StepTotal = await auth.DetectChallengeRisk(Request, account), | ||||
| @@ -75,7 +73,7 @@ public class AuthController( | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet("challenge/{id:guid}")] | ||||
|     public async Task<ActionResult<Challenge>> GetChallenge([FromRoute] Guid id) | ||||
|     public async Task<ActionResult<AuthChallenge>> GetChallenge([FromRoute] Guid id) | ||||
|     { | ||||
|         var challenge = await db.AuthChallenges | ||||
|             .Include(e => e.Account) | ||||
| @@ -135,7 +133,7 @@ public class AuthController( | ||||
|     } | ||||
| 
 | ||||
|     [HttpPatch("challenge/{id:guid}")] | ||||
|     public async Task<ActionResult<Challenge>> DoChallenge( | ||||
|     public async Task<ActionResult<AuthChallenge>> DoChallenge( | ||||
|         [FromRoute] Guid id, | ||||
|         [FromBody] PerformChallengeRequest request | ||||
|     ) | ||||
| @@ -239,7 +237,7 @@ public class AuthController( | ||||
|                 if (session is not null) | ||||
|                     return BadRequest("Session already exists for this challenge."); | ||||
| 
 | ||||
|                 session = new Session | ||||
|                 session = new AuthSession | ||||
|                 { | ||||
|                     LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow), | ||||
|                     ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)), | ||||
| @@ -1,11 +1,11 @@ | ||||
| using System.Security.Cryptography; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth; | ||||
| namespace DysonNetwork.Pass.Auth; | ||||
| 
 | ||||
| public class AuthService( | ||||
|     AppDatabase db, | ||||
| @@ -73,9 +73,9 @@ public class AuthService( | ||||
|         return totalRequiredSteps; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<Session> CreateSessionForOidcAsync(Account.Account account, Instant time, Guid? customAppId = null) | ||||
|     public async Task<AuthSession> CreateSessionForOidcAsync(Account.Account account, Instant time, Guid? customAppId = null) | ||||
|     { | ||||
|         var challenge = new Challenge | ||||
|         var challenge = new AuthChallenge | ||||
|         { | ||||
|             AccountId = account.Id, | ||||
|             IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||
| @@ -85,7 +85,7 @@ public class AuthService( | ||||
|             Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc | ||||
|         }; | ||||
| 
 | ||||
|         var session = new Session | ||||
|         var session = new AuthSession | ||||
|         { | ||||
|             AccountId = account.Id, | ||||
|             CreatedAt = time, | ||||
| @@ -154,7 +154,7 @@ public class AuthService( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public string CreateToken(Session session) | ||||
|     public string CreateToken(AuthSession session) | ||||
|     { | ||||
|         // Load the private key for signing | ||||
|         var privateKeyPem = File.ReadAllText(config["AuthToken:PrivateKeyPath"]!); | ||||
| @@ -183,7 +183,7 @@ public class AuthService( | ||||
|         return $"{payloadBase64}.{signatureBase64}"; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<bool> ValidateSudoMode(Session session, string? pinCode) | ||||
|     public async Task<bool> ValidateSudoMode(AuthSession session, string? pinCode) | ||||
|     { | ||||
|         // Check if the session is already in sudo mode (cached) | ||||
|         var sudoModeKey = $"accounts:{session.Id}:sudo"; | ||||
							
								
								
									
										62
									
								
								DysonNetwork.Pass/Auth/AuthServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								DysonNetwork.Pass/Auth/AuthServiceGrpc.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using DysonNetwork.Shared.Proto; | ||||
| using Grpc.Core; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Auth; | ||||
|  | ||||
| public class AuthServiceGrpc( | ||||
|     AuthService authService, | ||||
|     ICacheService cache, | ||||
|     AppDatabase db | ||||
| ) | ||||
|     : Shared.Proto.AuthService.AuthServiceBase | ||||
| { | ||||
|     public override async Task<AuthenticateResponse> Authenticate( | ||||
|         AuthenticateRequest request, | ||||
|         ServerCallContext context | ||||
|     ) | ||||
|     { | ||||
|         if (!authService.ValidateToken(request.Token, out var sessionId)) | ||||
|             return new AuthenticateResponse { Valid = false, Message = "Invalid token." }; | ||||
|  | ||||
|         var session = await cache.GetAsync<AuthSession>($"{DysonTokenAuthHandler.AuthCachePrefix}{sessionId}"); | ||||
|         if (session is not null) | ||||
|             return new AuthenticateResponse { Valid = true, Session = session.ToProtoValue() }; | ||||
|  | ||||
|         session = await db.AuthSessions | ||||
|             .AsNoTracking() | ||||
|             .Include(e => e.Challenge) | ||||
|             .Include(e => e.Account) | ||||
|             .ThenInclude(e => e.Profile) | ||||
|             .FirstOrDefaultAsync(s => s.Id == sessionId); | ||||
|         if (session == null) | ||||
|             return new AuthenticateResponse { Valid = false, Message = "Session was not found." }; | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         if (session.ExpiredAt.HasValue && session.ExpiredAt < now) | ||||
|             return new AuthenticateResponse { Valid = false, Message = "Session has been expired." }; | ||||
|  | ||||
|         await cache.SetWithGroupsAsync( | ||||
|             $"auth:{sessionId}", | ||||
|             session, | ||||
|             [$"{Account.AccountService.AccountCachePrefix}{session.Account.Id}"], | ||||
|             TimeSpan.FromHours(1) | ||||
|         ); | ||||
|  | ||||
|         return new AuthenticateResponse { Valid = true, Session = session.ToProtoValue() }; | ||||
|     } | ||||
|  | ||||
|     public override async Task<ValidateResponse> ValidatePin(ValidatePinRequest request, ServerCallContext context) | ||||
|     { | ||||
|         var accountId = Guid.Parse(request.AccountId); | ||||
|         var valid = await authService.ValidatePinCode(accountId, request.Pin); | ||||
|         return new ValidateResponse { Valid = valid }; | ||||
|     } | ||||
|      | ||||
|     public override async Task<ValidateResponse> ValidateCaptcha(ValidateCaptchaRequest request, ServerCallContext context) | ||||
|     { | ||||
|         var valid = await authService.ValidateCaptcha(request.Token); | ||||
|         return new ValidateResponse { Valid = valid }; | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| namespace DysonNetwork.Sphere.Auth; | ||||
| namespace DysonNetwork.Pass.Auth; | ||||
| 
 | ||||
| public class CaptchaVerificationResponse | ||||
| { | ||||
| @@ -1,13 +1,13 @@ | ||||
| using System.Security.Cryptography; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth; | ||||
| namespace DysonNetwork.Pass.Auth; | ||||
| 
 | ||||
| public class CompactTokenService(IConfiguration config) | ||||
| { | ||||
|     private readonly string _privateKeyPath = config["AuthToken:PrivateKeyPath"]  | ||||
|         ?? throw new InvalidOperationException("AuthToken:PrivateKeyPath configuration is missing"); | ||||
|      | ||||
|     public string CreateToken(Session session) | ||||
|     public string CreateToken(AuthSession session) | ||||
|     { | ||||
|         // Load the private key for signing | ||||
|         var privateKeyPem = File.ReadAllText(_privateKeyPath); | ||||
| @@ -1,18 +1,17 @@ | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Options; | ||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Responses; | ||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Services; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Services; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Options; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Options; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Controllers; | ||||
| namespace DysonNetwork.Pass.Auth.OidcProvider.Controllers; | ||||
| 
 | ||||
| [Route("/api/auth/open")] | ||||
| [ApiController] | ||||
| @@ -115,7 +114,7 @@ public class OidcProviderController( | ||||
|     public async Task<IActionResult> GetUserInfo() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser || | ||||
|             HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized(); | ||||
|             HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized(); | ||||
| 
 | ||||
|         // Get requested scopes from the token | ||||
|         var scopes = currentSession.Challenge.Scopes; | ||||
| @@ -2,7 +2,7 @@ using System; | ||||
| using System.Collections.Generic; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Models; | ||||
| namespace DysonNetwork.Pass.Auth.OidcProvider.Models; | ||||
| 
 | ||||
| public class AuthorizationCodeInfo | ||||
| { | ||||
| @@ -1,6 +1,6 @@ | ||||
| using System.Security.Cryptography; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Options; | ||||
| namespace DysonNetwork.Pass.Auth.OidcProvider.Options; | ||||
| 
 | ||||
| public class OidcProviderOptions | ||||
| { | ||||
| @@ -1,6 +1,6 @@ | ||||
| using System.Text.Json.Serialization; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; | ||||
| namespace DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||
| 
 | ||||
| public class AuthorizationResponse | ||||
| { | ||||
| @@ -1,6 +1,6 @@ | ||||
| using System.Text.Json.Serialization; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; | ||||
| namespace DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||
| 
 | ||||
| public class ErrorResponse | ||||
| { | ||||
| @@ -1,6 +1,6 @@ | ||||
| using System.Text.Json.Serialization; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; | ||||
| namespace DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||
| 
 | ||||
| public class TokenResponse | ||||
| { | ||||
| @@ -2,17 +2,17 @@ using System.IdentityModel.Tokens.Jwt; | ||||
| using System.Security.Claims; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Models; | ||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Options; | ||||
| using DysonNetwork.Sphere.Auth.OidcProvider.Responses; | ||||
| using DysonNetwork.Sphere.Developer; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Models; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Options; | ||||
| using DysonNetwork.Pass.Auth.OidcProvider.Responses; | ||||
| using DysonNetwork.Pass.Developer; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Options; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OidcProvider.Services; | ||||
| namespace DysonNetwork.Pass.Auth.OidcProvider.Services; | ||||
| 
 | ||||
| public class OidcProviderService( | ||||
|     AppDatabase db, | ||||
| @@ -38,7 +38,7 @@ public class OidcProviderService( | ||||
|             .FirstOrDefaultAsync(c => c.Id == appId); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<Session?> FindValidSessionAsync(Guid accountId, Guid clientId) | ||||
|     public async Task<AuthSession?> FindValidSessionAsync(Guid accountId, Guid clientId) | ||||
|     { | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
| 
 | ||||
| @@ -77,7 +77,7 @@ public class OidcProviderService( | ||||
|         if (client == null) | ||||
|             throw new InvalidOperationException("Client not found"); | ||||
| 
 | ||||
|         Session session; | ||||
|         AuthSession session; | ||||
|         var clock = SystemClock.Instance; | ||||
|         var now = clock.GetCurrentInstant(); | ||||
| 
 | ||||
| @@ -127,7 +127,7 @@ public class OidcProviderService( | ||||
| 
 | ||||
|     private string GenerateJwtToken( | ||||
|         CustomApp client, | ||||
|         Session session, | ||||
|         AuthSession session, | ||||
|         Instant expiresAt, | ||||
|         IEnumerable<string>? scopes = null | ||||
|     ) | ||||
| @@ -200,7 +200,7 @@ public class OidcProviderService( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public async Task<Session?> FindSessionByIdAsync(Guid sessionId) | ||||
|     public async Task<AuthSession?> FindSessionByIdAsync(Guid sessionId) | ||||
|     { | ||||
|         return await db.AuthSessions | ||||
|             .Include(s => s.Account) | ||||
| @@ -209,7 +209,7 @@ public class OidcProviderService( | ||||
|             .FirstOrDefaultAsync(s => s.Id == sessionId); | ||||
|     } | ||||
| 
 | ||||
|     private static string GenerateRefreshToken(Session session) | ||||
|     private static string GenerateRefreshToken(AuthSession session) | ||||
|     { | ||||
|         return Convert.ToBase64String(session.Id.ToByteArray()); | ||||
|     } | ||||
| @@ -222,7 +222,7 @@ public class OidcProviderService( | ||||
|     } | ||||
| 
 | ||||
|     public async Task<string> GenerateAuthorizationCodeForReuseSessionAsync( | ||||
|         Session session, | ||||
|         AuthSession session, | ||||
|         Guid clientId, | ||||
|         string redirectUri, | ||||
|         IEnumerable<string> scopes, | ||||
| @@ -1,8 +1,9 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| public class AfdianOidcService( | ||||
|     IConfiguration configuration, | ||||
| @@ -2,7 +2,7 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Text.Json.Serialization; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| public class AppleMobileConnectRequest | ||||
| { | ||||
| @@ -3,10 +3,11 @@ using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Implementation of OpenID Connect service for Apple Sign In | ||||
| @@ -1,11 +1,11 @@ | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| [ApiController] | ||||
| [Route("/api/accounts/me/connections")] | ||||
| @@ -63,7 +63,7 @@ public class ConnectionController( | ||||
|         return Ok(); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("/auth/connect/apple/mobile")] | ||||
|     [HttpPost("/api/auth/connect/apple/mobile")] | ||||
|     public async Task<ActionResult> ConnectAppleMobile([FromBody] AppleMobileConnectRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
| @@ -1,8 +1,9 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| public class DiscordOidcService( | ||||
|     IConfiguration configuration, | ||||
| @@ -1,8 +1,9 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| public class GitHubOidcService( | ||||
|     IConfiguration configuration, | ||||
| @@ -77,7 +78,7 @@ public class GitHubOidcService( | ||||
|         var client = HttpClientFactory.CreateClient(); | ||||
|         var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user"); | ||||
|         request.Headers.Add("Authorization", $"Bearer {accessToken}"); | ||||
|         request.Headers.Add("User-Agent", "DysonNetwork.Sphere"); | ||||
|         request.Headers.Add("User-Agent", "DysonNetwork.Pass"); | ||||
| 
 | ||||
|         var response = await client.SendAsync(request); | ||||
|         response.EnsureSuccessStatusCode(); | ||||
| @@ -109,7 +110,7 @@ public class GitHubOidcService( | ||||
|         var client = HttpClientFactory.CreateClient(); | ||||
|         var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user/emails"); | ||||
|         request.Headers.Add("Authorization", $"Bearer {accessToken}"); | ||||
|         request.Headers.Add("User-Agent", "DysonNetwork.Sphere"); | ||||
|         request.Headers.Add("User-Agent", "DysonNetwork.Pass"); | ||||
| 
 | ||||
|         var response = await client.SendAsync(request); | ||||
|         if (!response.IsSuccessStatusCode) return null; | ||||
| @@ -2,10 +2,11 @@ using System.IdentityModel.Tokens.Jwt; | ||||
| using System.Net.Http.Json; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| public class GoogleOidcService( | ||||
|     IConfiguration configuration, | ||||
| @@ -1,8 +1,7 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| public class MicrosoftOidcService( | ||||
|     IConfiguration configuration, | ||||
| @@ -1,11 +1,11 @@ | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| [ApiController] | ||||
| [Route("/api/auth/login")] | ||||
| @@ -68,8 +68,9 @@ public class OidcController( | ||||
|     /// Handles Apple authentication directly from mobile apps | ||||
|     /// </summary> | ||||
|     [HttpPost("apple/mobile")] | ||||
|     public async Task<ActionResult<Challenge>> AppleMobileLogin( | ||||
|         [FromBody] AppleMobileSignInRequest request) | ||||
|     public async Task<ActionResult<AuthChallenge>> AppleMobileLogin( | ||||
|         [FromBody] AppleMobileSignInRequest request | ||||
|     ) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
| @@ -1,13 +1,12 @@ | ||||
| using System.IdentityModel.Tokens.Jwt; | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Pass.Account; | ||||
| using DysonNetwork.Shared.Cache; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Base service for OpenID Connect authentication providers | ||||
| @@ -188,7 +187,7 @@ public abstract class OidcService( | ||||
|     /// Creates a challenge and session for an authenticated user | ||||
|     /// Also creates or updates the account connection | ||||
|     /// </summary> | ||||
|     public async Task<Challenge> CreateChallengeForUserAsync( | ||||
|     public async Task<AuthChallenge> CreateChallengeForUserAsync( | ||||
|         OidcUserInfo userInfo, | ||||
|         Account.Account account, | ||||
|         HttpContext request, | ||||
| @@ -218,7 +217,7 @@ public abstract class OidcService( | ||||
| 
 | ||||
|         // Create a challenge that's already completed | ||||
|         var now = SystemClock.Instance.GetCurrentInstant(); | ||||
|         var challenge = new Challenge | ||||
|         var challenge = new AuthChallenge | ||||
|         { | ||||
|             ExpiredAt = now.Plus(Duration.FromHours(1)), | ||||
|             StepTotal = await auth.DetectChallengeRisk(request.Request, account), | ||||
| @@ -1,7 +1,7 @@ | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Represents the state parameter used in OpenID Connect flows. | ||||
| @@ -1,4 +1,4 @@ | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| namespace DysonNetwork.Pass.Auth.OpenId; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Represents the user information from an OIDC provider | ||||
| @@ -1,13 +1,16 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Sphere.Developer; | ||||
| using DysonNetwork.Pass; | ||||
| using DysonNetwork.Pass.Developer; | ||||
| using DysonNetwork.Shared.Data; | ||||
| using NodaTime; | ||||
| using NodaTime.Serialization.Protobuf; | ||||
| using Point = NetTopologySuite.Geometries.Point; | ||||
| 
 | ||||
| namespace DysonNetwork.Sphere.Auth; | ||||
| namespace DysonNetwork.Pass.Auth; | ||||
| 
 | ||||
| public class Session : ModelBase | ||||
| public class AuthSession : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(1024)] public string? Label { get; set; } | ||||
| @@ -17,9 +20,22 @@ public class Session : ModelBase | ||||
|     public Guid AccountId { get; set; } | ||||
|     [JsonIgnore] public Account.Account Account { get; set; } = null!; | ||||
|     public Guid ChallengeId { get; set; } | ||||
|     public Challenge Challenge { get; set; } = null!; | ||||
|     public AuthChallenge Challenge { get; set; } = null!; | ||||
|     public Guid? AppId { get; set; } | ||||
|     public CustomApp? App { get; set; } | ||||
| 
 | ||||
|     public Shared.Proto.AuthSession ToProtoValue() => new() | ||||
|     { | ||||
|         Id = Id.ToString(), | ||||
|         Label = Label, | ||||
|         LastGrantedAt = LastGrantedAt?.ToTimestamp(), | ||||
|         ExpiredAt = ExpiredAt?.ToTimestamp(), | ||||
|         AccountId = AccountId.ToString(), | ||||
|         Account = Account.ToProtoValue(), | ||||
|         ChallengeId = ChallengeId.ToString(), | ||||
|         Challenge = Challenge.ToProtoValue(), | ||||
|         AppId = AppId?.ToString() | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| public enum ChallengeType | ||||
| @@ -40,7 +56,7 @@ public enum ChallengePlatform | ||||
|     Linux | ||||
| } | ||||
| 
 | ||||
| public class Challenge : ModelBase | ||||
| public class AuthChallenge : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     public Instant? ExpiredAt { get; set; } | ||||
| @@ -61,9 +77,28 @@ public class Challenge : ModelBase | ||||
|     public Guid AccountId { get; set; } | ||||
|     [JsonIgnore] public Account.Account Account { get; set; } = null!; | ||||
| 
 | ||||
|     public Challenge Normalize() | ||||
|     public AuthChallenge Normalize() | ||||
|     { | ||||
|         if (StepRemain == 0 && BlacklistFactors.Count == 0) StepRemain = StepTotal; | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     public Shared.Proto.AuthChallenge ToProtoValue() => new() | ||||
|     { | ||||
|         Id = Id.ToString(), | ||||
|         ExpiredAt = ExpiredAt?.ToTimestamp(), | ||||
|         StepRemain = StepRemain, | ||||
|         StepTotal = StepTotal, | ||||
|         FailedAttempts = FailedAttempts, | ||||
|         Platform = (Shared.Proto.ChallengePlatform)Platform, | ||||
|         Type = (Shared.Proto.ChallengeType)Type, | ||||
|         BlacklistFactors = { BlacklistFactors.Select(x => x.ToString()) }, | ||||
|         Audiences = { Audiences }, | ||||
|         Scopes = { Scopes }, | ||||
|         IpAddress = IpAddress, | ||||
|         UserAgent = UserAgent, | ||||
|         DeviceId = DeviceId, | ||||
|         Nonce = Nonce, | ||||
|         AccountId = AccountId.ToString() | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										9
									
								
								DysonNetwork.Pass/Client/.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								DysonNetwork.Pass/Client/.editorconfig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| [*.{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.Pass/Client/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								DysonNetwork.Pass/Client/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| * text=auto eol=lf | ||||
							
								
								
									
										30
									
								
								DysonNetwork.Pass/Client/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								DysonNetwork.Pass/Client/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
|  | ||||
| node_modules | ||||
| .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 | ||||
							
								
								
									
										6
									
								
								DysonNetwork.Pass/Client/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								DysonNetwork.Pass/Client/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|   "$schema": "https://json.schemastore.org/prettierrc", | ||||
|   "semi": false, | ||||
|   "singleQuote": true, | ||||
|   "printWidth": 100 | ||||
| } | ||||
							
								
								
									
										9
									
								
								DysonNetwork.Pass/Client/.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								DysonNetwork.Pass/Client/.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "recommendations": [ | ||||
|     "Vue.volar", | ||||
|     "dbaeumer.vscode-eslint", | ||||
|     "EditorConfig.EditorConfig", | ||||
|     "oxc.oxc-vscode", | ||||
|     "esbenp.prettier-vscode" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										33
									
								
								DysonNetwork.Pass/Client/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								DysonNetwork.Pass/Client/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| # Web | ||||
|  | ||||
| This template should help get you started developing with Vue 3 in Vite. | ||||
|  | ||||
| ## Recommended IDE Setup | ||||
|  | ||||
| [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). | ||||
|  | ||||
| ## Type Support for `.vue` Imports in TS | ||||
|  | ||||
| TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. | ||||
|  | ||||
| ## Customize configuration | ||||
|  | ||||
| See [Vite Configuration Reference](https://vite.dev/config/). | ||||
|  | ||||
| ## Project Setup | ||||
|  | ||||
| ```sh | ||||
| bun install | ||||
| ``` | ||||
|  | ||||
| ### Compile and Hot-Reload for Development | ||||
|  | ||||
| ```sh | ||||
| bun dev | ||||
| ``` | ||||
|  | ||||
| ### Type-Check, Compile and Minify for Production | ||||
|  | ||||
| ```sh | ||||
| bun run build | ||||
| ``` | ||||
							
								
								
									
										886
									
								
								DysonNetwork.Pass/Client/bun.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										886
									
								
								DysonNetwork.Pass/Client/bun.lock
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,886 @@ | ||||
| { | ||||
|   "lockfileVersion": 1, | ||||
|   "workspaces": { | ||||
|     "": { | ||||
|       "name": "@solar-network/pass", | ||||
|       "dependencies": { | ||||
|         "@fontsource-variable/nunito": "^5.2.6", | ||||
|         "@tailwindcss/vite": "^4.1.11", | ||||
|         "aspnet-prerendering": "^3.0.1", | ||||
|         "pinia": "^3.0.3", | ||||
|         "tailwindcss": "^4.1.11", | ||||
|         "vue": "^3.5.17", | ||||
|         "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=="], | ||||
|  | ||||
|     "@fontsource-variable/nunito": ["@fontsource-variable/nunito@5.2.6", "", {}, "sha512-dGYTQ0Hl94jjfMraYefrURHGH8fk/vL/1zYAZGofiPJVs6C0OkM8T87Te5Gwrbe6HG/XEMm5lib8AqasTN3ucw=="], | ||||
|  | ||||
|     "@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=="], | ||||
|  | ||||
|     "@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=="], | ||||
|  | ||||
|     "@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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], | ||||
|  | ||||
|     "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-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.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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-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=="], | ||||
|  | ||||
|     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], | ||||
|  | ||||
|     "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=="], | ||||
|  | ||||
|     "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.Pass/Client/env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								DysonNetwork.Pass/Client/env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /// <reference types="vite/client" /> | ||||
							
								
								
									
										31
									
								
								DysonNetwork.Pass/Client/eslint.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								DysonNetwork.Pass/Client/eslint.config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| 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, | ||||
| ) | ||||
							
								
								
									
										14
									
								
								DysonNetwork.Pass/Client/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								DysonNetwork.Pass/Client/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| <!doctype html> | ||||
| <html lang=""> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" href="/favicon.ico" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Solarpass</title> | ||||
|     <app-data /> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"></div> | ||||
|     <script type="module" src="/src/main.ts"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										4523
									
								
								DysonNetwork.Pass/Client/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4523
									
								
								DysonNetwork.Pass/Client/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										48
									
								
								DysonNetwork.Pass/Client/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								DysonNetwork.Pass/Client/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| { | ||||
|   "name": "@solar-network/pass", | ||||
|   "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": { | ||||
|     "@fontsource-variable/nunito": "^5.2.6", | ||||
|     "@tailwindcss/vite": "^4.1.11", | ||||
|     "aspnet-prerendering": "^3.0.1", | ||||
|     "pinia": "^3.0.3", | ||||
|     "tailwindcss": "^4.1.11", | ||||
|     "vue": "^3.5.17", | ||||
|     "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" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										9
									
								
								DysonNetwork.Pass/Client/src/assets/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								DysonNetwork.Pass/Client/src/assets/main.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| @import "tailwindcss"; | ||||
|  | ||||
| @layer theme, base, components, utilities; | ||||
|  | ||||
| @layer base { | ||||
|   body { | ||||
|     font-family: 'Nunito Variable', sans-serif; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										25
									
								
								DysonNetwork.Pass/Client/src/layouts/default.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								DysonNetwork.Pass/Client/src/layouts/default.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| <template> | ||||
|   <n-layout> | ||||
|     <n-layout-header class="border-b-1">Solar Network ID</n-layout-header> | ||||
|     <n-layout-content embedded content-style="padding: 24px;"> | ||||
|       <slot /> | ||||
|     </n-layout-content> | ||||
|   </n-layout> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { NLayout, NLayoutHeader, NLayoutContent } from 'naive-ui' | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .n-layout-header, | ||||
| .n-layout-footer { | ||||
|   padding: 8px 24px; | ||||
|   border-color: var(--n-border-color); | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .n-layout-content { | ||||
|   height: calc(100vh - 40px); | ||||
| } | ||||
| </style> | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user