diff --git a/Gateway/Gateway.Api/Gateway.Api.csproj b/Gateway/Gateway.Api/Gateway.Api.csproj index 562aa36..14af26c 100644 --- a/Gateway/Gateway.Api/Gateway.Api.csproj +++ b/Gateway/Gateway.Api/Gateway.Api.csproj @@ -7,11 +7,21 @@ bc1b7e89-5cda-4112-8b7c-ff0162e59ce3 Linux ..\.. + true - + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Gateway/Gateway.Api/Program.cs b/Gateway/Gateway.Api/Program.cs index 61abfb6..7a4767c 100644 --- a/Gateway/Gateway.Api/Program.cs +++ b/Gateway/Gateway.Api/Program.cs @@ -1,12 +1,62 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); builder.Services.AddSwaggerGen(); +byte[] key = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key is missing.")); + +builder.Services.AddAuthentication(cfg => +{ + cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}).AddJwtBearer(options => +{ + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(key), + }; +}); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("Bearer", policy => + { + policy.RequireAuthenticatedUser(); + policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); + }); +}); + builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); +builder.Services.AddCors( + options => + { + options.AddPolicy( + "CorsPolicy", + policyBuilder => policyBuilder + .AllowAnyMethod() + .AllowCredentials() + .SetIsOriginAllowed((_) => true) + .AllowAnyHeader()); + }); var app = builder.Build(); +app.UseCors("CorsPolicy"); + if (app.Environment.IsDevelopment()) { app.MapOpenApi(); @@ -16,6 +66,8 @@ app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); app.MapReverseProxy(); app.Run(); diff --git a/Gateway/Gateway.Api/appsettings.json b/Gateway/Gateway.Api/appsettings.json index d54d8ac..8e7e0f6 100644 --- a/Gateway/Gateway.Api/appsettings.json +++ b/Gateway/Gateway.Api/appsettings.json @@ -6,16 +6,31 @@ } }, "AllowedHosts": "*", + "Jwt": { + "Key": "YourSuperLongSecretKeyThatIsAtLeast32Characters!", + "Issuer": "AuthService", + "Audience": "Gateway" + }, "ReverseProxy": { "Routes": { "CoreService": { "ClusterId": "coreServiceCluster", + "AuthorizationPolicy": "Bearer", "Match": { "Path": "/core-api/{**catch-all}" }, "Transforms": [ { "PathPattern": "/api/{**catch-all}" } ] + }, + "AuthService": { + "ClusterId": "authServiceCluster", + "Match": { "Path": "/auth-api/{**catch-all}" }, + "Transforms": [ + { + "PathPattern": "/{**catch-all}" + } + ] } }, "Clusters": { @@ -23,6 +38,11 @@ "Destinations": { "CoreService": { "Address": "http://core.api:8080/" } } + }, + "authServiceCluster": { + "Destinations": { + "AuthService": { "Address": "http://auth.api:8080/" } + } } } } diff --git a/Helpers/GatewayAuthHandler/GatewayAuthHandler.cs b/Helpers/GatewayAuthHandler/GatewayAuthHandler.cs new file mode 100644 index 0000000..b7ffb96 --- /dev/null +++ b/Helpers/GatewayAuthHandler/GatewayAuthHandler.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace GatewayAuthHandler +{ + using System.IdentityModel.Tokens.Jwt; + using System.Security.Claims; + using System.Text.Encodings.Web; + using Microsoft.AspNetCore.Authentication; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + + /// + /// Gateway authentication handler. + /// + public class GatewayAuthHandler : AuthenticationHandler + { + /// + /// Initializes a new instance of the class. + /// + /// Authenitcation scheme options. + /// Logger. + /// Encoder. + /// System clock. + public GatewayAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock systemClock) + : base(options, logger, encoder, systemClock) + { + } + + /// + /// Handle authenticvation method. + /// + /// Authentication result. + protected override Task HandleAuthenticateAsync() + { + if (!this.Request.Headers.TryGetValue("Authorization", out var authHeader)) + { + return Task.FromResult(AuthenticateResult.Fail("No Authorization Header")); + } + + var token = authHeader.ToString().Replace("Bearer ", string.Empty, StringComparison.OrdinalIgnoreCase); + var handler = new JwtSecurityTokenHandler(); + var jwtToken = handler.ReadJwtToken(token); + + var claims = jwtToken.Claims.ToList(); + var identity = new ClaimsIdentity(claims, "GatewayAuth"); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, "GatewayAuth"); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } + } +} diff --git a/Helpers/GatewayAuthHandler/GatewayAuthHandler.csproj b/Helpers/GatewayAuthHandler/GatewayAuthHandler.csproj new file mode 100644 index 0000000..c5e013c --- /dev/null +++ b/Helpers/GatewayAuthHandler/GatewayAuthHandler.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/PracticesService.sln b/PracticesService.sln index ca5b7b5..83c7bc4 100644 --- a/PracticesService.sln +++ b/PracticesService.sln @@ -13,6 +13,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gateway", "Gateway", "{222D EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gateway.Api", "Gateway\Gateway.Api\Gateway.Api.csproj", "{C5116E8B-8D09-406D-80CD-0039B4AC5B68}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthService.Api", "Services\AuthService.Api\AuthService.Api.csproj", "{924ED870-509D-4512-A12A-1DC2E81A6E41}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Helpers", "Helpers", "{8CD65409-E7E2-4FC3-8AA5-3BFA5D08779B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GatewayAuthHandler", "Helpers\GatewayAuthHandler\GatewayAuthHandler.csproj", "{48704C05-2136-429D-A249-628CDEF16B0D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{1665257F-51D8-4832-A7ED-94603EA35B23}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared\Shared.csproj", "{712BF23F-5D58-4EB5-94F5-8525ED05965D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{39D921BF-8448-4A15-A870-778AEFA3EF2B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests\Tests.csproj", "{76C651ED-813E-4E68-8C0D-C6D7C354B29C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +45,22 @@ Global {C5116E8B-8D09-406D-80CD-0039B4AC5B68}.Debug|Any CPU.Build.0 = Debug|Any CPU {C5116E8B-8D09-406D-80CD-0039B4AC5B68}.Release|Any CPU.ActiveCfg = Release|Any CPU {C5116E8B-8D09-406D-80CD-0039B4AC5B68}.Release|Any CPU.Build.0 = Release|Any CPU + {924ED870-509D-4512-A12A-1DC2E81A6E41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {924ED870-509D-4512-A12A-1DC2E81A6E41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {924ED870-509D-4512-A12A-1DC2E81A6E41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {924ED870-509D-4512-A12A-1DC2E81A6E41}.Release|Any CPU.Build.0 = Release|Any CPU + {48704C05-2136-429D-A249-628CDEF16B0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48704C05-2136-429D-A249-628CDEF16B0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48704C05-2136-429D-A249-628CDEF16B0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48704C05-2136-429D-A249-628CDEF16B0D}.Release|Any CPU.Build.0 = Release|Any CPU + {712BF23F-5D58-4EB5-94F5-8525ED05965D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {712BF23F-5D58-4EB5-94F5-8525ED05965D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {712BF23F-5D58-4EB5-94F5-8525ED05965D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {712BF23F-5D58-4EB5-94F5-8525ED05965D}.Release|Any CPU.Build.0 = Release|Any CPU + {76C651ED-813E-4E68-8C0D-C6D7C354B29C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76C651ED-813E-4E68-8C0D-C6D7C354B29C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76C651ED-813E-4E68-8C0D-C6D7C354B29C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76C651ED-813E-4E68-8C0D-C6D7C354B29C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -38,6 +68,10 @@ Global GlobalSection(NestedProjects) = preSolution {94A409B2-9D11-41F4-9BB8-6B89AC26764E} = {89CC78B4-9A7B-46F4-B786-7FB0D49911B2} {C5116E8B-8D09-406D-80CD-0039B4AC5B68} = {222D815F-4394-417D-AF31-DA86AE6E21C1} + {924ED870-509D-4512-A12A-1DC2E81A6E41} = {89CC78B4-9A7B-46F4-B786-7FB0D49911B2} + {48704C05-2136-429D-A249-628CDEF16B0D} = {8CD65409-E7E2-4FC3-8AA5-3BFA5D08779B} + {712BF23F-5D58-4EB5-94F5-8525ED05965D} = {1665257F-51D8-4832-A7ED-94603EA35B23} + {76C651ED-813E-4E68-8C0D-C6D7C354B29C} = {39D921BF-8448-4A15-A870-778AEFA3EF2B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC6C6DEA-3EB6-439A-AE03-43698361909A} diff --git a/README.md b/README.md index d77eb71..0b6a5b5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,104 @@ # PracticesService -Сервис для работы с учебными/производственными практиками кафедры + +## Описание + +**PracticesService** — Сервис для работы с учебными/производственными практиками кафедры. Запуск производится через Docker Compose. + +## Контейнеры + +- **RabbitMQ** — брокер сообщений +- **gateway.api** — шлюзовый API +- **core.api / core.db** — основной сервис и его база данных +- **auth.api / auth.db** — сервис авторизации и его база данных +- **frontend** — клиентская часть + +## Предварительные требования + +- [Docker](https://www.docker.com/) +- [Docker Compose](https://docs.docker.com/compose/) + +## Запуск проекта + +1. Клонируйте репозиторий: + + ```bash + git clone + cd <название директории> + ``` + +2. Запустите все сервисы: + + ```bash + docker-compose up --build + ``` + + Все сервисы поднимутся автоматически, включая RabbitMQ, базы данных и API. + +3. Убедитесь, что RabbitMQ доступен по адресу: + ``` + http://localhost:15672 + ``` + Логин: `admin`, пароль: `admin123` + +## Применение миграций для AuthService + +После запуска контейнеров, необходимо применить миграции к базе данных авторизации. + +1. Войдите в контейнер `auth.api`: + + ```bash + docker exec -it /bin/sh + ``` + + Например: + + ```bash + docker exec -it auth.api /bin/sh + ``` + +2. Примените миграции (предположим, используется Entity Framework CLI): + + ```bash + dotnet ef database update --project Services/AuthService.Api + ``` + + > Убедитесь, что внутри контейнера установлен `dotnet-ef`. Если нет — установите или примените миграции локально и пересоберите образ. + +## Доступ к сервисам + +| Сервис | URL | +|--------------|----------------------------| +| Gateway API | http://localhost:5000 | +| Core API | http://localhost:5001 | +| Auth API | http://localhost:5002 | +| Frontend | http://localhost:8000 | +| RabbitMQ UI | http://localhost:15672 | + +## Сеть + +Все сервисы находятся в общей Docker-сети `proxybackend`. + +--- + +## Полезные команды + +- Остановка всех сервисов: + + ```bash + docker-compose down + ``` + +- Просмотр логов: + + ```bash + docker-compose logs -f + ``` + +- Проверка состояния контейнеров: + + ```bash + docker ps + ``` + + + diff --git a/Services/AuthService.Api/AuthService.Api.csproj b/Services/AuthService.Api/AuthService.Api.csproj new file mode 100644 index 0000000..92899e1 --- /dev/null +++ b/Services/AuthService.Api/AuthService.Api.csproj @@ -0,0 +1,47 @@ + + + + net9.0 + enable + enable + bc7ad2eb-e1dc-45a0-8580-18b7e93dedfb + Linux + ..\.. + true + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Services/AuthService.Api/Consumers/UserWithRoleActionConsumer.cs b/Services/AuthService.Api/Consumers/UserWithRoleActionConsumer.cs new file mode 100644 index 0000000..0415f4e --- /dev/null +++ b/Services/AuthService.Api/Consumers/UserWithRoleActionConsumer.cs @@ -0,0 +1,147 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace AuthService.Api.Consumers; + +using Contracts; +using MassTransit; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +/// +/// Consumer for user with roles events. +/// +public class UserWithRoleActionConsumer : IConsumer +{ + private readonly AuthDbContext context; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// Auth DB context. + /// Logger. + public UserWithRoleActionConsumer( + AuthDbContext context, + ILogger logger) + { + this.context = context; + this.logger = logger; + } + + /// + /// Consumes event. + /// + /// Consume event context. + /// A representing the asynchronous operation. + public async Task Consume(ConsumeContext consumeContext) + { + try + { + switch (consumeContext.Message.Action) + { + case UserActionType.Delete: + await this.HandleRoleRemoval(consumeContext.Message.UserId, consumeContext.Message.Role); + break; + + case UserActionType.Create: + await this.HandleRoleAssignment(consumeContext.Message.UserId, consumeContext.Message.Role); + break; + + case UserActionType.Update: + await this.HandleUserUpdate( + consumeContext.Message.UserId, + consumeContext.Message.FirstName, + consumeContext.Message.LastName, + consumeContext.Message.MiddleName); + break; + + default: + this.logger.LogWarning("Unhandled action type: {Action}", consumeContext.Message.Action); + break; + } + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error processing user role action for user {UserId}", consumeContext.Message.UserId); + throw; + } + } + + private async Task HandleRoleAssignment(string userId, string roleName) + { + var role = await this.context.Roles + .FirstOrDefaultAsync(r => r.Name == roleName); + + if (role == null) + { + this.logger.LogError("Role {RoleName} not found", roleName); + throw new InvalidOperationException($"Role {roleName} not found"); + } + + var existingAssignment = await this.context.UserRoles + .AnyAsync(ur => ur.UserId == userId && ur.RoleId == role.Id); + + if (existingAssignment) + { + this.logger.LogWarning("User {UserId} already has role {RoleName}", userId, roleName); + return; + } + + this.context.UserRoles.Add(new IdentityUserRole + { + UserId = userId, + RoleId = role.Id, + }); + + await this.context.SaveChangesAsync(); + this.logger.LogInformation("Assigned role {RoleName} to user {UserId}", roleName, userId); + } + + private async Task HandleRoleRemoval(string userId, string roleName) + { + var role = await this.context.Roles + .FirstOrDefaultAsync(r => r.Name == roleName); + + if (role == null) + { + this.logger.LogError("Role {RoleName} not found", roleName); + throw new InvalidOperationException($"Role {roleName} not found"); + } + + var userRole = await this.context.UserRoles + .FirstOrDefaultAsync(ur => ur.UserId == userId && ur.RoleId == role.Id); + + if (userRole == null) + { + this.logger.LogWarning("User {UserId} doesn't have role {RoleName}", userId, roleName); + return; + } + + this.context.UserRoles.Remove(userRole); + await this.context.SaveChangesAsync(); + this.logger.LogInformation("Removed role {RoleName} from user {UserId}", roleName, userId); + } + + private async Task HandleUserUpdate( + string userId, + string firstName, + string lastName, + string? middleName) + { + var user = await this.context.Users.FindAsync(userId); + if (user == null) + { + this.logger.LogError("User {UserId} not found", userId); + throw new InvalidOperationException($"User {userId} not found"); + } + + user.FirstName = firstName; + user.LastName = lastName; + user.MiddleName = middleName; + + await this.context.SaveChangesAsync(); + this.logger.LogInformation("Updated user details for {UserId}", userId); + } +} \ No newline at end of file diff --git a/Services/AuthService.Api/Data/AuthDbContext.cs b/Services/AuthService.Api/Data/AuthDbContext.cs new file mode 100644 index 0000000..f56b239 --- /dev/null +++ b/Services/AuthService.Api/Data/AuthDbContext.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +using AuthService.Api.Models; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +/// +/// Authentication service Db context. +/// +public class AuthDbContext : IdentityDbContext +{ + /// + /// Initializes a new instance of the class. + /// Auth Db Context Constructor. + /// + /// Db Context options. + public AuthDbContext(DbContextOptions options) + : base(options) + { + } + + /// + /// Gets or sets refresh token table. + /// + public DbSet RefreshTokens { get; set; } + + /// + /// On model creating method. + /// + /// Model builder. + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + } +} \ No newline at end of file diff --git a/Services/AuthService.Api/Dockerfile b/Services/AuthService.Api/Dockerfile new file mode 100644 index 0000000..faa54fe --- /dev/null +++ b/Services/AuthService.Api/Dockerfile @@ -0,0 +1,30 @@ +# См. статью по ссылке https://aka.ms/customizecontainer, чтобы узнать как настроить контейнер отладки и как Visual Studio использует этот Dockerfile для создания образов для ускорения отладки. + +# Этот этап используется при запуске из VS в быстром режиме (по умолчанию для конфигурации отладки) +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 ["Services/AuthService.Api/AuthService.Api.csproj", "Services/AuthService.Api/"] +RUN dotnet restore "./Services/AuthService.Api/AuthService.Api.csproj" +COPY . . +WORKDIR "/src/Services/AuthService.Api" +RUN dotnet build "./AuthService.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# Этот этап используется для публикации проекта службы, который будет скопирован на последний этап +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./AuthService.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# Этот этап используется в рабочей среде или при запуске из VS в обычном режиме (по умолчанию, когда конфигурация отладки не используется) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "AuthService.Api.dll"] \ No newline at end of file diff --git a/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.Designer.cs b/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.Designer.cs new file mode 100644 index 0000000..532777e --- /dev/null +++ b/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.Designer.cs @@ -0,0 +1,276 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AuthService.Api.Migrations +{ + [DbContext(typeof(AuthDbContext))] + [Migration("20250212205718_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.cs b/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.cs new file mode 100644 index 0000000..ffcc3b1 --- /dev/null +++ b/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.cs @@ -0,0 +1,227 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +#nullable disable + +namespace AuthService.Api.Migrations +{ + using System; + using Microsoft.EntityFrameworkCore.Migrations; + using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + RoleId = table.Column(type: "text", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/Services/AuthService.Api/Migrations/20250418071219_AddNames.Designer.cs b/Services/AuthService.Api/Migrations/20250418071219_AddNames.Designer.cs new file mode 100644 index 0000000..c713313 --- /dev/null +++ b/Services/AuthService.Api/Migrations/20250418071219_AddNames.Designer.cs @@ -0,0 +1,290 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AuthService.Api.Migrations +{ + [DbContext(typeof(AuthDbContext))] + [Migration("20250418071219_AddNames")] + partial class AddNames + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MiddleName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Services/AuthService.Api/Migrations/20250418071219_AddNames.cs b/Services/AuthService.Api/Migrations/20250418071219_AddNames.cs new file mode 100644 index 0000000..8d6abf4 --- /dev/null +++ b/Services/AuthService.Api/Migrations/20250418071219_AddNames.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +#nullable disable + +namespace AuthService.Api.Migrations +{ + using Microsoft.EntityFrameworkCore.Migrations; + + /// + public partial class AddNames : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FirstName", + table: "AspNetUsers", + type: "character varying(100)", + maxLength: 100, + nullable: false, + defaultValue: string.Empty); + + migrationBuilder.AddColumn( + name: "LastName", + table: "AspNetUsers", + type: "character varying(100)", + maxLength: 100, + nullable: false, + defaultValue: string.Empty); + + migrationBuilder.AddColumn( + name: "MiddleName", + table: "AspNetUsers", + type: "character varying(100)", + maxLength: 100, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FirstName", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "LastName", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "MiddleName", + table: "AspNetUsers"); + } + } +} diff --git a/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.Designer.cs b/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.Designer.cs new file mode 100644 index 0000000..9840c54 --- /dev/null +++ b/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.Designer.cs @@ -0,0 +1,330 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AuthService.Api.Migrations +{ + [DbContext(typeof(AuthDbContext))] + [Migration("20250429162411_AddedRefreshToken")] + partial class AddedRefreshToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MiddleName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AuthService.Api.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Revoked") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AuthService.Api.Models.RefreshToken", b => + { + b.HasOne("ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.cs b/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.cs new file mode 100644 index 0000000..24db2f3 --- /dev/null +++ b/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +#nullable disable + +namespace AuthService.Api.Migrations +{ + using System; + using Microsoft.EntityFrameworkCore.Migrations; + using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + + /// + public partial class AddedRefreshToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RefreshTokens", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "text", nullable: false), + Token = table.Column(type: "text", nullable: false), + Expires = table.Column(type: "timestamp with time zone", nullable: false), + Revoked = table.Column(type: "timestamp with time zone", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshTokens", x => x.Id); + table.ForeignKey( + name: "FK_RefreshTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RefreshTokens_UserId", + table: "RefreshTokens", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RefreshTokens"); + } + } +} diff --git a/Services/AuthService.Api/Migrations/AuthDbContextModelSnapshot.cs b/Services/AuthService.Api/Migrations/AuthDbContextModelSnapshot.cs new file mode 100644 index 0000000..46322ef --- /dev/null +++ b/Services/AuthService.Api/Migrations/AuthDbContextModelSnapshot.cs @@ -0,0 +1,327 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AuthService.Api.Migrations +{ + [DbContext(typeof(AuthDbContext))] + partial class AuthDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MiddleName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AuthService.Api.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Revoked") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AuthService.Api.Models.RefreshToken", b => + { + b.HasOne("ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Services/AuthService.Api/Models/ApplicationUser.cs b/Services/AuthService.Api/Models/ApplicationUser.cs new file mode 100644 index 0000000..facb335 --- /dev/null +++ b/Services/AuthService.Api/Models/ApplicationUser.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Identity; + +/// +/// Application User model. +/// +public class ApplicationUser : IdentityUser +{ + /// + /// Gets or sets FirstName column. + /// + [MaxLength(100)] + public string FirstName { get; set; } = null!; + + /// + /// Gets or sets LastName column. + /// + [MaxLength(100)] + public string LastName { get; set; } = null!; + + /// + /// Gets or sets MiddleName column. + /// + [MaxLength(100)] + public string? MiddleName { get; set; } +} diff --git a/Services/AuthService.Api/Models/ApplicationUserDTO.cs b/Services/AuthService.Api/Models/ApplicationUserDTO.cs new file mode 100644 index 0000000..7a58100 --- /dev/null +++ b/Services/AuthService.Api/Models/ApplicationUserDTO.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace AuthService.Api.Models; + +using System.ComponentModel.DataAnnotations; + +/// +/// Application User DTO. +/// +public class ApplicationUserDTO +{ + /// + /// Gets or sets user email. + /// + public required string Email { get; set; } = string.Empty; + + /// + /// Gets or sets user password. + /// + public required string Password { get; set; } = string.Empty; + + /// + /// Gets or sets FirstName column. + /// + [MaxLength(100)] + public string FirstName { get; set; } = null!; + + /// + /// Gets or sets LastName column. + /// + [MaxLength(100)] + public string LastName { get; set; } = null!; + + /// + /// Gets or sets MiddleName column. + /// + [MaxLength(100)] + public string? MiddleName { get; set; } + + /// + /// Gets or sets Roles. + /// + public string[]? Roles { get; set; } +} \ No newline at end of file diff --git a/Services/AuthService.Api/Models/AuthResponse.cs b/Services/AuthService.Api/Models/AuthResponse.cs new file mode 100644 index 0000000..267cd0b --- /dev/null +++ b/Services/AuthService.Api/Models/AuthResponse.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace AuthService.Api.Models; + +/// +/// Auth response class. +/// +public class AuthResponse +{ + /// + /// Gets or sets Token. + /// + public string Token { get; set; } = string.Empty; + + /// + /// Gets or sets Refresh Token. + /// + public string RefreshToken { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Services/AuthService.Api/Models/LoginModel.cs b/Services/AuthService.Api/Models/LoginModel.cs new file mode 100644 index 0000000..fc4e221 --- /dev/null +++ b/Services/AuthService.Api/Models/LoginModel.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace AuthService.Api.Models +{ + /// + /// Login model. + /// + public class LoginModel + { + /// + /// Gets or sets user email. + /// + public required string Email { get; set; } = string.Empty; + + /// + /// Gets or sets user password. + /// + public required string Password { get; set; } = string.Empty; + } +} diff --git a/Services/AuthService.Api/Models/RefreshToken.cs b/Services/AuthService.Api/Models/RefreshToken.cs new file mode 100644 index 0000000..b700cac --- /dev/null +++ b/Services/AuthService.Api/Models/RefreshToken.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace AuthService.Api.Models +{ + using System; + + /// + /// Refresh token table model. + /// + public class RefreshToken + { + /// + /// Gets or sets the unique identifier. + /// + public int Id { get; set; } + + /// + /// Gets or sets the user identifier. + /// + public string UserId { get; set; } + + /// + /// Gets or sets the token value. + /// + public string Token { get; set; } + + /// + /// Gets or sets the expiration date and time. + /// + public DateTime Expires { get; set; } + + /// + /// Gets a value indicating whether the token is expired. + /// + public bool IsExpired => DateTime.UtcNow >= this.Expires; + + /// + /// Gets or sets the revocation date and time (if revoked). + /// + public DateTime? Revoked { get; set; } + + /// + /// Gets a value indicating whether the token is active. + /// + public bool IsActive => !this.IsExpired && this.Revoked == null; + + /// + /// Gets or sets the associated user. + /// + public ApplicationUser User { get; set; } + } +} \ No newline at end of file diff --git a/Services/AuthService.Api/Program.cs b/Services/AuthService.Api/Program.cs new file mode 100644 index 0000000..e97878b --- /dev/null +++ b/Services/AuthService.Api/Program.cs @@ -0,0 +1,348 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +using System.Data; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using AuthService.Api; +using AuthService.Api.Consumers; +using AuthService.Api.Models; +using Contracts; +using MassTransit; +using MassTransit.Transports; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; + +var predefinedRoles = RoleNames.GetAllRoleNames(); + +var builder = WebApplication.CreateBuilder(args); + +var currentEnvironment = Environment.GetEnvironmentVariable("ENVIRONMENT") ?? "Default"; + +builder.Services.AddDbContext( + options => + options.UseNpgsql(builder.Configuration.GetConnectionString(currentEnvironment))); + +builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + +byte[] key = Encoding.UTF8.GetBytes( + builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key is missing.")); + +builder.Services.AddAuthentication( + cfg => + { + cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer( + options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(key), + }; + }); + +builder.Services.AddAuthorization( + options => { options.AddPolicy("AdminOnly", policy => policy.RequireRole("Администратор")); }); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen( + c => + { + c.AddSecurityDefinition( + "Bearer", + new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Enter JWT token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "Bearer", + }); + + c.AddSecurityRequirement( + new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }, + }, + new List() + }, + }); + }); + +builder.Services.AddCors( + options => + { + options.AddPolicy( + "CorsPolicy", + policyBuilder => policyBuilder + .AllowAnyMethod() + .AllowCredentials() + .SetIsOriginAllowed((_) => true) + .AllowAnyHeader()); + }); + +builder.Services.AddMassTransit( + x => + { + x.AddConsumer(); + + x.UsingRabbitMq( + (context, cfg) => + { + cfg.Host( + builder.Configuration["RabbitMQ:Host"], + "/", + h => + { + h.Username(builder.Configuration["RabbitMQ:Username"]); + h.Password(builder.Configuration["RabbitMQ:Password"]); + }); + + cfg.Message(x => x.SetEntityName("user-events")); + + cfg.ReceiveEndpoint("user-with-role-events", e => + { + e.ConfigureConsumer(context); + }); + }); + }); + +builder.Services.AddScoped(); + +var app = builder.Build(); + +app.UseCors("CorsPolicy"); + +// Enable Swagger in Development Mode +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapPost( + "/register", + async ( + UserManager userManager, + RoleManager roleManager, + IPublishEndpoint publishEndpoint, + ApplicationUserDTO userDto, + TokenService tokenService) => + { + var user = new ApplicationUser + { + UserName = userDto.Email, Email = userDto.Email, FirstName = userDto.FirstName, LastName = userDto.LastName, + MiddleName = userDto.MiddleName, + }; + var result = await userManager.CreateAsync(user, userDto.Password); + + if (!result.Succeeded) + { + return Results.BadRequest(result.Errors); + } + + var assignedRoles = new List(); + if (userDto.Roles?.Length > 0) + { + foreach (var role in userDto.Roles) + { + if (!await roleManager.RoleExistsAsync(role)) + { + await roleManager.CreateAsync(new IdentityRole(role)); + } + + await userManager.AddToRoleAsync(user, role); + assignedRoles.Add(role); + } + } + + await publishEndpoint.Publish( + new UserCreatedEvent( + user.Id, + user.UserName, + userDto.FirstName, + userDto.LastName, + userDto.MiddleName, + assignedRoles.ToArray(), + DateTime.UtcNow)); + + var token = await tokenService.GenerateJwtToken(user); + var refreshToken = await tokenService.GenerateRefreshToken(user); + + return Results.Ok( + new + { + UserId = user.Id, + AssignedRoles = assignedRoles, + Token = token, + RefreshToken = refreshToken, + }); + }); + +app.MapPost("/login", async (LoginModel login, UserManager userManager, SignInManager signInManager, TokenService tokenService) => +{ + var user = await userManager.FindByEmailAsync(login.Email); + if (user == null) + { + return Results.BadRequest("Invalid credentials"); + } + + var result = await signInManager.CheckPasswordSignInAsync(user, login.Password, false); + if (!result.Succeeded) + { + return Results.BadRequest("Invalid credentials"); + } + + var token = await tokenService.GenerateJwtToken(user); + var refreshToken = await tokenService.GenerateRefreshToken(user); + + return Results.Ok(new AuthResponse + { + Token = token, + RefreshToken = refreshToken, + }); +}); + +app.MapPost("/refresh", async (HttpContext context, TokenService tokenService, AuthResponse model) => +{ + var token = model.Token; + var refreshToken = model.RefreshToken; + + if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(refreshToken)) + { + return Results.BadRequest("Invalid tokens"); + } + + try + { + var response = await tokenService.RefreshTokenAsync(token, refreshToken); + return Results.Ok(response); + } + catch (SecurityTokenException ex) + { + return Results.BadRequest(ex.Message); + } +}); + +// **Add Role to User (Admin Only)** +app.MapPost( + "/add-role", + async ( + UserManager userManager, + RoleManager roleManager, + string email, + string role) => + { + if (!predefinedRoles.Contains(role)) + { + return Results.BadRequest("Invalid role"); + } + + var user = await userManager.FindByEmailAsync(email); + if (user == null) + { + return Results.NotFound("User not found"); + } + + if (!await roleManager.RoleExistsAsync(role)) + { + await roleManager.CreateAsync(new IdentityRole(role)); + } + + await userManager.AddToRoleAsync(user, role); + return Results.Ok($"Role '{role}' added to {email}"); + }).RequireAuthorization(); + +app.MapGet("/user", async (UserManager userManager, string userId) => +{ + var user = await userManager.FindByIdAsync(userId); + if (user == null) + { + return null; + } + + var roles = await userManager.GetRolesAsync(user); + var model = new UserDTO() + { + UserId = user.Id, + Email = user.Email ?? string.Empty, + UserName = user.UserName ?? string.Empty, + FirstName = user.FirstName, + LastName = user.LastName, + MiddleName = user.MiddleName, + Roles = roles.ToArray(), + }; + return model; +}); + +app.MapGet("/userId", async (UserManager userManager, string userName) => +{ + var user = await userManager.FindByNameAsync(userName); + if (user == null) + { + return null; + } + + return user.Id; +}); + +app.MapGet("/users", async (UserManager userManager) => +{ + var users = await userManager.Users.ToListAsync(); + + var userDtos = new List(); + + foreach (var user in users) + { + var roles = await userManager.GetRolesAsync(user); + userDtos.Add(new UserDTO + { + UserId = user.Id, + Email = user.Email ?? string.Empty, + UserName = user.UserName ?? string.Empty, + FirstName = user.FirstName, + LastName = user.LastName, + MiddleName = user.MiddleName, + Roles = roles.ToArray(), + }); + } + + return Results.Ok(userDtos); +}).RequireAuthorization("AdminOnly"); + +// **Ensure Roles Exist in Database** +using (var scope = app.Services.CreateScope()) +{ + var roleManager = scope.ServiceProvider.GetRequiredService>(); + + foreach (var role in predefinedRoles) + { + if (!await roleManager.RoleExistsAsync(role)) + { + await roleManager.CreateAsync(new IdentityRole(role)); + } + } +} + +app.UseAuthentication(); +app.UseAuthorization(); + +app.Run(); \ No newline at end of file diff --git a/Services/AuthService.Api/Properties/launchSettings.json b/Services/AuthService.Api/Properties/launchSettings.json new file mode 100644 index 0000000..879572d --- /dev/null +++ b/Services/AuthService.Api/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5135" + }, + "https": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7248;http://localhost:5135" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/Services/AuthService.Api/TokenService.cs b/Services/AuthService.Api/TokenService.cs new file mode 100644 index 0000000..4d38126 --- /dev/null +++ b/Services/AuthService.Api/TokenService.cs @@ -0,0 +1,174 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace AuthService.Api +{ + using System; + using System.Collections.Generic; + using System.IdentityModel.Tokens.Jwt; + using System.Linq; + using System.Security.Claims; + using System.Security.Cryptography; + using System.Text; + using System.Threading.Tasks; + using AuthService.Api.Models; + using Microsoft.AspNetCore.Identity; + using Microsoft.EntityFrameworkCore; + using Microsoft.IdentityModel.Tokens; + + /// + /// Service for handling JWT and refresh token operations. + /// + public class TokenService + { + private readonly IConfiguration config; + private readonly AuthDbContext context; + private readonly UserManager userManager; + + /// + /// Initializes a new instance of the class. + /// + /// The application configuration. + /// The database context. + /// User manager. + public TokenService(IConfiguration config, AuthDbContext context, UserManager userManager) + { + this.config = config; + this.context = context; + this.userManager = userManager; + } + + /// + /// Generates a JWT token for the specified user. + /// + /// The user to generate token for. + /// The generated JWT token. + public async Task GenerateJwtToken(ApplicationUser user) + { + var userRoles = await this.userManager.GetRolesAsync(user); + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Name, user.UserName ?? string.Empty), + new Claim(ClaimTypes.Email, user.Email ?? string.Empty), + new(ClaimTypes.GivenName, user.FirstName), + new(ClaimTypes.Surname, user.LastName), + new("middle_name", user.MiddleName ?? string.Empty), + }; + + claims.AddRange(userRoles.Select(role => new Claim(ClaimTypes.Role, role))); + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.config["Jwt:Key"])); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + var expires = DateTime.Now.AddMinutes(Convert.ToDouble(this.config["Jwt:ExpireMinutes"])); + + var token = new JwtSecurityToken( + this.config["Jwt:Issuer"], + this.config["Jwt:Audience"], + claims, + expires: expires, + signingCredentials: creds); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + /// + /// Generates and stores a refresh token for the specified user. + /// + /// The user to generate token for. + /// The generated refresh token. + public async Task GenerateRefreshToken(ApplicationUser user) + { + var refreshToken = new RefreshToken + { + UserId = user.Id, + Token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)), + Expires = DateTime.UtcNow.AddDays(7), + Revoked = null, + }; + + this.context.RefreshTokens.Add(refreshToken); + await this.context.SaveChangesAsync(); + + return refreshToken.Token; + } + + /// + /// Refreshes an expired JWT token using a valid refresh token. + /// + /// The expired JWT token. + /// The valid refresh token. + /// Authentication response with new tokens. + /// Thrown when tokens are invalid. + public async Task RefreshTokenAsync(string token, string refreshToken) + { + var principal = this.GetPrincipalFromExpiredToken(token); + var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier); + + var user = await this.context.Users.FindAsync(userId); + if (user == null) + { + throw new SecurityTokenException("Invalid user"); + } + + var storedRefreshToken = await this.context.RefreshTokens + .FirstOrDefaultAsync(x => x.Token == refreshToken && x.UserId == userId); + + if (storedRefreshToken == null || storedRefreshToken.IsExpired || storedRefreshToken.Revoked != null) + { + throw new SecurityTokenException("Invalid refresh token"); + } + + var newToken = await this.GenerateJwtToken(user); + var newRefreshToken = await this.GenerateRefreshToken(user); + + storedRefreshToken.Revoked = DateTime.UtcNow; + + // Periodically remove old revoked/expired tokens + var cutoff = DateTime.UtcNow.AddMonths(-1); + var oldTokens = await this.context.RefreshTokens + .Where(t => t.Revoked < cutoff || t.Expires < cutoff) + .ToListAsync(); + + this.context.RefreshTokens.RemoveRange(oldTokens); + + await this.context.SaveChangesAsync(); + + return new AuthResponse + { + Token = newToken, + RefreshToken = newRefreshToken, + }; + } + + /// + /// Gets the principal from an expired token. + /// + /// The expired token. + /// The claims principal. + /// Thrown when token is invalid. + private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) + { + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.config["Jwt:Key"])), + ValidateLifetime = false, + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken); + + if (securityToken is not JwtSecurityToken jwtSecurityToken || + !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) + { + throw new SecurityTokenException("Invalid token"); + } + + return principal; + } + } +} \ No newline at end of file diff --git a/Services/AuthService.Api/appsettings.Development.json b/Services/AuthService.Api/appsettings.Development.json new file mode 100644 index 0000000..a34cd70 --- /dev/null +++ b/Services/AuthService.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Services/AuthService.Api/appsettings.json b/Services/AuthService.Api/appsettings.json new file mode 100644 index 0000000..29c6967 --- /dev/null +++ b/Services/AuthService.Api/appsettings.json @@ -0,0 +1,25 @@ +{ + "RabbitMQ": { + "Host": "rabbitmq", + "Username": "admin", + "Password": "admin123", + "Port": 5672 + }, + "Jwt": { + "Key": "YourSuperLongSecretKeyThatIsAtLeast32Characters!", + "Issuer": "AuthService", + "Audience": "Gateway", + "ExpireMinutes": "15" + }, + "ConnectionStrings": { + "Default": "Host=localhost:1435;Database=postgres;Username=postgres;Password=postgres", + "Docker": "Server=auth.db;Database=postgres;Username=postgres;Password=postgres" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Services/CoreService.Api/Consumers/UserCreatedConsumer.cs b/Services/CoreService.Api/Consumers/UserCreatedConsumer.cs new file mode 100644 index 0000000..0e57da6 --- /dev/null +++ b/Services/CoreService.Api/Consumers/UserCreatedConsumer.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace CoreService.Api.Consumers +{ + using Contracts; + using CoreService.Api.Core; + using CoreService.Api.Core.Models; + using CoreService.Api.Core.Queries; + using MassTransit; + + /// + /// Consumer of user creation event. + /// + public class UserCreatedConsumer : IConsumer + { + private readonly LecturersQueries lecturersQueries; + private readonly StudentsQueries studentsQueries; + private readonly ConsultantsQueries consultantsQueries; + + /// + /// Initializes a new instance of the class. + /// + /// Core DB context. + public UserCreatedConsumer(CoreContext context) + { + this.lecturersQueries = new LecturersQueries(context); + this.studentsQueries = new StudentsQueries(context); + this.consultantsQueries = new ConsultantsQueries(context); + } + + /// + /// Consumes Event. + /// + /// Consume event context. + /// A representing the asynchronous operation. + public async Task Consume(ConsumeContext context) + { + if (context.Message.Roles.Contains(RoleNames.GetName(UserRoleType.Supervisor))) + { + var lecturer = new Lecturer() + { + Userid = context.Message.UserId, + FirstName = context.Message.FirstName, + LastName = context.Message.LastName, + MiddleName = context.Message.MiddleName, + }; + await this.lecturersQueries.InsertOrUpdateLecturer(lecturer); + } + else if (context.Message.Roles.Contains(RoleNames.GetName(UserRoleType.Student))) + { + var student = new Student() + { + Userid = context.Message.UserId, + FirstName = context.Message.FirstName, + LastName = context.Message.LastName, + MiddleName = context.Message.MiddleName, + }; + await this.studentsQueries.InsertOrUpdateStudent(student); + } + else if (context.Message.Roles.Contains(RoleNames.GetName(UserRoleType.Consultant))) + { + var consultant = new Consultant() + { + Userid = context.Message.UserId, + FirstName = context.Message.FirstName, + LastName = context.Message.LastName, + MiddleName = context.Message.MiddleName, + }; + await this.consultantsQueries.InsertOrUpdateConsultant(consultant); + } + } + } +} \ No newline at end of file diff --git a/Services/CoreService.Api/Core/CoreContext.cs b/Services/CoreService.Api/Core/CoreContext.cs index a3b6b31..140a19b 100644 --- a/Services/CoreService.Api/Core/CoreContext.cs +++ b/Services/CoreService.Api/Core/CoreContext.cs @@ -2,11 +2,9 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core; +namespace CoreService.Api.Core; -using System; -using System.Collections.Generic; -using CoreService.Core.Models; +using CoreService.Api.Core.Models; using Microsoft.EntityFrameworkCore; /// @@ -72,14 +70,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.ToTable("consultants"); + entity.Property(e => e.FirstName) + .HasMaxLength(100) + .HasColumnName("firstname"); + entity.Property(e => e.LastName) + .HasMaxLength(100) + .HasColumnName("lastname"); + entity.Property(e => e.MiddleName) + .HasMaxLength(100) + .HasColumnName("middlename"); entity.Property(e => e.Id).HasColumnName("id"); entity.Property(e => e.Contact) .HasMaxLength(500) .HasColumnName("contact"); - entity.Property(e => e.Name) - .HasMaxLength(255) - .HasColumnName("name"); - entity.Property(e => e.Userid).HasColumnName("userid"); + entity.Property(e => e.Userid).HasMaxLength(255).HasColumnName("userid"); }); modelBuilder.Entity(entity => @@ -105,13 +109,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.ToTable("lecturers"); entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.FirstName) + .HasMaxLength(100) + .HasColumnName("firstname"); + entity.Property(e => e.LastName) + .HasMaxLength(100) + .HasColumnName("lastname"); + entity.Property(e => e.MiddleName) + .HasMaxLength(100) + .HasColumnName("middlename"); entity.Property(e => e.Cansupervisevkr) .HasDefaultValue(false) .HasColumnName("cansupervisevkr"); entity.Property(e => e.Department) .HasMaxLength(500) .HasColumnName("department"); - entity.Property(e => e.Userid).HasColumnName("userid"); + entity.Property(e => e.Userid).HasMaxLength(255).HasColumnName("userid"); }); modelBuilder.Entity(entity => @@ -132,6 +145,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasMaxLength(50) .HasColumnName("status"); entity.Property(e => e.Studentid).HasColumnName("studentid"); + entity.Property(e => e.Consultantid).HasColumnName("consultantid"); + entity.Property(e => e.Supervisorid).HasColumnName("supervisorid"); entity.Property(e => e.Themeid).HasColumnName("themeid"); entity.Property(e => e.Type) .HasMaxLength(255) @@ -146,6 +161,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("student_fk"); + entity.HasOne(d => d.Consultant).WithMany(p => p.Practices) + .HasForeignKey(d => d.Consultantid) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("consultant_fk"); + + entity.HasOne(d => d.Supervisor).WithMany(p => p.Practices) + .HasForeignKey(d => d.Supervisorid) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("supervisor_fk"); + entity.HasOne(d => d.Theme).WithMany(p => p.Practices) .HasForeignKey(d => d.Themeid) .OnDelete(DeleteBehavior.ClientSetNull) @@ -158,9 +183,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.ToTable("students"); + entity.Property(e => e.FirstName) + .HasMaxLength(100) + .HasColumnName("firstname"); + entity.Property(e => e.LastName) + .HasMaxLength(100) + .HasColumnName("lastname"); + entity.Property(e => e.MiddleName) + .HasMaxLength(100) + .HasColumnName("middlename"); entity.Property(e => e.Id).HasColumnName("id"); entity.Property(e => e.Groupid).HasColumnName("groupid"); - entity.Property(e => e.Userid).HasColumnName("userid"); + entity.Property(e => e.Userid).HasMaxLength(255).HasColumnName("userid"); entity.HasOne(d => d.Group).WithMany(p => p.Students) .HasForeignKey(d => d.Groupid) @@ -192,8 +226,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Level) .HasMaxLength(255) .HasColumnName("level"); - entity.Property(e => e.Suggestedby) + entity.Property(e => e.Source) .HasMaxLength(500) + .HasColumnName("source"); + entity.Property(e => e.Suggestedby) + .HasMaxLength(100) .HasColumnName("suggestedby"); entity.Property(e => e.Supervisorid).HasColumnName("supervisorid"); entity.Property(e => e.Tags) diff --git a/Services/CoreService.Api/Core/Models/Consultant.cs b/Services/CoreService.Api/Core/Models/Consultant.cs index 79e7585..6ea5373 100644 --- a/Services/CoreService.Api/Core/Models/Consultant.cs +++ b/Services/CoreService.Api/Core/Models/Consultant.cs @@ -2,10 +2,7 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Models; - -using System; -using System.Collections.Generic; +namespace CoreService.Api.Core.Models; /// /// Consultant table model. @@ -18,9 +15,19 @@ public partial class Consultant public int Id { get; set; } /// - /// Gets or sets Name column. + /// Gets or sets FirstName column. + /// + public string FirstName { get; set; } = null!; + + /// + /// Gets or sets LastName column. + /// + public string LastName { get; set; } = null!; + + /// + /// Gets or sets MiddleName column. /// - public string Name { get; set; } = null!; + public string? MiddleName { get; set; } /// /// Gets or sets Contact column. @@ -30,10 +37,15 @@ public partial class Consultant /// /// Gets or sets UserId column. /// - public Guid? Userid { get; set; } + public string? Userid { get; set; } /// /// Gets or sets virtual Themes. /// public virtual ICollection Themes { get; set; } = new List(); + + /// + /// Gets or sets virtual Practices. + /// + public virtual ICollection Practices { get; set; } = new List(); } diff --git a/Services/CoreService.Api/Core/Models/Group.cs b/Services/CoreService.Api/Core/Models/Group.cs index 2a22024..e6d50e4 100644 --- a/Services/CoreService.Api/Core/Models/Group.cs +++ b/Services/CoreService.Api/Core/Models/Group.cs @@ -2,10 +2,7 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Models; - -using System; -using System.Collections.Generic; +namespace CoreService.Api.Core.Models; /// /// Group model. diff --git a/Services/CoreService.Api/Core/Models/Lecturer.cs b/Services/CoreService.Api/Core/Models/Lecturer.cs index ccabfa3..34ae209 100644 --- a/Services/CoreService.Api/Core/Models/Lecturer.cs +++ b/Services/CoreService.Api/Core/Models/Lecturer.cs @@ -2,10 +2,7 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Models; - -using System; -using System.Collections.Generic; +namespace CoreService.Api.Core.Models; /// /// Lecturer model. @@ -17,10 +14,25 @@ public partial class Lecturer /// public int Id { get; set; } + /// + /// Gets or sets FirstName column. + /// + public string FirstName { get; set; } = null!; + + /// + /// Gets or sets LastName column. + /// + public string LastName { get; set; } = null!; + + /// + /// Gets or sets MiddleName column. + /// + public string? MiddleName { get; set; } + /// /// Gets or sets UserId column. /// - public Guid Userid { get; set; } + public string Userid { get; set; } = null!; /// /// Gets or sets Department column. @@ -36,4 +48,9 @@ public partial class Lecturer /// Gets or sets virtual Themes. /// public virtual ICollection Themes { get; set; } = new List(); + + /// + /// Gets or sets virtual Practices. + /// + public virtual ICollection Practices { get; set; } = new List(); } diff --git a/Services/CoreService.Api/Core/Models/Practice.cs b/Services/CoreService.Api/Core/Models/Practice.cs index 8c7aaa6..fccec98 100644 --- a/Services/CoreService.Api/Core/Models/Practice.cs +++ b/Services/CoreService.Api/Core/Models/Practice.cs @@ -2,10 +2,7 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Models; - -using System; -using System.Collections.Generic; +namespace CoreService.Api.Core.Models; /// /// Practice model. @@ -22,6 +19,16 @@ public partial class Practice /// public int Studentid { get; set; } + /// + /// Gets or sets ConsultantId column. + /// + public int? Consultantid { get; set; } + + /// + /// Gets or sets SupervisorId column. + /// + public int Supervisorid { get; set; } + /// /// Gets or sets ThemeId column. /// @@ -57,6 +64,16 @@ public partial class Practice /// public virtual Student Student { get; set; } = null!; + /// + /// Gets or sets virtual Consultant. + /// + public virtual Consultant Consultant { get; set; } = null!; + + /// + /// Gets or sets virtual Supervisor. + /// + public virtual Lecturer Supervisor { get; set; } = null!; + /// /// Gets or sets virtual Theme. /// diff --git a/Services/CoreService.Api/Core/Models/Student.cs b/Services/CoreService.Api/Core/Models/Student.cs index e4c349f..19866e0 100644 --- a/Services/CoreService.Api/Core/Models/Student.cs +++ b/Services/CoreService.Api/Core/Models/Student.cs @@ -2,10 +2,7 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Models; - -using System; -using System.Collections.Generic; +namespace CoreService.Api.Core.Models; /// /// Student model. @@ -17,15 +14,30 @@ public partial class Student /// public int Id { get; set; } + /// + /// Gets or sets FirstName column. + /// + public string FirstName { get; set; } = null!; + + /// + /// Gets or sets LastName column. + /// + public string LastName { get; set; } = null!; + + /// + /// Gets or sets MiddleName column. + /// + public string? MiddleName { get; set; } + /// /// Gets or sets UserId column. /// - public Guid Userid { get; set; } + public string Userid { get; set; } = null!; /// /// Gets or sets GroupId. /// - public int Groupid { get; set; } + public int? Groupid { get; set; } /// /// Gets or sets virtual Group. diff --git a/Services/CoreService.Api/Core/Models/Theme.cs b/Services/CoreService.Api/Core/Models/Theme.cs index ed4f4b2..687ea74 100644 --- a/Services/CoreService.Api/Core/Models/Theme.cs +++ b/Services/CoreService.Api/Core/Models/Theme.cs @@ -2,10 +2,7 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Models; - -using System; -using System.Collections.Generic; +namespace CoreService.Api.Core.Models; /// /// Theme model. @@ -47,6 +44,11 @@ public partial class Theme /// public bool Isarchived { get; set; } + /// + /// Gets or sets Source column. + /// + public string Source { get; set; } = null!; + /// /// Gets or sets SuggestedBy column. /// @@ -55,12 +57,12 @@ public partial class Theme /// /// Gets or sets ConsultantId column. /// - public int Consultantid { get; set; } + public int? Consultantid { get; set; } /// /// Gets or sets SupervisorId. /// - public int Supervisorid { get; set; } + public int? Supervisorid { get; set; } /// /// Gets or sets CreatedDate column. diff --git a/Services/CoreService.Api/Core/Queries/ConsultantsQueries.cs b/Services/CoreService.Api/Core/Queries/ConsultantsQueries.cs index 1e2cd0d..19d955b 100644 --- a/Services/CoreService.Api/Core/Queries/ConsultantsQueries.cs +++ b/Services/CoreService.Api/Core/Queries/ConsultantsQueries.cs @@ -2,9 +2,9 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Queries; +namespace CoreService.Api.Core.Queries; -using CoreService.Core.Models; +using CoreService.Api.Core.Models; using Microsoft.EntityFrameworkCore; /// @@ -34,11 +34,17 @@ public async Task> GetConsultants(int? id = null) /// /// Input consultant. /// Response status. - public async Task InsertConsultant(Consultant consultant) + public async Task InsertOrUpdateConsultant(Consultant consultant) { + var prev = await context.Consultants.FindAsync(consultant.Id); + if (prev == null) + { + return await this.UpdateConsultant(consultant); + } + context.Consultants.Add(consultant); await context.SaveChangesAsync(); - return consultant.Id; + return Results.Ok(consultant.Id); } /// @@ -56,8 +62,10 @@ public async Task UpdateConsultant(Consultant consultant) return Results.BadRequest(); } - prev.Name = consultant.Name; - prev.Contact = consultant.Contact; + prev.FirstName = string.IsNullOrEmpty(consultant.FirstName) ? prev.FirstName : consultant.FirstName; + prev.LastName = string.IsNullOrEmpty(consultant.LastName) ? prev.LastName : consultant.LastName; + prev.MiddleName = string.IsNullOrEmpty(consultant.MiddleName) ? prev.MiddleName : consultant.MiddleName; + prev.Contact = string.IsNullOrEmpty(consultant.Contact) ? prev.Contact : consultant.Contact; await context.SaveChangesAsync(); return Results.Ok(); } diff --git a/Services/CoreService.Api/Core/Queries/GroupsQueries.cs b/Services/CoreService.Api/Core/Queries/GroupsQueries.cs index 4f7fd62..e6e7aa2 100644 --- a/Services/CoreService.Api/Core/Queries/GroupsQueries.cs +++ b/Services/CoreService.Api/Core/Queries/GroupsQueries.cs @@ -2,9 +2,9 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Queries; +namespace CoreService.Api.Core.Queries; -using CoreService.Core.Models; +using CoreService.Api.Core.Models; using Microsoft.EntityFrameworkCore; /// @@ -56,8 +56,8 @@ public async Task UpdateGroup(Group group) return Results.BadRequest(); } - prev.Name = group.Name; - prev.Program = group.Program; + prev.Name = string.IsNullOrEmpty(group.Name) ? prev.Name : group.Name; + prev.Program = string.IsNullOrEmpty(group.Program) ? prev.Program : group.Program; prev.Year = group.Year; await context.SaveChangesAsync(); return Results.Ok(); diff --git a/Services/CoreService.Api/Core/Queries/LecturersQueries.cs b/Services/CoreService.Api/Core/Queries/LecturersQueries.cs index 0f7652f..babd7a9 100644 --- a/Services/CoreService.Api/Core/Queries/LecturersQueries.cs +++ b/Services/CoreService.Api/Core/Queries/LecturersQueries.cs @@ -2,9 +2,9 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Queries; +namespace CoreService.Api.Core.Queries; -using CoreService.Core.Models; +using CoreService.Api.Core.Models; using Microsoft.EntityFrameworkCore; /// @@ -34,11 +34,17 @@ public async Task> GetLecturers(int? id = null) /// /// Input lecturer. /// Response status. - public async Task InsertLecturer(Lecturer lecturer) + public async Task InsertOrUpdateLecturer(Lecturer lecturer) { + var prev = await context.Lecturers.FindAsync(lecturer.Id); + if (prev != null) + { + return await this.UpdateLecturer(lecturer); + } + context.Lecturers.Add(lecturer); await context.SaveChangesAsync(); - return lecturer.Id; + return Results.Ok(lecturer.Id); } /// @@ -56,7 +62,10 @@ public async Task UpdateLecturer(Lecturer lecturer) return Results.BadRequest(); } - prev.Department = lecturer.Department; + prev.FirstName = string.IsNullOrEmpty(lecturer.FirstName) ? prev.FirstName : lecturer.FirstName; + prev.LastName = string.IsNullOrEmpty(lecturer.LastName) ? prev.LastName : lecturer.LastName; + prev.MiddleName = string.IsNullOrEmpty(lecturer.MiddleName) ? prev.MiddleName : lecturer.MiddleName; + prev.Department = string.IsNullOrEmpty(lecturer.Department) ? prev.Department : lecturer.Department; prev.Cansupervisevkr = lecturer.Cansupervisevkr; await context.SaveChangesAsync(); return Results.Ok(); @@ -74,8 +83,12 @@ public async Task UpdateLecturer(Lecturer lecturer) /// Response status. public async Task DeleteLecturer(int id) { - var deletedLecturer = context.Lecturers.First(lecturer => lecturer.Id == id); - context.Lecturers.Remove(deletedLecturer); + var deletedLecturer = await context.Lecturers.FindAsync(id); + if (deletedLecturer != null) + { + context.Lecturers.Remove(deletedLecturer); + } + await context.SaveChangesAsync(); return Results.Ok(); } diff --git a/Services/CoreService.Api/Core/Queries/PracticesQueries.cs b/Services/CoreService.Api/Core/Queries/PracticesQueries.cs index 9e7d3d6..3ae7698 100644 --- a/Services/CoreService.Api/Core/Queries/PracticesQueries.cs +++ b/Services/CoreService.Api/Core/Queries/PracticesQueries.cs @@ -2,9 +2,9 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Queries; +namespace CoreService.Api.Core.Queries; -using CoreService.Core.Models; +using CoreService.Api.Core.Models; using Microsoft.EntityFrameworkCore; /// @@ -29,6 +29,24 @@ public async Task> GetPractices(int? id = null) return await result.ToListAsync(); } + /// + /// Gets queried Practices. + /// + /// User id. + /// List of practices. + public async Task> GetQueriedPractices(string userId) + { + var result = context.Practices.Include(p => p.Student).AsQueryable(); + if (string.IsNullOrEmpty(userId)) + { + return new List(); + } + + result = result.Where(p => p.Student.Userid == userId); + + return await result.ToListAsync(); + } + /// /// Inserts new practice. /// @@ -56,10 +74,14 @@ public async Task UpdatePractice(Practice practice) return Results.BadRequest(); } + prev.Themeid = practice.Themeid; + prev.Consultantid = practice.Consultantid; + prev.Supervisorid = practice.Supervisorid; + prev.Studentid = practice.Studentid; prev.Finalgrade = practice.Finalgrade; - prev.Status = practice.Status; + prev.Status = string.IsNullOrEmpty(practice.Status) ? prev.Status : practice.Status; prev.Updateddate = DateTime.UtcNow; - prev.Type = practice.Type; + prev.Type = string.IsNullOrEmpty(practice.Type) ? prev.Type : practice.Type; await context.SaveChangesAsync(); return Results.Ok(); } diff --git a/Services/CoreService.Api/Core/Queries/StudentsQueries.cs b/Services/CoreService.Api/Core/Queries/StudentsQueries.cs index f94b797..5fa3120 100644 --- a/Services/CoreService.Api/Core/Queries/StudentsQueries.cs +++ b/Services/CoreService.Api/Core/Queries/StudentsQueries.cs @@ -2,9 +2,9 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Queries; +namespace CoreService.Api.Core.Queries; -using CoreService.Core.Models; +using CoreService.Api.Core.Models; using Microsoft.EntityFrameworkCore; /// @@ -26,7 +26,19 @@ public async Task> GetStudents(int? id = null) result = result.Where(student => student.Id == id); } - return await result.ToListAsync(); + return await result.Include(s => s.Group).Include(s => s.Practices).ToListAsync(); + } + + /// + /// Gets Student by UserId. + /// + /// User Id. + /// Student with selected user id. + public async Task GetStudentByUserId(string userId) + { + var result = await context.Students.FirstOrDefaultAsync(s => s.Userid == userId); + + return result; } /// @@ -34,11 +46,17 @@ public async Task> GetStudents(int? id = null) /// /// Input student. /// Response status. - public async Task InsertStudent(Student student) + public async Task InsertOrUpdateStudent(Student student) { + var prev = await context.Students.FindAsync(student.Id); + if (prev != null) + { + return await this.UpdateStudent(student); + } + context.Students.Add(student); await context.SaveChangesAsync(); - return student.Id; + return Results.Ok(student.Id); } /// @@ -56,6 +74,9 @@ public async Task UpdateStudent(Student student) return Results.BadRequest(); } + prev.FirstName = string.IsNullOrEmpty(student.FirstName) ? prev.FirstName : student.FirstName; + prev.LastName = string.IsNullOrEmpty(student.LastName) ? prev.LastName : student.LastName; + prev.MiddleName = string.IsNullOrEmpty(student.MiddleName) ? prev.MiddleName : student.MiddleName; prev.Groupid = student.Groupid; await context.SaveChangesAsync(); return Results.Ok(); diff --git a/Services/CoreService.Api/Core/Queries/ThemesQueries.cs b/Services/CoreService.Api/Core/Queries/ThemesQueries.cs index 05c6a4e..6fda9b1 100644 --- a/Services/CoreService.Api/Core/Queries/ThemesQueries.cs +++ b/Services/CoreService.Api/Core/Queries/ThemesQueries.cs @@ -2,10 +2,9 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService.Core.Queries; +namespace CoreService.Api.Core.Queries; -using System; -using CoreService.Core.Models; +using CoreService.Api.Core.Models; using Microsoft.EntityFrameworkCore; /// @@ -27,7 +26,7 @@ public async Task> GetThemes(int? id = null) result = result.Where(theme => theme.Id == id); } - return await result.ToListAsync(); + return await result.Include(theme => theme.Consultant).Include(theme => theme.Supervisor).ToListAsync(); } /// @@ -58,15 +57,18 @@ public async Task UpdateTheme(Theme theme) } prev.Updateddate = DateTime.Now; - prev.Description = theme.Description; - prev.Title = theme.Title; - prev.Suggestedby = theme.Suggestedby; - prev.Consultantid = theme.Consultantid; - prev.Department = theme.Department; + prev.Description = string.IsNullOrEmpty(theme.Description) ? prev.Description : theme.Description; + prev.Title = string.IsNullOrEmpty(theme.Title) ? prev.Title : theme.Title; + prev.Description = string.IsNullOrEmpty(theme.Description) ? prev.Description : theme.Description; + prev.Suggestedby = string.IsNullOrEmpty(theme.Suggestedby) ? prev.Suggestedby : theme.Suggestedby; + prev.Source = string.IsNullOrEmpty(theme.Source) ? prev.Source : theme.Source; + prev.Department = string.IsNullOrEmpty(theme.Department) ? prev.Department : theme.Department; + prev.Tags = string.IsNullOrEmpty(theme.Tags) ? prev.Tags : theme.Tags; + prev.Level = string.IsNullOrEmpty(theme.Level) ? prev.Level : theme.Level; + prev.Consultantid = theme.Consultantid ?? prev.Consultantid; + prev.Supervisorid = theme.Supervisorid ?? prev.Supervisorid; prev.Isarchived = theme.Isarchived; - prev.Tags = theme.Tags; - prev.Supervisorid = theme.Supervisorid; - prev.Level = theme.Level; + await context.SaveChangesAsync(); return Results.Ok(); } diff --git a/Services/CoreService.Api/CoreService.Api.csproj b/Services/CoreService.Api/CoreService.Api.csproj index 5aedbce..3eb6e9e 100644 --- a/Services/CoreService.Api/CoreService.Api.csproj +++ b/Services/CoreService.Api/CoreService.Api.csproj @@ -10,11 +10,19 @@ true - - - + + + + + + + + + + + diff --git a/Services/CoreService.Api/Endpoints/EndpointGroups.cs b/Services/CoreService.Api/Endpoints/EndpointGroups.cs index 5f22b6d..c8383e2 100644 --- a/Services/CoreService.Api/Endpoints/EndpointGroups.cs +++ b/Services/CoreService.Api/Endpoints/EndpointGroups.cs @@ -2,11 +2,13 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -namespace CoreService; +namespace CoreService.Api.Endpoints; -using CoreService.Core; -using CoreService.Core.Models; -using CoreService.Core.Queries; +using Contracts; +using CoreService.Api.Core; +using CoreService.Api.Core.Models; +using CoreService.Api.Core.Queries; +using MassTransit; /// /// Endpoints groups. @@ -28,14 +30,14 @@ public static RouteGroupBuilder ThemesGroup(this RouteGroupBuilder group) (int themeId, CoreContext context) => new ThemesQueries(context).GetThemes(themeId).Result); group.MapPost( "/", - (Theme theme, CoreContext context) => new ThemesQueries(context).InsertTheme(theme).Result); + (Theme theme, CoreContext context) => new ThemesQueries(context).InsertTheme(theme).Result).RequireAuthorization(); group.MapPut( "/", (Theme theme, CoreContext context) => - new ThemesQueries(context).UpdateTheme(theme).Result); + new ThemesQueries(context).UpdateTheme(theme).Result).RequireAuthorization(); group.MapDelete( "/{themeId:int}", - (int themeId, CoreContext context) => new ThemesQueries(context).DeleteTheme(themeId).Result); + (int themeId, CoreContext context) => new ThemesQueries(context).DeleteTheme(themeId).Result).RequireAuthorization(); return group; } @@ -55,14 +57,59 @@ public static RouteGroupBuilder ConsultantsGroup(this RouteGroupBuilder group) (int consultantId, CoreContext context) => new ConsultantsQueries(context).GetConsultants(consultantId).Result); group.MapPost( "/", - (Consultant consultant, CoreContext context) => new ConsultantsQueries(context).InsertConsultant(consultant).Result); + async (Consultant consultant, CoreContext context, IPublishEndpoint publishEndpoint) => + { + var result = await new ConsultantsQueries(context).InsertOrUpdateConsultant(consultant); + await publishEndpoint.Publish( + new UserWithRoleActionEvent( + consultant.Userid, + consultant.FirstName, + consultant.LastName, + consultant.MiddleName, + UserActionType.Update, + RoleNames.GetName(UserRoleType.Consultant), + DateTime.UtcNow)); + return result; + }).RequireAuthorization(); group.MapPut( "/", - (Consultant consultant, CoreContext context) => - new ConsultantsQueries(context).UpdateConsultant(consultant).Result); + async (Consultant consultant, CoreContext context, IPublishEndpoint publishEndpoint) => + { + var result = await new ConsultantsQueries(context).UpdateConsultant(consultant); + var prev = await context.Consultants.FindAsync(consultant.Id); + if (prev == null) + { + return Results.BadRequest(); + } + + await publishEndpoint.Publish( + new UserWithRoleActionEvent( + consultant.Userid, + string.IsNullOrEmpty(consultant.FirstName) ? prev.FirstName : consultant.FirstName, + string.IsNullOrEmpty(consultant.LastName) ? prev.LastName : consultant.LastName, + string.IsNullOrEmpty(consultant.MiddleName) ? prev.MiddleName : consultant.MiddleName, + UserActionType.Update, + RoleNames.GetName(UserRoleType.Consultant), + DateTime.UtcNow)); + return result; + }).RequireAuthorization(); group.MapDelete( "/{consultantId:int}", - (int consultantId, CoreContext context) => new ConsultantsQueries(context).DeleteConsultant(consultantId).Result); + async (int consultantId, CoreContext context, IPublishEndpoint publishEndpoint) => + { + var consultant = await context.Consultants.FindAsync(consultantId); + var result = await new ConsultantsQueries(context).DeleteConsultant(consultantId); + await publishEndpoint.Publish( + new UserWithRoleActionEvent( + consultant.Userid, + consultant.FirstName, + consultant.LastName, + consultant.MiddleName, + UserActionType.Update, + RoleNames.GetName(UserRoleType.Consultant), + DateTime.UtcNow)); + return result; + }).RequireAuthorization(); return group; } @@ -82,14 +129,14 @@ public static RouteGroupBuilder GroupsGroup(this RouteGroupBuilder group) (int groupId, CoreContext context) => new GroupsQueries(context).GetGroups(groupId).Result); group.MapPost( "/", - (Group group, CoreContext context) => new GroupsQueries(context).InsertGroup(group).Result); + (Group group, CoreContext context) => new GroupsQueries(context).InsertGroup(group).Result).RequireAuthorization(); group.MapPut( "/", (Group group, CoreContext context) => - new GroupsQueries(context).UpdateGroup(group).Result); + new GroupsQueries(context).UpdateGroup(group).Result).RequireAuthorization(); group.MapDelete( "/{groupId:int}", - (int groupId, CoreContext context) => new GroupsQueries(context).DeleteGroup(groupId).Result); + (int groupId, CoreContext context) => new GroupsQueries(context).DeleteGroup(groupId).Result).RequireAuthorization(); return group; } @@ -109,14 +156,65 @@ public static RouteGroupBuilder LecturersGroup(this RouteGroupBuilder group) (int lecturerId, CoreContext context) => new LecturersQueries(context).GetLecturers(lecturerId).Result); group.MapPost( "/", - (Lecturer lecturer, CoreContext context) => new LecturersQueries(context).InsertLecturer(lecturer).Result); + async (Lecturer lecturer, CoreContext context, IPublishEndpoint publishEndpoint) => + { + var result = await new LecturersQueries(context).InsertOrUpdateLecturer(lecturer); + await publishEndpoint.Publish( + new UserWithRoleActionEvent( + lecturer.Userid, + lecturer.FirstName, + lecturer.LastName, + lecturer.MiddleName, + UserActionType.Create, + RoleNames.GetName(UserRoleType.Supervisor), + DateTime.UtcNow)); + + return result; + }).RequireAuthorization("AdminOnly"); group.MapPut( "/", - (Lecturer lecturer, CoreContext context) => - new LecturersQueries(context).UpdateLecturer(lecturer).Result); + async (Lecturer lecturer, CoreContext context, IPublishEndpoint publishEndpoint) => + { + var result = await new LecturersQueries(context).UpdateLecturer(lecturer); + var prev = await context.Lecturers.FindAsync(lecturer.Id); + if (prev == null) + { + return Results.BadRequest(); + } + + await publishEndpoint.Publish( + new UserWithRoleActionEvent( + lecturer.Userid, + string.IsNullOrEmpty(lecturer.FirstName) ? prev.FirstName : lecturer.FirstName, + string.IsNullOrEmpty(lecturer.LastName) ? prev.LastName : lecturer.LastName, + string.IsNullOrEmpty(lecturer.MiddleName) ? prev.MiddleName : lecturer.MiddleName, + UserActionType.Update, + RoleNames.GetName(UserRoleType.Supervisor), + DateTime.UtcNow)); + return result; + }).RequireAuthorization("AdminOnly"); group.MapDelete( "/{lecturerId:int}", - (int lecturerId, CoreContext context) => new LecturersQueries(context).DeleteLecturer(lecturerId).Result); + async (int lecturerId, CoreContext context, IPublishEndpoint publishEndpoint) => + { + var lecturer = await context.Lecturers.FindAsync(lecturerId); + var result = await new LecturersQueries(context).DeleteLecturer(lecturerId); + + if (lecturer != null) + { + await publishEndpoint.Publish( + new UserWithRoleActionEvent( + lecturer.Userid, + lecturer.FirstName, + lecturer.LastName, + lecturer.MiddleName, + UserActionType.Delete, + RoleNames.GetName(UserRoleType.Supervisor), + DateTime.UtcNow)); + } + + return result; + }).RequireAuthorization("AdminOnly"); return group; } @@ -134,16 +232,19 @@ public static RouteGroupBuilder PracticesGroup(this RouteGroupBuilder group) group.MapGet( "/{practiceId:int}", (int practiceId, CoreContext context) => new PracticesQueries(context).GetPractices(practiceId).Result); + group.MapGet( + "/query", + (string userId, CoreContext context) => new PracticesQueries(context).GetQueriedPractices(userId).Result); group.MapPost( "/", - (Practice practice, CoreContext context) => new PracticesQueries(context).InsertPractice(practice).Result); + (Practice practice, CoreContext context) => new PracticesQueries(context).InsertPractice(practice).Result).RequireAuthorization(); group.MapPut( "/", (Practice practice, CoreContext context) => - new PracticesQueries(context).UpdatePractice(practice).Result); + new PracticesQueries(context).UpdatePractice(practice).Result).RequireAuthorization(); group.MapDelete( "/{practiceId:int}", - (int practiceId, CoreContext context) => new PracticesQueries(context).DeletePractice(practiceId).Result); + (int practiceId, CoreContext context) => new PracticesQueries(context).DeletePractice(practiceId).Result).RequireAuthorization(); return group; } @@ -161,16 +262,69 @@ public static RouteGroupBuilder StudentsGroup(this RouteGroupBuilder group) group.MapGet( "/{studentId:int}", (int studentId, CoreContext context) => new StudentsQueries(context).GetStudents(studentId).Result); + group.MapGet( + "/byUserId", + (string userId, CoreContext context) => new StudentsQueries(context).GetStudentByUserId(userId).Result); group.MapPost( "/", - (Student student, CoreContext context) => new StudentsQueries(context).InsertStudent(student).Result); + async (Student student, CoreContext context, IPublishEndpoint publishEndpoint) => + { + var result = await new StudentsQueries(context).InsertOrUpdateStudent(student); + await publishEndpoint.Publish( + new UserWithRoleActionEvent( + student.Userid, + student.FirstName, + student.LastName, + student.MiddleName, + UserActionType.Update, + RoleNames.GetName(UserRoleType.Student), + DateTime.UtcNow)); + return result; + }).RequireAuthorization(); group.MapPut( "/", - (Student student, CoreContext context) => - new StudentsQueries(context).UpdateStudent(student).Result); + async (Student student, CoreContext context, IPublishEndpoint publishEndpoint) => + { + var result = await new StudentsQueries(context).UpdateStudent(student); + var prev = await context.Students.FindAsync(student.Id); + if (prev == null) + { + return Results.BadRequest(); + } + + await publishEndpoint.Publish( + new UserWithRoleActionEvent( + student.Userid, + string.IsNullOrEmpty(student.FirstName) ? prev.FirstName : student.FirstName, + string.IsNullOrEmpty(student.LastName) ? prev.LastName : student.LastName, + string.IsNullOrEmpty(student.MiddleName) ? prev.MiddleName : student.MiddleName, + UserActionType.Update, + RoleNames.GetName(UserRoleType.Student), + DateTime.UtcNow)); + return result; + }).RequireAuthorization(); group.MapDelete( "/{studentId:int}", - (int studentId, CoreContext context) => new StudentsQueries(context).DeleteStudent(studentId).Result); + async (int studentId, CoreContext context, IPublishEndpoint publishEndpoint) => + { + var student = await context.Students.FindAsync(studentId); + var result = await new LecturersQueries(context).DeleteLecturer(studentId); + + if (student != null) + { + await publishEndpoint.Publish( + new UserWithRoleActionEvent( + student.Userid, + student.FirstName, + student.LastName, + student.MiddleName, + UserActionType.Update, + RoleNames.GetName(UserRoleType.Student), + DateTime.UtcNow)); + } + + return result; + }).RequireAuthorization(); return group; } diff --git a/Services/CoreService.Api/Program.cs b/Services/CoreService.Api/Program.cs index 6b5f04b..c256a00 100644 --- a/Services/CoreService.Api/Program.cs +++ b/Services/CoreService.Api/Program.cs @@ -2,17 +2,65 @@ // Copyright (c) Gleb Kargin. All rights reserved. // -using CoreService; -using CoreService.Core; +using System.Security.Claims; +using System.Text.Json.Serialization; +using Contracts; +using CoreService.Api.Consumers; +using CoreService.Api.Core; +using CoreService.Api.Endpoints; +using CoreService.Api.Services; +using MassTransit; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http.Json; using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; var builder = WebApplication.CreateBuilder(args); +builder.Services.Configure(options => +{ + options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; +}); + // Add services to the container. // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Enter JWT token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "Bearer", + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }, + }, + new List() + }, + }); +}); + +builder.Services.AddHttpClient("AuthService", client => +{ + client.BaseAddress = new Uri("http://auth.api:8080/"); +}); + +builder.Services.AddAuthentication("GatewayAuth") + .AddScheme("GatewayAuth", null); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AdminOnly", policy => policy.RequireRole("Администратор")); +}); // Current environment var currentEnvironment = Environment.GetEnvironmentVariable("ENVIRONMENT") ?? "Default"; @@ -21,8 +69,45 @@ builder.Services.AddDbContext( opt => opt.UseNpgsql(builder.Configuration.GetConnectionString(currentEnvironment))); +builder.Services.AddCors( + options => + { + options.AddPolicy( + "CorsPolicy", + policyBuilder => policyBuilder + .AllowAnyMethod() + .AllowCredentials() + .SetIsOriginAllowed((_) => true) + .AllowAnyHeader()); + }); + +builder.Services.AddMassTransit(x => +{ + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.Host(builder.Configuration["RabbitMQ:Host"], "/", h => + { + h.Username(builder.Configuration["RabbitMQ:Username"]); + h.Password(builder.Configuration["RabbitMQ:Password"]); + }); + + cfg.ReceiveEndpoint("user-events", e => + { + e.ConfigureConsumer(context); + }); + + cfg.Message(x => x.SetEntityName("user-with-role-events")); + }); +}); + +builder.Services.AddScoped(); + var app = builder.Build(); +app.UseCors("CorsPolicy"); + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { @@ -33,40 +118,6 @@ app.UseHttpsRedirection(); -// Mock user database -var mockUsers = new List -{ - new { Id = 1, Name = "John Doe", Email = "student@example.com", Roles = new[] { "Student" } }, - new { Id = 2, Name = "Jane Smith", Email = "lecturer@example.com", Roles = new[] { "Lecturer" } }, - new { Id = 3, Name = "Admin User", Email = "admin@example.com", Roles = new[] { "Admin", "Lecturer" } }, -}; - -// Endpoint to get all users -app.MapGet("/api/authmock/users", () => -{ - return Results.Ok(mockUsers); -}); - -// Endpoint to validate token and return user info -app.MapGet("/api/authmock/validate", (string token) => -{ - // Simulate token-to-user mapping (mocked) - var user = token switch - { - "token-student" => mockUsers[0], - "token-lecturer" => mockUsers[1], - "token-admin" => mockUsers[2], - _ => null, - }; - - if (user == null) - { - return Results.Unauthorized(); - } - - return Results.Ok(user); -}); - // Themes Endpoints app.MapGroup("api/themes/").ThemesGroup().WithTags("Themes"); @@ -85,4 +136,32 @@ // Students Endpoints app.MapGroup("api/students/").StudentsGroup().WithTags("Students"); +app.MapGet("api/me", async (HttpContext context, UserResolverService userResolver) => +{ + var user = context.User; + + var username = user.Identity?.Name ?? string.Empty; + var firstName = user.FindFirst(ClaimTypes.GivenName)?.Value ?? string.Empty; + var lastName = user.FindFirst(ClaimTypes.Surname)?.Value ?? string.Empty; + var middleName = user.FindFirst("middle_name")?.Value; // Custom claim + var email = user.FindFirst(ClaimTypes.Email)?.Value ?? string.Empty; + + var roles = user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray(); + + var userId = await userResolver.GetUserIdAsync(username); + return new UserDTO() + { + UserId = userId ?? string.Empty, + UserName = username, + FirstName = firstName, + LastName = lastName, + MiddleName = middleName, + Email = email, + Roles = roles, + }; +}).RequireAuthorization(); + +app.UseAuthentication(); +app.UseAuthorization(); + app.Run(); diff --git a/Services/CoreService.Api/Services/UserResolverService.cs b/Services/CoreService.Api/Services/UserResolverService.cs new file mode 100644 index 0000000..68c3625 --- /dev/null +++ b/Services/CoreService.Api/Services/UserResolverService.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace CoreService.Api.Services; + +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Contracts; + +/// +/// Service for resolving user information. +/// +public class UserResolverService +{ + private readonly IHttpClientFactory httpClientFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client factory. + public UserResolverService(IHttpClientFactory httpClientFactory) + { + this.httpClientFactory = httpClientFactory; + } + + /// + /// Gets user information by user ID. + /// + /// The user ID. + /// The user DTO or null if not found. + public async Task GetUserAsync(Guid userId) + { + var client = this.httpClientFactory.CreateClient("AuthService"); + var response = await client.GetAsync($"user?userId={userId}"); + + if (!response.IsSuccessStatusCode) + { + return null; + } + + return await response.Content.ReadFromJsonAsync(); + } + + /// + /// Gets user ID by username. + /// + /// The username. + /// The user ID as string or null if not found. + public async Task GetUserIdAsync(string username) + { + var client = this.httpClientFactory.CreateClient("AuthService"); + var response = await client.GetAsync($"userId?userName={username}"); + + if (!response.IsSuccessStatusCode) + { + return null; + } + + return await response.Content.ReadAsStringAsync(); + } +} \ No newline at end of file diff --git a/Services/CoreService.Api/appsettings.json b/Services/CoreService.Api/appsettings.json index 0987a41..dd1bd71 100644 --- a/Services/CoreService.Api/appsettings.json +++ b/Services/CoreService.Api/appsettings.json @@ -1,4 +1,10 @@ { + "RabbitMQ": { + "Host": "rabbitmq", + "Username": "admin", + "Password": "admin123", + "Port": 5672 + }, "ConnectionStrings": { "Default": "Host=localhost:5435;Database=postgres;Username=postgres;Password=postgres", "Docker": "Server=core.db;Database=postgres;Username=postgres;Password=postgres" diff --git a/Services/CoreService.Api/init-db/initial.sql b/Services/CoreService.Api/init-db/initial.sql index fec50bc..d086436 100644 --- a/Services/CoreService.Api/init-db/initial.sql +++ b/Services/CoreService.Api/init-db/initial.sql @@ -9,7 +9,10 @@ CREATE TABLE Groups CREATE TABLE Lecturers ( Id SERIAL PRIMARY KEY, - UserId UUID NOT NULL, -- + FirstName VARCHAR(100) NOT NULL, + LastName VARCHAR(100) NOT NULL, + MiddleName VARCHAR(100), + UserId VARCHAR(255) NOT NULL, Department VARCHAR(500), CanSuperviseVKR BOOLEAN NOT NULL DEFAULT FALSE ); @@ -17,17 +20,22 @@ CREATE TABLE Lecturers CREATE TABLE Students ( Id SERIAL PRIMARY KEY, - UserId UUID NOT NULL, -- - GroupId INT NOT NULL, + FirstName VARCHAR(100) NOT NULL, + LastName VARCHAR(100) NOT NULL, + MiddleName VARCHAR(100), + UserId VARCHAR(255) NOT NULL, + GroupId INT, CONSTRAINT Group_FK FOREIGN KEY (GroupId) REFERENCES Groups (Id) ); CREATE TABLE Consultants ( Id SERIAL PRIMARY KEY, - Name VARCHAR(255) NOT NULL, + FirstName VARCHAR(100) NOT NULL, + LastName VARCHAR(100) NOT NULL, + MiddleName VARCHAR(100), Contact VARCHAR(500) NOT NULL, - UserId UUID -- , + UserId VARCHAR(255) ); CREATE TABLE Themes @@ -39,9 +47,10 @@ CREATE TABLE Themes Level VARCHAR(255) NOT NULL, Department VARCHAR(500), IsArchived BOOLEAN NOT NULL DEFAULT FALSE, - SuggestedBy VARCHAR(500) NOT NULL, - ConsultantId INT NOT NULL, - SupervisorId INT NOT NULL, + SuggestedBy VARCHAR(100), + Source VARCHAR(500) NOT NULL, + ConsultantId INT, + SupervisorId INT, CreatedDate TIMESTAMP NOT NULL DEFAULT NOW(), UpdatedDate TIMESTAMP NOT NULL DEFAULT NOW(), CONSTRAINT Lecturer_FK FOREIGN KEY (SupervisorId) REFERENCES Lecturers (Id), @@ -52,6 +61,8 @@ CREATE TABLE Practices ( Id SERIAL PRIMARY KEY, StudentId INT NOT NULL, + ConsultantId INT NOT NULL, + SupervisorId INT, ThemeId INT NOT NULL, Type VARCHAR(255) NOT NULL, FinalGrade VARCHAR(5), @@ -61,5 +72,3 @@ CREATE TABLE Practices CONSTRAINT Student_FK FOREIGN KEY (StudentId) REFERENCES Students (Id), CONSTRAINT Theme_FK FOREIGN KEY (ThemeId) REFERENCES Themes (Id) ); - -INSERT INTO Lecturers (UserId, Department, CanSuperviseVKR) VALUES ('e5c49d19-89a1-4a67-8b7c-9b3c6b84e90d', 'Software Enginering', TRUE); diff --git a/Shared/Shared/RoleNames.cs b/Shared/Shared/RoleNames.cs new file mode 100644 index 0000000..94fcda5 --- /dev/null +++ b/Shared/Shared/RoleNames.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace Contracts; + +/// +/// Role names. +/// +public static class RoleNames +{ + private static readonly Dictionary RoleNamesValue = new() + { + { UserRoleType.Student, "Студент" }, + { UserRoleType.Supervisor, "Научный руководитель" }, + { UserRoleType.Consultant, "Консультант" }, + { UserRoleType.PracticeLeader, "Руководитель практики" }, + { UserRoleType.Reviewer, "Рецензент" }, + { UserRoleType.Administrator, "Администратор" }, + }; + + /// + /// Get all roles names. + /// + /// Array of role names. + public static string[] GetAllRoleNames() => RoleNamesValue.Values.ToArray(); + + /// + /// Get roel name. + /// + /// Role enum. + /// String name of role. + public static string GetName(UserRoleType role) + => RoleNamesValue.TryGetValue(role, out var name) ? name : role.ToString(); +} \ No newline at end of file diff --git a/Shared/Shared/Shared.csproj b/Shared/Shared/Shared.csproj new file mode 100644 index 0000000..904247a --- /dev/null +++ b/Shared/Shared/Shared.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + true + Contracts + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/Shared/Shared/UserActionType.cs b/Shared/Shared/UserActionType.cs new file mode 100644 index 0000000..55ede06 --- /dev/null +++ b/Shared/Shared/UserActionType.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace Contracts; + +/// +/// Enum for actions. +/// +public enum UserActionType +{ + /// + /// Enum for Create. + /// + Create = 1, + + /// + /// Enum for Update. + /// + Update = 2, + + /// + /// Enum for Delete. + /// + Delete = 3, +} \ No newline at end of file diff --git a/Shared/Shared/UserCreatedEvent.cs b/Shared/Shared/UserCreatedEvent.cs new file mode 100644 index 0000000..5f6a3b2 --- /dev/null +++ b/Shared/Shared/UserCreatedEvent.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace Contracts +{ + /// + /// Event for creating a User. + /// + /// User id. + /// Username. + /// First Name. + /// Last Name. + /// Middle Name. + /// Roles. + /// Date of creation. + public record UserCreatedEvent( + string UserId, + string Username, + string FirstName, + string LastName, + string? MiddleName, + string[] Roles, + DateTime CreatedAt); +} diff --git a/Shared/Shared/UserDTO.cs b/Shared/Shared/UserDTO.cs new file mode 100644 index 0000000..092f73e --- /dev/null +++ b/Shared/Shared/UserDTO.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace Contracts; + +using System.ComponentModel.DataAnnotations; + +/// +/// Application User DTO. +/// +public class UserDTO +{ + /// + /// Gets or sets User Id column. + /// + public string UserId { get; set; } = null!; + + /// + /// Gets or sets Email column. + /// + [MaxLength(100)] + public string Email { get; set; } = null!; + + /// + /// Gets or sets UserName column. + /// + [MaxLength(100)] + public string UserName { get; set; } = null!; + + /// + /// Gets or sets FirstName column. + /// + [MaxLength(100)] + public string FirstName { get; set; } = null!; + + /// + /// Gets or sets LastName column. + /// + [MaxLength(100)] + public string LastName { get; set; } = null!; + + /// + /// Gets or sets MiddleName column. + /// + [MaxLength(100)] + public string? MiddleName { get; set; } + + /// + /// Gets or sets Roles. + /// + public string[]? Roles { get; set; } +} \ No newline at end of file diff --git a/Shared/Shared/UserRoleType.cs b/Shared/Shared/UserRoleType.cs new file mode 100644 index 0000000..3b70f77 --- /dev/null +++ b/Shared/Shared/UserRoleType.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace Contracts; + +/// +/// User roles enum. +/// +public enum UserRoleType +{ + /// + /// Student enum. + /// + Student = 1, + + /// + /// Supervisor enum. + /// + Supervisor, + + /// + /// Consultant enum. + /// + Consultant, + + /// + /// Practice Leader enum. + /// + PracticeLeader, + + /// + /// Reviewer enum. + /// + Reviewer, + + /// + /// Administrator enum. + /// + Administrator, +} diff --git a/Shared/Shared/UserWithRoleActionEvent.cs b/Shared/Shared/UserWithRoleActionEvent.cs new file mode 100644 index 0000000..b2b2e2e --- /dev/null +++ b/Shared/Shared/UserWithRoleActionEvent.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +namespace Contracts; + +/// +/// Event for actioning with role user tables. +/// +/// User id. +/// First Name. +/// Last Name. +/// Middle Name. +/// Action. +/// Role. +/// Date of creation. +public record UserWithRoleActionEvent( + string UserId, + string FirstName, + string LastName, + string? MiddleName, + UserActionType Action, + string Role, + DateTime CreatedAt); \ No newline at end of file diff --git a/Tests/Tests/AuthServiceTests.cs b/Tests/Tests/AuthServiceTests.cs new file mode 100644 index 0000000..cf26686 --- /dev/null +++ b/Tests/Tests/AuthServiceTests.cs @@ -0,0 +1,195 @@ +// +// Copyright (c) Gleb Kargin. All rights reserved. +// + +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using AuthService.Api; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Testcontainers.PostgreSql; + +/// +/// Integration tests for . +/// +[TestFixture] +public class AuthServiceTests : IAsyncDisposable +{ + private PostgreSqlContainer postgresContainer; + private AuthDbContext dbContext; + private UserManager userManager; + private RoleManager roleManager; + + /// + /// Initializes the test environment before any tests run. + /// + /// A representing the asynchronous operation. + [OneTimeSetUp] + public async Task OneTimeSetUp() + { + this.postgresContainer = new PostgreSqlBuilder() + .WithImage("postgres:latest") + .WithDatabase("test_db") + .WithUsername("postgres") + .WithPassword("postgres") + .Build(); + + await this.postgresContainer.StartAsync(); + + var options = new DbContextOptionsBuilder() + .UseNpgsql(this.postgresContainer.GetConnectionString()) + .Options; + + this.dbContext = new AuthDbContext(options); + await this.dbContext.Database.EnsureCreatedAsync(); + + var store = new UserStore(this.dbContext); + + this.userManager = new UserManager( + store, + null, + new PasswordHasher(), + new List> { new UserValidator() }, + new List> { new PasswordValidator() }, + new UpperInvariantLookupNormalizer(), + new IdentityErrorDescriber(), + null, + null); + + var roleStore = new RoleStore(this.dbContext); + this.roleManager = new RoleManager( + roleStore, + new List> { new RoleValidator() }, + new UpperInvariantLookupNormalizer(), + new IdentityErrorDescriber(), + null); + } + + /// + /// Tests that a user can be successfully created in the database. + /// + /// A representing the asynchronous unit test. + [Test] + public async Task CanCreateUser() + { + var user = new ApplicationUser + { + FirstName = "John", + LastName = "Johnov", + UserName = "test@example.com", + Email = "test@example.com", + EmailConfirmed = true, + }; + + var result = await this.userManager.CreateAsync(user, "SecurePassword123!"); + Assert.That(result.Succeeded, Is.True); + var dbUser = await this.userManager.FindByEmailAsync("test@example.com"); + Assert.That(dbUser, Is.Not.Null); + } + + /// + /// Tests that a role can be successfully assigned to a user. + /// + /// A representing the asynchronous unit test. + [Test] + public async Task CanAddUserToRole() + { + var user = new ApplicationUser + { + FirstName = "John", + LastName = "Johnov", + UserName = "roleuser@example.com", + Email = "roleuser@example.com", + }; + + await this.userManager.CreateAsync(user, "SecurePassword123!"); + + const string RoleName = "TestRole"; + await this.roleManager.CreateAsync(new IdentityRole(RoleName)); + + var result = await this.userManager.AddToRoleAsync(user, RoleName); + Assert.That(result.Succeeded, Is.True); + var roles = await this.userManager.GetRolesAsync(user); + Assert.That(roles, Contains.Item(RoleName)); + } + + /// + /// Tests that a valid JWT token can be generated and contains expected claims. + /// + /// A representing the asynchronous unit test. + [Test] + public async Task CanGenerateAndValidateJwtToken() + { + var user = new ApplicationUser + { + FirstName = "John", + LastName = "Johnov", + UserName = "tokenuser@example.com", + Email = "tokenuser@example.com", + }; + + await this.userManager.CreateAsync(user, "SecurePassword123!"); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Jwt:Key"] = "YourTestKeyMustBeAtLeast128BitsLong", + ["Jwt:Issuer"] = "TestIssuer", + ["Jwt:Audience"] = "TestAudience", + ["Jwt:ExpireMinutes"] = "30", + }) + .Build(); + + var tokenService = new TokenService(configuration, this.dbContext, this.userManager); + var token = await tokenService.GenerateJwtToken(user); + var handler = new JwtSecurityTokenHandler(); + var jwtToken = handler.ReadJwtToken(token); + + Assert.That(jwtToken, Is.Not.Null); + Assert.That(jwtToken.Claims.Any(c => c.Type == ClaimTypes.Email && c.Value == user.Email), Is.True); + } + + /// + /// Tests that multiple roles can be assigned to a single user. + /// + /// A representing the asynchronous unit test. + [Test] + public async Task CanCreateUserWithMultipleRoles() + { + var user = new ApplicationUser + { + FirstName = "John", + LastName = "Johnov", + UserName = "multirole@example.com", + Email = "multirole@example.com", + }; + + await this.userManager.CreateAsync(user, "SecurePassword123!"); + + var roles = new[] { "Role1", "Role2", "Role3" }; + foreach (var role in roles) + { + await this.roleManager.CreateAsync(new IdentityRole(role)); + } + + foreach (var role in roles) + { + await this.userManager.AddToRoleAsync(user, role); + } + + var userRoles = await this.userManager.GetRolesAsync(user); + Assert.That(userRoles, Is.EquivalentTo(roles)); + } + + /// + /// Performs cleanup of test resources. + /// + /// A representing the asynchronous dispose operation. + public async ValueTask DisposeAsync() + { + await this.postgresContainer.DisposeAsync(); + await this.dbContext.DisposeAsync(); + } +} \ No newline at end of file diff --git a/Tests/Tests/Tests.csproj b/Tests/Tests/Tests.csproj new file mode 100644 index 0000000..8447bf7 --- /dev/null +++ b/Tests/Tests/Tests.csproj @@ -0,0 +1,45 @@ + + + + net9.0 + latest + enable + enable + false + true + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + diff --git a/docker-compose.dcproj b/docker-compose.dcproj index 03237ff..afc278b 100644 --- a/docker-compose.dcproj +++ b/docker-compose.dcproj @@ -13,4 +13,10 @@ + + + + + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 2524e0f..d58bd19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,17 +2,41 @@ version: "3.9" name: "practices-service" services: + rabbitmq: + image: rabbitmq:3.12-management-alpine + container_name: rabbitmq + ports: + - "5672:5672" + - "15672:15672" + environment: + - RABBITMQ_DEFAULT_USER=admin + - RABBITMQ_DEFAULT_PASS=admin123 + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "status"] + interval: 30s + timeout: 10s + retries: 5 + networks: + - proxybackend + gateway.api: image: ${DOCKER_REGISTRY-}gatewayapi environment: - ASPNETCORE_ENVIRONMENT=Development + - RabbitMQ__Host=rabbitmq + - RabbitMQ__Username=admin + - RabbitMQ__Password=admin123 build: context: . dockerfile: Gateway/Gateway.Api/Dockerfile + depends_on: + rabbitmq: + condition: service_healthy ports: - "5000:8080" networks: - proxybackend + core.db: container_name: core.db image: postgres:14.5-alpine @@ -37,6 +61,9 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Development - ENVIRONMENT=Docker + - RabbitMQ__Host=rabbitmq + - RabbitMQ__Username=admin + - RabbitMQ__Password=admin123 extra_hosts: - "host.docker.internal:host-gateway" build: @@ -46,11 +73,62 @@ services: ports: - "5001:8080" depends_on: + rabbitmq: + condition: service_healthy core.db: condition: service_healthy networks: - proxybackend + auth.db: + container_name: auth.db + image: postgres:14.5-alpine + volumes: + - ./Services/AuthService.Api/pg-data:/var/lib/postgresql/data + ports: + - "1435:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=postgres + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U postgres" ] + interval: 5s + timeout: 5s + retries: 5 + networks: + - proxybackend + + auth.api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ENVIRONMENT=Docker + - RabbitMQ__Host=rabbitmq + - RabbitMQ__Username=admin + - RabbitMQ__Password=admin123 + extra_hosts: + - "host.docker.internal:host-gateway" + build: + dockerfile: Services/AuthService.Api/Dockerfile + volumes: + - './:/auth' + ports: + - "5002:8080" + depends_on: + auth.db: + condition: service_healthy + rabbitmq: + condition: service_healthy + networks: + - proxybackend + + frontend: + build: + context: frontend + dockerfile: Dockerfile + ports: + - "8000:8000" + networks: proxybackend: name: proxybackend diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..dc0871a --- /dev/null +++ b/frontend/.env @@ -0,0 +1,2 @@ +PORT=8000 +REACT_APP_API_URL=http://localhost:5173/ \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..ada6dba --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,10 @@ +FROM node:lts-alpine + +WORKDIR /app + +COPY package.json . +RUN npm install --legacy-peer-deps + +COPY . . + +CMD [ "npm", "run", "dev" ] \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..40ede56 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,54 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config({ + extends: [ + // Remove ...tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config({ + plugins: { + // Add the react-x and react-dom plugins + 'react-x': reactX, + 'react-dom': reactDom, + }, + rules: { + // other rules... + // Enable its recommended typescript rules + ...reactX.configs['recommended-typescript'].rules, + ...reactDom.configs.recommended.rules, + }, +}) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9ab30cf --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Practices Service + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..8f2b69e --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5968 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.0.0", + "@mui/material": "^7.0.0", + "@uiw/react-md-editor": "^4.0.5", + "axios": "^1.8.2", + "bootstrap-icons": "^1.11.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.3.0" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/node": "^22.13.10", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^6.2.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", + "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "dependencies": { + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", + "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", + "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "node_modules/@emotion/styled": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.0.tgz", + "integrity": "sha512-RoV8Xs9eNwiDvhv7M+xcL4PWyRyIXRY/FLp3buU4h1EYfdF7unWUy3dOjPqb3C7rMUewIcqwW850PgS8h1o1yg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", + "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "dev": true, + "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" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", + "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.12.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.0.0.tgz", + "integrity": "sha512-/o5yrEV/8JtsuKoMDX3GHbYNDxOVS7Eq6FpHSJ2X6GDr9gCAXd8vaX7LHwYMm5vovf036PISolcyP81a6qvf0w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.0.0.tgz", + "integrity": "sha512-1coOyY9nmkEURD1WCtR2bE7hKz6GOC+JzTqVQTE/c+wD/5CdUDzcV876phmeWAdxbSV5NI4PNxNwkOAcAnxxjg==", + "dependencies": { + "@babel/runtime": "^7.26.10" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.0.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.0.0.tgz", + "integrity": "sha512-bcfHUIwoeh3IePsMbiwcvBUY2ju3bCazEyGuxGAnylIQ08V1mZUKEHH1x0lnlJxOR1rFKdH6EcR8S14px4skjQ==", + "dependencies": { + "@babel/runtime": "^7.26.10", + "@mui/core-downloads-tracker": "^7.0.0", + "@mui/system": "^7.0.0", + "@mui/types": "^7.4.0", + "@mui/utils": "^7.0.0", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.0.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.0.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==" + }, + "node_modules/@mui/private-theming": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.0.0.tgz", + "integrity": "sha512-I6iUTlpQEsJ7G2+88aLriyLUtTZp7a3p6l62OQtRo02PAQ4NznYzaN/ck1PQbcKwKdvPBpshdDdV3zdGioIiJQ==", + "dependencies": { + "@babel/runtime": "^7.26.10", + "@mui/utils": "^7.0.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.0.0.tgz", + "integrity": "sha512-Rm2q8FVo++rwgaMZil+0bJ6ZRY8Rm0UvvN3t/mXvUnyZA3+NqYMFBomS85LzriaEIY5hTSl9PE1z9l7Pox3aeA==", + "dependencies": { + "@babel/runtime": "^7.26.10", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.0.0.tgz", + "integrity": "sha512-fXUtOdgHRN/NLuv3kNCtqN4/IS7FXXRx7W45HU4FMpyq31JgcUPJpt7WBsU+Vvcn2lffk4YzavE4wc0Q3kUaiw==", + "dependencies": { + "@babel/runtime": "^7.26.10", + "@mui/private-theming": "^7.0.0", + "@mui/styled-engine": "^7.0.0", + "@mui/types": "^7.4.0", + "@mui/utils": "^7.0.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.0.tgz", + "integrity": "sha512-TxJ4ezEeedWHBjOmLtxI203a9DII9l4k83RXmz1PYSAmnyEcK2PglTNmJGxswC/wM5cdl9ap2h8lnXvt2swAGQ==", + "dependencies": { + "@babel/runtime": "^7.26.10" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.0.0.tgz", + "integrity": "sha512-oCRO9o08klpO13lZvPUt+ocmkyMlnAk76Eo8IIel6dcCBQQ0sTI5QNiSMzGC+JvusfPMGdvgIOVtHeyhRijJfQ==", + "dependencies": { + "@babel/runtime": "^7.26.10", + "@mui/types": "^7.4.0", + "@types/prop-types": "^15.7.14", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz", + "integrity": "sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.35.0.tgz", + "integrity": "sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.35.0.tgz", + "integrity": "sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.35.0.tgz", + "integrity": "sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.35.0.tgz", + "integrity": "sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.35.0.tgz", + "integrity": "sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.35.0.tgz", + "integrity": "sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.35.0.tgz", + "integrity": "sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.35.0.tgz", + "integrity": "sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.35.0.tgz", + "integrity": "sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.35.0.tgz", + "integrity": "sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.35.0.tgz", + "integrity": "sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.35.0.tgz", + "integrity": "sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.35.0.tgz", + "integrity": "sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.35.0.tgz", + "integrity": "sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.35.0.tgz", + "integrity": "sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.35.0.tgz", + "integrity": "sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.35.0.tgz", + "integrity": "sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz", + "integrity": "sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "node_modules/@types/node": { + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "dev": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" + }, + "node_modules/@types/react": { + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", + "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", + "integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/type-utils": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", + "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", + "integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz", + "integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", + "integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz", + "integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "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.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.1.tgz", + "integrity": "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz", + "integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.26.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@uiw/copy-to-clipboard": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.17.tgz", + "integrity": "sha512-O2GUHV90Iw2VrSLVLK0OmNIMdZ5fgEg4NhvtwINsX+eZ/Wf6DWD0TdsK9xwV7dNRnK/UI2mQtl0a2/kRgm1m1A==", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/react-markdown-preview": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@uiw/react-markdown-preview/-/react-markdown-preview-5.1.3.tgz", + "integrity": "sha512-jV02wO4XHWFk54kz7sLqOkdPgJLttSfKLyen47XgjcyGgQXU2I4WJBygmdpV2AT9m/MiQ8qrN1Y+E5Syv9ZDpw==", + "dependencies": { + "@babel/runtime": "^7.17.2", + "@uiw/copy-to-clipboard": "~1.0.12", + "react-markdown": "~9.0.1", + "rehype-attr": "~3.0.1", + "rehype-autolink-headings": "~7.1.0", + "rehype-ignore": "^2.0.0", + "rehype-prism-plus": "2.0.0", + "rehype-raw": "^7.0.0", + "rehype-rewrite": "~4.0.0", + "rehype-slug": "~6.0.0", + "remark-gfm": "~4.0.0", + "remark-github-blockquote-alert": "^1.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@uiw/react-md-editor": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@uiw/react-md-editor/-/react-md-editor-4.0.5.tgz", + "integrity": "sha512-x+S7ZMz1B+KwVODOZk663H2gdOZhcFsrPUF8V69K4L2BbTZ3A4sBCw/uTrBE9dHH0gajAzRAYbAYCHsUQaTcyA==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "@uiw/react-markdown-preview": "^5.0.6", + "rehype": "~13.0.0", + "rehype-prism-plus": "~2.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/bootstrap-icons": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz", + "integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ] + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001703", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz", + "integrity": "sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.1.1.tgz", + "integrity": "sha512-Y+DuvJ7JAjpL1f4DeILe5sXCC3kRXMl0DxM4lAWbS8/jEZ29o3V0L5TL6zIifj4Csmj6c+jiF2ENjida2OVOGA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", + "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.114", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz", + "integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", + "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.1.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.22.0", + "@eslint/plugin-kit": "^0.2.7", + "@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.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.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" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "dev": true, + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "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" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", + "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", + "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-markdown": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.3.tgz", + "integrity": "sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.3.0.tgz", + "integrity": "sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.3.0.tgz", + "integrity": "sha512-z7Q5FTiHGgQfEurX/FBinkOXhWREJIAB2RiU24lvcBa82PxUpwqvs/PAXb9lJyPjTs2jrl6UkLvCZVGJPeNuuQ==", + "dependencies": { + "react-router": "7.3.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/refractor": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-4.9.0.tgz", + "integrity": "sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^7.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/refractor/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/refractor/node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/refractor/node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/refractor/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-attr": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/rehype-attr/-/rehype-attr-3.0.3.tgz", + "integrity": "sha512-Up50Xfra8tyxnkJdCzLBIBtxOcB2M1xdeKe1324U06RAvSjYm7ULSeoM+b/nYPQPVd7jsXJ9+39IG1WAJPXONw==", + "dependencies": { + "unified": "~11.0.0", + "unist-util-visit": "~5.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/rehype-autolink-headings": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-ignore": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/rehype-ignore/-/rehype-ignore-2.0.2.tgz", + "integrity": "sha512-BpAT/3lU9DMJ2siYVD/dSR0A/zQgD6Fb+fxkJd4j+wDVy6TYbYpK+FZqu8eM9EuNKGvi4BJR7XTZ/+zF02Dq8w==", + "dependencies": { + "hast-util-select": "^6.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-prism-plus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rehype-prism-plus/-/rehype-prism-plus-2.0.0.tgz", + "integrity": "sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ==", + "dependencies": { + "hast-util-to-string": "^3.0.0", + "parse-numeric-range": "^1.3.0", + "refractor": "^4.8.0", + "rehype-parse": "^9.0.0", + "unist-util-filter": "^5.0.0", + "unist-util-visit": "^5.0.0" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-rewrite": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-4.0.2.tgz", + "integrity": "sha512-rjLJ3z6fIV11phwCqHp/KRo8xuUCO8o9bFJCNw5o6O2wlLk6g8r323aRswdGBQwfXPFYeSuZdAjp4tzo6RGqEg==", + "dependencies": { + "hast-util-select": "^6.0.0", + "unified": "^11.0.3", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-github-blockquote-alert": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/remark-github-blockquote-alert/-/remark-github-blockquote-alert-1.3.0.tgz", + "integrity": "sha512-cwkBA4x+VH4J2VAMzhbmSeAmK5tBd5iwesgSUUQuRtuQ48XQm6sXXNLY9PR7ohZmZiqMeoDMUGCTur5zwR4lTQ==", + "dependencies": { + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.35.0.tgz", + "integrity": "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.35.0", + "@rollup/rollup-android-arm64": "4.35.0", + "@rollup/rollup-darwin-arm64": "4.35.0", + "@rollup/rollup-darwin-x64": "4.35.0", + "@rollup/rollup-freebsd-arm64": "4.35.0", + "@rollup/rollup-freebsd-x64": "4.35.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", + "@rollup/rollup-linux-arm-musleabihf": "4.35.0", + "@rollup/rollup-linux-arm64-gnu": "4.35.0", + "@rollup/rollup-linux-arm64-musl": "4.35.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", + "@rollup/rollup-linux-riscv64-gnu": "4.35.0", + "@rollup/rollup-linux-s390x-gnu": "4.35.0", + "@rollup/rollup-linux-x64-gnu": "4.35.0", + "@rollup/rollup-linux-x64-musl": "4.35.0", + "@rollup/rollup-win32-arm64-msvc": "4.35.0", + "@rollup/rollup-win32-ia32-msvc": "4.35.0", + "@rollup/rollup-win32-x64-msvc": "4.35.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", + "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", + "dependencies": { + "style-to-object": "1.0.8" + } + }, + "node_modules/style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.26.1.tgz", + "integrity": "sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.26.1", + "@typescript-eslint/parser": "8.26.1", + "@typescript-eslint/utils": "8.26.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-filter": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-5.0.1.tgz", + "integrity": "sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz", + "integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..6588d8d --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.0.0", + "@mui/material": "^7.0.0", + "@uiw/react-md-editor": "^4.0.5", + "axios": "^1.8.2", + "bootstrap-icons": "^1.11.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.3.0" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/node": "^22.13.10", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^6.2.0" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/main.tsx b/frontend/src/app/main.tsx new file mode 100644 index 0000000..bee20d8 --- /dev/null +++ b/frontend/src/app/main.tsx @@ -0,0 +1,8 @@ +import ReactDOM from 'react-dom/client' +import '../index.css' +import {RouterProvider} from 'react-router-dom' +import {routes} from "./routes/routes"; + +ReactDOM.createRoot(document.getElementById('root')!).render( + , +) \ No newline at end of file diff --git a/frontend/src/app/routes/routes.tsx b/frontend/src/app/routes/routes.tsx new file mode 100644 index 0000000..1188141 --- /dev/null +++ b/frontend/src/app/routes/routes.tsx @@ -0,0 +1,66 @@ +import {createBrowserRouter} from "react-router-dom"; +import {LoginPage} from "@pages/LoginPage.tsx"; +import {ThemesIndexPage} from "@pages/Themes/ThemesIndexPage.tsx"; +import {ThemePage} from "@pages/Themes/ThemePage.tsx"; +import {CreateThemePage} from "@pages/Themes/CreateThemePage.tsx"; +import {EditThemePage} from "@pages/Themes/EditThemePage.tsx"; +import {ProfilePage} from "@pages/ProfilePage.tsx"; +import {BasePage} from "@pages/BasePage.tsx"; +import {PracticesIndexPage} from "@pages/Practices/PracticesIndexPage.tsx"; +import {CreatePracticePage} from "@pages/Practices/CreatePracticePage.tsx"; +import {AdminBasePage} from "@pages/Admin/AdminBasePage"; +import {AdminUsersPage} from "@pages/Admin/AdminUsersPage"; +import { RegisterPage } from "@/pages/RegisterPage"; + +export const routes = createBrowserRouter([ + { + path: "/", + element: , + }, + { + path: "/themes", + element: , + }, + { + path: "/practices", + element: , + }, + { + path: "/create/practice", + element: , + }, + { + path: "/login", + element: , + }, + { + path: "/register", + element: , + }, + { + path: "/theme/:id", + element: , + }, + { + path: "/create/theme", + element: , + }, + { + path: "/edit/theme/:id", + element: , + }, + { + path: "/profile", + element: , + }, + { + path: "/admin", + element: , + children: [ + { + path: "users", + element: , + }, + ] + }, +]); diff --git a/frontend/src/entities/Consultant.ts b/frontend/src/entities/Consultant.ts new file mode 100644 index 0000000..75e689c --- /dev/null +++ b/frontend/src/entities/Consultant.ts @@ -0,0 +1,7 @@ +export interface Consultant { + id: number; + firstName: string; + lastName: string; + middleName: string; + contact: string; +} \ No newline at end of file diff --git a/frontend/src/entities/Group.ts b/frontend/src/entities/Group.ts new file mode 100644 index 0000000..a764fb7 --- /dev/null +++ b/frontend/src/entities/Group.ts @@ -0,0 +1,6 @@ +export interface Group { + id: number; + name: string; + program: string; + year: number; +} \ No newline at end of file diff --git a/frontend/src/entities/Lecturer.ts b/frontend/src/entities/Lecturer.ts new file mode 100644 index 0000000..4987a49 --- /dev/null +++ b/frontend/src/entities/Lecturer.ts @@ -0,0 +1,7 @@ +export interface Lecturer { + id: number; + firstName: string; + lastName: string; + middleName: string; + department: string; +} \ No newline at end of file diff --git a/frontend/src/entities/LoginResponse.ts b/frontend/src/entities/LoginResponse.ts new file mode 100644 index 0000000..029ce57 --- /dev/null +++ b/frontend/src/entities/LoginResponse.ts @@ -0,0 +1,4 @@ +export interface LoginResponse { + token: string; + refreshToken: string; +} \ No newline at end of file diff --git a/frontend/src/entities/Practice.ts b/frontend/src/entities/Practice.ts new file mode 100644 index 0000000..5f2f526 --- /dev/null +++ b/frontend/src/entities/Practice.ts @@ -0,0 +1,23 @@ +import { Student } from "./Student.ts"; +import { Theme } from "./Theme.ts"; + +export interface Practice { + id: number; + studentid: number; + themeid: 0; + type: string; + finalgrade: string; + status: string; + createddate: Date; + updateddate: Date; + student: Student; + theme: Theme; +} + +export interface InputPractice { + studentid: number; + themeid: number; + type: string; + finalgrade: string; + status: string; +} \ No newline at end of file diff --git a/frontend/src/entities/RegisterData.ts b/frontend/src/entities/RegisterData.ts new file mode 100644 index 0000000..ccc39d9 --- /dev/null +++ b/frontend/src/entities/RegisterData.ts @@ -0,0 +1,8 @@ +interface RegisterData { + email: string; + password: string; + firstName: string; + lastName: string; + middleName?: string; + roles?: string[]; +} \ No newline at end of file diff --git a/frontend/src/entities/Student.ts b/frontend/src/entities/Student.ts new file mode 100644 index 0000000..9953e5c --- /dev/null +++ b/frontend/src/entities/Student.ts @@ -0,0 +1,10 @@ +import {Group} from "./Group.ts"; + +export interface Student { + id: number; + firstName: string; + lastName: string; + middleName: string; + groupId: number; + group: Group +} \ No newline at end of file diff --git a/frontend/src/entities/Theme.ts b/frontend/src/entities/Theme.ts new file mode 100644 index 0000000..88a2f04 --- /dev/null +++ b/frontend/src/entities/Theme.ts @@ -0,0 +1,32 @@ +import {Lecturer} from "./Lecturer.ts"; +import {Consultant} from "./Consultant.ts"; + +export interface Theme { + id: number; + title: string; + description: string; + tags: string; + level: string; + department: string; + isarchived: boolean; + suggestedby: string; + source: string; + consultantid: number; + supervisorid: number; + createddate: Date; + updateddate: Date; + consultant: Consultant; + supervisor: Lecturer; +} + +export interface InputTheme { + title: string; + description: string; + tags: string; + level: string; + department: string; + isarchived: boolean; + suggestedby: string; + consultantid: number; + supervisorid: number; +} \ No newline at end of file diff --git a/frontend/src/entities/User.ts b/frontend/src/entities/User.ts new file mode 100644 index 0000000..887e8e8 --- /dev/null +++ b/frontend/src/entities/User.ts @@ -0,0 +1,11 @@ +import {UserRole} from "./UserRoles"; + +export interface User { + userId: string; + email: string; + userName: string; + firstName: string; + lastName: string; + middleName: string; + roles: UserRole[]; +} \ No newline at end of file diff --git a/frontend/src/entities/UserRoles.ts b/frontend/src/entities/UserRoles.ts new file mode 100644 index 0000000..e814637 --- /dev/null +++ b/frontend/src/entities/UserRoles.ts @@ -0,0 +1,8 @@ +export enum UserRole { + ADMIN = 'Администратор', + STUDENT = 'Студент', + SUPERVISOR = 'Научный руководитель', + CONSULTANT = 'Консультант', + REVIEWER = 'Рецензент', + PRACTICE_SUPERVISOR = 'Руководитель практики', +} \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..fb92a9f --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,66 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/frontend/src/pages/Admin/AdminBasePage.tsx b/frontend/src/pages/Admin/AdminBasePage.tsx new file mode 100644 index 0000000..d39f8e0 --- /dev/null +++ b/frontend/src/pages/Admin/AdminBasePage.tsx @@ -0,0 +1,80 @@ +import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import { + AppBar, + Box, + Button, + Container, + CssBaseline, + Tab, + Tabs, + Toolbar, + Typography +} from "@mui/material"; +import { useEffect, useState } from "react"; +import { getMe } from "@/shared/services/axios.service"; +import { User } from "@/entities/User"; +import {UserRole} from "../../entities/UserRoles"; + +const tabRoutes = [ + { label: "Панель", path: "/admin" }, + { label: "Пользователи", path: "/admin/users" }, + { label: "Консультанты", path: "/admin/" }, + { label: "Преподаватели", path: "/admin/teachers" }, + { label: "Студенты", path: "/admin/students" }, + { label: "Темы", path: "/admin/themes" }, + { label: "Практики", path: "/admin/practices" } +]; + +export function AdminBasePage() { + const navigate = useNavigate(); + const location = useLocation(); + const [selectedTab, setSelectedTab] = useState(0); + + useEffect(() => { + getMe().then(res => { + const user: User = res.data; + if (!user.roles.includes(UserRole.ADMIN)) { + navigate("/"); + } + }); + }, [navigate]); + + useEffect(() => { + const currentIndex = tabRoutes.findIndex(tab => location.pathname.startsWith(tab.path)); + if (currentIndex !== -1) setSelectedTab(currentIndex); + }, [location.pathname]); + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setSelectedTab(newValue); + navigate(tabRoutes[newValue].path); + }; + + return ( + + + + + + + Панель администратора + + + + + + + {tabRoutes.map((tab, index) => ( + + ))} + + + + + + + + + ); +} diff --git a/frontend/src/pages/Admin/AdminUsersPage.tsx b/frontend/src/pages/Admin/AdminUsersPage.tsx new file mode 100644 index 0000000..0e4d1dd --- /dev/null +++ b/frontend/src/pages/Admin/AdminUsersPage.tsx @@ -0,0 +1,58 @@ +import React, { useEffect, useState } from "react"; +import { Container, Typography, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, CircularProgress } from "@mui/material"; +import { getAllUsers } from "@/shared/services/axios.service"; +import { User } from "@/entities/User"; + +export function AdminUsersPage() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + getAllUsers() + .then(res => { + setUsers(res.data); + }) + .finally(() => setLoading(false)); + }, []); + + return ( + + + Пользователи + + + {loading ? ( + + ) : ( + + + + + ID + Фамилия + Имя + Отчество + Username + Email + Роли + + + + {users.map((user) => ( + + {user.userId} + {user.lastName} + {user.firstName} + {user.middleName} + {user.userName} + {user.email} + {user.roles.join(", ")} + + ))} + +
+
+ )} +
+ ); +} diff --git a/frontend/src/pages/BasePage.tsx b/frontend/src/pages/BasePage.tsx new file mode 100644 index 0000000..9d5ea2c --- /dev/null +++ b/frontend/src/pages/BasePage.tsx @@ -0,0 +1,61 @@ +import { Layout } from "@shared/ui/layout/Layout.tsx"; +import { + Container, + Typography, + Box, + useTheme, + Fade +} from "@mui/material"; + +export function BasePage() { + const theme = useTheme(); + + return ( + + + + + + Добро пожаловать + + + + + + Сервис для работы с учебными практиками + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx new file mode 100644 index 0000000..a46bc21 --- /dev/null +++ b/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,99 @@ +import { Layout } from "@shared/ui/layout/Layout.tsx"; +import { ChangeEvent, FormEvent, useState } from "react"; +import { login } from "@shared/services/axios.service.ts"; +import { LoginResponse } from "@entities/LoginResponse.ts"; +import { setJWTToken, setRefreshToken } from "@shared/services/localStorage.service.ts"; +import { + Container, + TextField, + Button, + Typography, + Paper, + Box +} from "@mui/material"; +import { useNavigate } from "react-router-dom"; + +// Page for login +export function LoginPage() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const navigate = useNavigate(); + + const onChangeLogin = (event: ChangeEvent) => { + setEmail(event.target.value); + }; + + const onChangePassword = (event: ChangeEvent) => { + setPassword(event.target.value); + }; + + const onSubmit = (event: FormEvent) => { + event.preventDefault(); + login(email, password) + .then(response => { + const loginResponse: LoginResponse = response.data; + setJWTToken(loginResponse.token); + setRefreshToken(loginResponse.refreshToken); + window.location.assign("/"); + }) + .catch(e => { + console.log(e); + alert("Не удалось войти"); + }); + }; + + return ( + + + + + Вход + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/pages/Practices/CreatePracticePage.tsx b/frontend/src/pages/Practices/CreatePracticePage.tsx new file mode 100644 index 0000000..5eec871 --- /dev/null +++ b/frontend/src/pages/Practices/CreatePracticePage.tsx @@ -0,0 +1,176 @@ +import { useEffect, useState } from "react"; +import { + Container, + TextField, + Button, + MenuItem, + Typography, + Grid, + Paper, + FormControl, + InputLabel, + Select, Stack, FormControlLabel, Checkbox +} from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import { getThemes, getMe, postPractice, getStudentByUserId, getLecturers, getConsultants } from "@/shared/services/axios.service"; +import { Theme } from "@/entities/Theme"; +import { User } from "@/entities/User"; +import { InputPractice } from "@/entities/Practice"; +import {Student} from "../../entities/Student"; +import {Layout} from "@shared/ui/layout/Layout.tsx"; +import MDEditor from "@uiw/react-md-editor"; +import { Consultant } from "@/entities/Consultant"; +import { Lecturer } from "@/entities/Lecturer"; +import {UserRole} from "../../entities/UserRoles"; + +export function CreatePracticePage() { + const [type, setType] = useState(""); + const [themeId, setThemeId] = useState(""); + const [themes, setThemes] = useState([]); + const [consultantId, setConsultantId] = useState(""); + const [consultants, setConsultants] = useState([]); + const [supervisorId, setSupervisorId] = useState(""); + const [lecturers, setLecturers] = useState([]); + const [me, setMe] = useState(null); + const [currentStudent, setCurrentStudent] = useState(null); + + const navigate = useNavigate(); + + useEffect(() => { + getThemes().then(res => setThemes(res.data)); + getLecturers().then(res => setLecturers(res.data)); + getConsultants().then(res => setConsultants(res.data)); + getMe().then(res => { + const meData: User = res.data; + setMe(res.data); + + getStudentByUserId(meData.userId).then(r => setCurrentStudent(r.data)) + }); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!me || themeId === "" || !type) { + alert("Пожалуйста, заполните все обязательные поля."); + return; + } + + try { + const practice: InputPractice = { + studentid: currentStudent.id, + themeid: themeId, + type, + finalgrade: "", + status: "Активна", + supervisorid: supervisorId, + consultantid: consultantId + }; + + await postPractice(practice); + navigate("/practices"); + } catch (error) { + console.error("Ошибка при создании практики:", error); + alert("Не удалось создать практику. Попробуйте позже."); + } + }; + + return ( + + + + + + + Создание новой практики + + +
+ + + Ваша группа + {currentStudent?.group.name} + + + + + Тип практики + + + + + + + Тема + + + + + + + {UserRole.SUPERVISOR} + + + + + + + {UserRole.CONSULTANT} + + + + + + + + +
+
+
+
+ ); +} diff --git a/frontend/src/pages/Practices/HandbookTab.tsx b/frontend/src/pages/Practices/HandbookTab.tsx new file mode 100644 index 0000000..a68a93d --- /dev/null +++ b/frontend/src/pages/Practices/HandbookTab.tsx @@ -0,0 +1,128 @@ +import { Paper, Typography, List, ListItem, ListItemText, Link, Box } from "@mui/material"; + +export function HandbookTab() { + return ( + + + Порядок работы над учебной практикой + + + + + + + + + + + + + + + • Погружение в предметную область + • Обзор аналогов и используемых инструментов + • Проектирование и реализация + • Апробация + • Написание текста, подготовка к защите + + } + /> + + + + + + + + + + + + + + Полезные материалы: + + + + + + + Подробный гайд по учебным практикам (рекомендуем к прочтению!) + + + + + Шаблоны: + + + отчёта + + + презентации + + + отзывов: + + + научника + + + консультанта + + + + + акта о внедрении + + + + Примеры курсовых работ: + + + + весенняя практика 3 курса с 2020 по 2021 год + + + + + магистерские ВКР с 2016 по 2018 год + + + + + и многие другие... + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/Practices/PracticeCard.tsx b/frontend/src/pages/Practices/PracticeCard.tsx new file mode 100644 index 0000000..e7f5063 --- /dev/null +++ b/frontend/src/pages/Practices/PracticeCard.tsx @@ -0,0 +1,51 @@ +import { Card, CardContent, Typography, Chip, Divider, Box } from "@mui/material"; +import { Practice } from "@/entities/Practice"; + +interface PracticeCardProps { + practice: Practice; + onClick: () => void; +} + +export function PracticeCard({ practice, onClick }: PracticeCardProps) { + return ( + + + + + {practice.theme?.title || "Без темы"} + + + + + + + + Тип практики: {practice.type} + + + + Итоговая оценка: {practice.finalgrade || "Не указана"} + + + + Дата создания: {new Date(practice.createddate).toLocaleDateString()} + + + + Последнее обновление: {new Date(practice.updateddate).toLocaleDateString()} + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/Practices/PracticesIndexPage.tsx b/frontend/src/pages/Practices/PracticesIndexPage.tsx new file mode 100644 index 0000000..0ed4b77 --- /dev/null +++ b/frontend/src/pages/Practices/PracticesIndexPage.tsx @@ -0,0 +1,125 @@ +import {Layout} from "@shared/ui/layout/Layout.tsx"; +import {getJWTToken} from "@/shared/services/localStorage.service.ts"; +import {Navigate, useNavigate} from "react-router-dom"; +import {useEffect, useState} from "react"; +import {getUserPractices} from "@/shared/services/axios.service.ts"; +import {Practice} from "@/entities/Practice.ts"; +import { + Container, + Grid, + Card, + CardContent, + Typography, + Button, + Tabs, + Tab, + Box, + Paper, + Chip, + Divider, + List, + ListItem, + ListItemText, + Link +} from "@mui/material"; +import AddIcon from '@mui/icons-material/Add'; +import { User } from "@/entities/User.ts"; +import { getMe } from "@/shared/services/axios.service.ts"; +import { HandbookTab } from "./HandbookTab"; +import { PracticeCard } from "./PracticeCard"; + +export function PracticesIndexPage() { + const tokenIsEmpty = getJWTToken() === ""; + const [practices, setPractices] = useState([]); + const [me, setMe] = useState(); + const [activeTab, setActiveTab] = useState(2); + const navigate = useNavigate(); + + useEffect(() => { + getMe().then(response => { + const data: User = response.data; + setMe(data); + + getUserPractices(data.userId).then(response => { + setPractices(response.data); + }); + }); + }, []); + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setActiveTab(newValue); + }; + + const activePractices = practices.filter(practice => practice.status != 'Завершено'); + const completedPractices = practices.filter(practice => practice.status == 'Завершено'); + + return tokenIsEmpty ? : ( + + + + Мои практики + + + + + + + + + + + + + + + + {activeTab === 0 ? ( + activePractices.length > 0 ? ( + activePractices.map((practice, index) => ( + + navigate(`/practices/${practice.id}`)} + /> + + )) + ) : ( + + + Нет активных практик + + + ) + ) : activeTab === 1 ? ( + completedPractices.length > 0 ? ( + completedPractices.map((practice, index) => ( + + navigate(`/practices/${practice.id}`)} + /> + + )) + ) : ( + + + Нет завершенных практик + + + ) + ) : ( + + + + )} + + + + ); +} diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx new file mode 100644 index 0000000..a2e191a --- /dev/null +++ b/frontend/src/pages/ProfilePage.tsx @@ -0,0 +1,115 @@ +import { Layout } from "@shared/ui/layout/Layout.tsx"; +import { getJWTToken } from "../shared/services/localStorage.service.ts"; +import { Navigate } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { getMe } from "../shared/services/axios.service.ts"; +import { + Container, + Paper, + Typography, + Avatar, + Box, + Button, + Grid +} from "@mui/material"; +import EditIcon from '@mui/icons-material/Edit'; +import { User } from "@/entities/User.ts"; +import {useNavigate} from "react-router-dom"; + +export function ProfilePage() { + const navigate = useNavigate(); + const tokenIsEmpty = getJWTToken() === ""; + const [user, setUser] = useState(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + getMe().then(response => { + const data: User = response.data + setUser(data); + setLoading(false); + }).catch(() => { + setLoading(false); + }); + }, []); + + if (tokenIsEmpty) { + return ; + } + + if (loading) { + return ( + + + Загрузка... + + + ); + } + + const fullName = `${user.lastName} ${user.firstName} ${user.middleName}`.trim(); + + return ( + + + + + {/**/} + {/* Профиль пользователя*/} + {/* }*/} + {/* onClick={() => /!* Add edit functionality *!/}*/} + {/* >*/} + {/* Редактировать*/} + {/* */} + {/**/} + + + + + + + + {fullName || "Не указано"} + + + {user.userName} + + + + + + + + Email + + + {user.email || "Не указан"} + + + + + + + + Роли + + + {user.roles.join(", ") || "Не указан"} + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/RegisterPage.tsx b/frontend/src/pages/RegisterPage.tsx new file mode 100644 index 0000000..07cc902 --- /dev/null +++ b/frontend/src/pages/RegisterPage.tsx @@ -0,0 +1,158 @@ +import { Layout } from "@shared/ui/layout/Layout.tsx"; +import { ChangeEvent, FormEvent, useState } from "react"; +import { register } from "@shared/services/axios.service.ts"; // You'll need to create this function +import { setJWTToken, setRefreshToken } from "@shared/services/localStorage.service.ts"; +import { + Container, + TextField, + Button, + Typography, + Paper, + Box, + Grid +} from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import {UserRole} from "../entities/UserRoles"; + +export function RegisterPage() { + const [formData, setFormData] = useState({ + email: "", + password: "", + firstName: "", + lastName: "", + middleName: "" + }); + + const navigate = useNavigate(); + + const handleChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const onSubmit = (event: FormEvent) => { + event.preventDefault(); + + if (!formData.email || !formData.password || !formData.firstName || !formData.lastName) { + alert("Пожалуйста, заполните все обязательные поля"); + return; + } + + formData.roles = [UserRole.STUDENT]; + register(formData) + .then(response => { + setJWTToken(response.data.token); + setRefreshToken(response.data.refreshToken); + window.location.assign("/"); + }) + .catch(e => { + console.error("Registration error:", e); + alert("Не удалось зарегистрироваться: " + (e.response?.data?.message || e.message)); + }); + }; + + return ( + + + + + Регистрация + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/Themes/CreateThemePage.tsx b/frontend/src/pages/Themes/CreateThemePage.tsx new file mode 100644 index 0000000..83edeac --- /dev/null +++ b/frontend/src/pages/Themes/CreateThemePage.tsx @@ -0,0 +1,271 @@ +import {Layout} from "@shared/ui/layout/Layout.tsx"; +import {useEffect, useMemo, useState} from "react"; +import { + Container, + TextField, + Button, + Grid, + Paper, + Typography, + Checkbox, + FormControlLabel, + Select, + MenuItem, + InputLabel, + FormControl, + Stack +} from "@mui/material"; +import {useNavigate} from "react-router-dom"; +import {InputTheme, Theme} from "@/entities/Theme.ts"; +import {getConsultants, getLecturers, getMe, getThemes, postTheme} from "@/shared/services/axios.service.ts"; +import MDEditor from '@uiw/react-md-editor'; +import {Lecturer} from "@/entities/Lecturer.ts"; +import {Consultant} from "@/entities/Consultant.ts"; +import { User } from "@/entities/User.ts"; +import {UserRole} from "../../entities/UserRoles"; + +export function CreateThemePage() { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [levels, setLevels] = useState({ + secondCourse: false, + thirdCourse: false, + bachelor: false, + master: false + }); + const [lecturerId, setLecturerId] = useState(); + const [consultantId, setConsultantId] = useState(); + const departments = useMemo(() => ["Кафедра системного программирования", "Кафедра параллельных алгоритмов", + "Кафедра информатики", "Кафедра информационно-аналитических систем"], []) + const [department, setDepartment] = useState(); + const [source, setSource] = useState("") + const [me, setMe] = useState() + const navigate = useNavigate(); + const [lecturers, setLecturers] = useState(); + const [consultants, setConsultants] = useState(); + + const [isMdTouched, setIsMdTouched] = useState(false); + + const isMdError = isMdTouched && !description; + + useEffect(() => { + getThemes().then(response => { + const themes: Theme[] = response.data; + }); + + getLecturers().then(response => { + setLecturers(response.data); + }); + + getConsultants().then(response => { + setConsultants(response.data); + }); + + getMe().then(response => { + const data: User = response.data + setMe(data); + setSource(`${data.lastName} ${data.firstName} ${data.middleName}`); + }) + }, []); + + const transformLevelsToString = (levels: { + secondCourse: boolean; + thirdCourse: boolean; + bachelor: boolean; + master: boolean; + }): string => { + const selectedLevels: string[] = []; + + if (levels.secondCourse) selectedLevels.push("2 курс"); + if (levels.thirdCourse) selectedLevels.push("3 курс"); + if (levels.bachelor) selectedLevels.push("Бакалаврская ВКР"); + if (levels.master) selectedLevels.push("Магистерская ВКР"); + + return selectedLevels.join(", "); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!Object.values(levels).some(v => v)) return; + + try { + const theme: InputTheme = { + title: title, + description: description, + level: transformLevelsToString(levels), + source: source, + suggestedby: me.userId, + department: department, + supervisorid: lecturerId, + consultantid: consultantId + }; + + await postTheme(theme); + navigate("/"); + + } catch (error) { + console.error("Ошибка при создании темы:", error); + alert("Не удалось создать тему. Проверьте данные и повторите попытку. " + error); + } + }; + + return ( + + + + + + + Предложить новую тему + + +
+ + + + Название: + + setTitle(e.target.value)} + required + /> + + + + + Описание: + + { + setDescription(val); + setIsMdTouched(true); + }} + height={200} + preview="edit" + visibleDragbar={false} + textareaProps={{ + placeholder: 'Введите описание темы...', + }} + /> + + + + + Уровень + + + {Object.entries(levels).map(([key, value]) => ( + setLevels({ + ...levels, + [key]: e.target.checked + })} + /> + } + label={ + key === 'secondCourse' ? '2 курс' : + key === 'thirdCourse' ? '3 курс' : + key === 'bachelor' ? 'Бакалаврская ВКР' : + 'Магистерская ВКР' + } + /> + ))} + + + + + + Кафедра: + + + Кафедра + + + + + + + Источник темы: + + setSource(e.target.value)} + required + /> + + + + + {UserRole.CONSULTANT}: + + + {UserRole.CONSULTANT} + + + + + + + {UserRole.SUPERVISOR}: + + + {UserRole.SUPERVISOR} + + + + + + + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Themes/EditThemePage.tsx b/frontend/src/pages/Themes/EditThemePage.tsx new file mode 100644 index 0000000..e58a138 --- /dev/null +++ b/frontend/src/pages/Themes/EditThemePage.tsx @@ -0,0 +1,304 @@ +import {Layout} from "@shared/ui/layout/Layout.tsx"; +import {useEffect, useMemo, useState} from "react"; +import { + Container, + TextField, + Button, + Grid, + Paper, + Typography, + Checkbox, + FormControlLabel, + Select, + MenuItem, + InputLabel, + FormControl, + Stack +} from "@mui/material"; +import {useNavigate, useParams} from "react-router-dom"; +import {InputTheme, Theme} from "@/entities/Theme.ts"; +import {getConsultants, getLecturers, getMe, getThemes, postTheme, putTheme} from "@/shared/services/axios.service.ts"; +import MDEditor from '@uiw/react-md-editor'; +import {Lecturer} from "@/entities/Lecturer.ts"; +import {Consultant} from "@/entities/Consultant.ts"; +import { User } from "@/entities/User.ts"; +import {UserRole} from "../../entities/UserRoles"; + +export function EditThemePage() { + const {id} = useParams(); + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [levels, setLevels] = useState({ + secondCourse: false, + thirdCourse: false, + bachelor: false, + master: false + }); + const [lecturerId, setLecturerId] = useState(); + const [consultantId, setConsultantId] = useState(); + const departments = useMemo(() => ["Кафедра системного программирования", "Кафедра параллельных алгоритмов", + "Кафедра информатики", "Кафедра информационно-аналитических систем"], []) + const [department, setDepartment] = useState(); + const [sources, setSources] = useState(); + const [source, setSource] = useState("") + const [me, setMe] = useState() + const navigate = useNavigate(); + const [lecturers, setLecturers] = useState(); + const [consultants, setConsultants] = useState(); + + const [isMdTouched, setIsMdTouched] = useState(false); + + const isMdError = isMdTouched && !description; + + useEffect(() => { + getThemes().then(response => { + const themes: Theme[] = response.data; + const currentTheme = themes.find(s => s.id == id); + setTitle(currentTheme?.title ?? title); + setDescription(currentTheme?.description ?? description); + setDepartment(currentTheme?.department ?? department); + setSource(currentTheme?.source ?? source); + transformStringToLevels(currentTheme?.level); + + setSources(Array.from(new Set(themes.map((t) => t.source)))); + }); + + getLecturers().then(response => { + setLecturers(response.data); + }); + + getConsultants().then(response => { + setConsultants(response.data); + }); + + getMe().then(response => { + const data: User = response.data + setMe(data); + }) + }, []); + + const transformLevelsToString = (levels: { + secondCourse: boolean; + thirdCourse: boolean; + bachelor: boolean; + master: boolean; + }): string => { + const selectedLevels: string[] = []; + + if (levels.secondCourse) selectedLevels.push("2 курс"); + if (levels.thirdCourse) selectedLevels.push("3 курс"); + if (levels.bachelor) selectedLevels.push("Бакалаврская ВКР"); + if (levels.master) selectedLevels.push("Магистерская ВКР"); + + return selectedLevels.join(", "); + }; + + const transformStringToLevels = (levelString: string | undefined | null) => { + if (!levelString) { + setLevels({ + secondCourse: false, + thirdCourse: false, + bachelor: false, + master: false + }); + return; + } + + const levelParts = levelString.split(',').map(part => part.trim()); + + setLevels({ + secondCourse: levelParts.includes("2 курс"), + thirdCourse: levelParts.includes("3 курс"), + bachelor: levelParts.includes("Бакалаврская ВКР"), + master: levelParts.includes("Магистерская ВКР") + }) + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + const theme: Theme = { + id: id, + title: title, + description: description, + level: transformLevelsToString(levels), + source: source, + suggestedby: me.userId, + department: department, + supervisorid: lecturerId, + consultantid: consultantId + }; + + await putTheme(theme); + navigate("/"); + + } catch (error) { + console.error("Ошибка при обновлении темы:", error); + alert("Не удалось создать тему. Проверьте данные и повторите попытку. " + error); + } + }; + + return ( + + + + + + + Изменить тему + + +
+ + + + Название: + + setTitle(e.target.value)} + required + /> + + + + + Описание: + + { + setDescription(val); + setIsMdTouched(true); + }} + height={200} + preview="edit" + visibleDragbar={false} + textareaProps={{ + placeholder: 'Введите описание темы...', + }} + /> + + + + + Уровень + + + {Object.entries(levels).map(([key, value]) => ( + setLevels({ + ...levels, + [key]: e.target.checked + })} + /> + } + label={ + key === 'secondCourse' ? '2 курс' : + key === 'thirdCourse' ? '3 курс' : + key === 'bachelor' ? 'Бакалаврская ВКР' : + 'Магистерская ВКР' + } + /> + ))} + + + + + + Кафедра: + + + Кафедра + + + + + + + Источник темы: + + + Источник темы + setSource(e.target.value)} + required + /> + + + + + + {UserRole.CONSULTANT}: + + + {UserRole.CONSULTANT} + + + + + + + {UserRole.SUPERVISOR}: + + + {UserRole.SUPERVISOR} + + + + + + + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Themes/ThemePage.tsx b/frontend/src/pages/Themes/ThemePage.tsx new file mode 100644 index 0000000..1da334f --- /dev/null +++ b/frontend/src/pages/Themes/ThemePage.tsx @@ -0,0 +1,61 @@ +import {useParams, useNavigate} from "react-router-dom"; +import {useEffect, useState} from "react"; +import {getTheme} from "@/shared/services/axios.service.ts"; +import {Theme} from "@/entities/Theme.ts"; +import {Layout} from "@shared/ui/layout/Layout.tsx"; +import {Button, Container, Typography, Paper, CircularProgress, Box} from "@mui/material"; +import {UserRole} from "../../entities/UserRoles"; + +export function ThemePage() { + const {id} = useParams(); + const navigate = useNavigate(); + const [theme, setTheme] = useState(null); + + useEffect(() => { + if (!id) return; + + getTheme(parseInt(id)).then(response => { + const selectedTheme = response.data.find((t: Theme) => t.id.toString() === id); + setTheme(selectedTheme); + }); + }, [id]); + + if (!theme) { + return ( + + + + ); + } + + return ( + + + + + + + {theme.title} + + + + Уровень: {theme.level} + Кафедра: {theme.department} + Источник: {theme.source} + {UserRole.SUPERVISOR}: {theme.supervisor ? `${theme.supervisor.lastName} ${theme.supervisor.firstName} ${theme.supervisor.middleName}` : "Не назначен"} + + Консультант: {theme.consultant ? `${theme.consultant.lastName} ${theme.consultant.firstName} ${theme.consultant.middleName}` : "Не назначен"} + + Контакты + консультанта: {theme.consultant?.contact ?? ""} + Описание: {theme.description} + + + + + + ); +} diff --git a/frontend/src/pages/Themes/ThemesIndexPage.tsx b/frontend/src/pages/Themes/ThemesIndexPage.tsx new file mode 100644 index 0000000..6e808c6 --- /dev/null +++ b/frontend/src/pages/Themes/ThemesIndexPage.tsx @@ -0,0 +1,242 @@ +import {Layout} from "@shared/ui/layout/Layout.tsx"; +import {getJWTToken} from "@/shared/services/localStorage.service.ts"; +import {Navigate, useNavigate} from "react-router-dom"; +import {useEffect, useState} from "react"; +import {getMe, getThemes, putTheme} from "@/shared/services/axios.service.ts"; +import {Theme} from "@/entities/Theme.ts"; +import { + Container, + Card, + CardContent, + CardHeader, + Typography, + MenuItem, + Button, + Pagination, TextField, Paper, Box, Checkbox, FormControlLabel, Stack +} from "@mui/material"; +import ArchiveIcon from '@mui/icons-material/Archive'; +import EditIcon from '@mui/icons-material/Edit'; +import { User } from "@/entities/User.ts"; +import {UserRole} from "../../entities/UserRoles"; + +export function ThemesIndexPage() { + const tokenIsEmpty = getJWTToken() === ""; + const [themes, setThemes] = useState([]); + const [filteredThemes, setFilteredThemes] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 5; + const navigate = useNavigate(); + const levels = ["2 курс", "3 курс", "Бакалаврская ВКР", "Магистерская ВКР"] + const [me, setMe] = useState(); + + const [level, setLevel] = useState(""); + const [department, setDepartment] = useState(""); + const [source, setSource] = useState(""); + const [supervisor, setSupervisor] = useState(""); + const [isArchived, setIsArchived] = useState(false); + const isPracticeSupervisor = me?.roles.includes(UserRole.PRACTICE_SUPERVISOR); + + const handleArchiveAll = async () => { + if (window.confirm("Вы уверены, что хотите архивировать все отфильтрованные темы?")) { + try { + const archivePromises = filteredThemes.map(theme => + putTheme({ ...theme, isarchived: !isArchived }) + ); + + await Promise.all(archivePromises); + + setThemes(themes.map(theme => { + const isFiltered = filteredThemes.some(t => t.id === theme.id); + return isFiltered ? { ...theme, isarchived: !isArchived } : theme; + })); + + alert(`Темы успешно ${isArchived ? 'восстановлены из архива' : 'архивированы'}`); + } catch (error) { + console.error("Ошибка при архивировании:", error); + alert("Произошла ошибка при архивировании"); + } + + } + }; + + + useEffect(() => { + getThemes().then(response => { + setThemes(response.data); + setFilteredThemes(response.data); + }); + + getMe().then(response => { + const data: User = response.data + setMe(data); + }) + }, []); + + useEffect(() => { + let filtered = themes; + + if (level) filtered = filtered.filter(theme => theme.level.includes(level)); + if (department) filtered = filtered.filter(theme => theme.department === department); + if (source) filtered = filtered.filter(theme => theme.source === source); + if (supervisor) filtered = filtered.filter(theme => theme.supervisorid.toString() === supervisor); + filtered = filtered.filter(theme => theme.isarchived == isArchived); + + setFilteredThemes(filtered); + setCurrentPage(1); + }, [level, department, source, supervisor, themes, isArchived]); + + const indexOfLastTheme = currentPage * itemsPerPage; + const indexOfFirstTheme = indexOfLastTheme - itemsPerPage; + const currentThemes = filteredThemes.slice(indexOfFirstTheme, indexOfLastTheme); + + const rearchiveTheme = async (id: number) => { + const theme = themes.find(t => t.id === id); + if (!theme) return; + theme.isarchived = !isArchived; + await putTheme(theme); + setFilteredThemes(filteredThemes.filter(t => t.id !== id)); + } + + const handlePageChange = (_event: React.ChangeEvent, value: number) => setCurrentPage(value); + + return tokenIsEmpty ? : ( + + + Список тем + + + + + {isPracticeSupervisor && ( + + )} + + + + Фильтры + + setLevel(e.target.value)} margin="dense"> + Все + {levels.map((lvl, i) => {lvl})} + + + setDepartment(e.target.value)} margin="dense"> + Все + {Array.from(new Set(themes.map((t) => t.department))).map((dep, i) => ( + {dep} + ))} + + + setSource(e.target.value)} margin="dense"> + Все + {Array.from(new Set(themes.filter(t => t.source).map(t => t.source))).map((src, i) => ( + {src} + ))} + + + setSupervisor(e.target.value)} margin="dense"> + Все + {Array.from(new Set(themes.filter(t => t.supervisor).map(t => `${t.supervisor?.lastName} ${t.supervisor?.firstName} ${t.supervisor?.middleName}`))).map((sup, i) => ( + {sup} + ))} + + + setIsArchived(e.target.checked)} /> + } + label="Показать архивные" + sx={{mt: 1, mb: 1}} + /> + + + + + + + {currentThemes.length > 0 ? ( + currentThemes.map((theme) => ( + + navigate(`/theme/${theme.id}`)} + > + + + Уровень: {theme.level} + Кафедра: {theme.department} + Источник: {theme.source} + + Научный руководитель: {theme.supervisor + ? `${theme.supervisor.lastName} ${theme.supervisor.firstName} ${theme.supervisor.middleName}` + : "Не назначен"} + + + Консультант: {theme.consultant + ? `${theme.consultant.lastName} ${theme.consultant.firstName} ${theme.consultant.middleName}` + : "Не назначен"} + + + + + {me?.userId === theme.suggestedby && ( + + + + + )} + + )) + ) : ( + Нет доступных тем + )} + + {filteredThemes.length > itemsPerPage && ( + + + + )} + + + + + ); +} diff --git a/frontend/src/shared/services/auth.service.ts b/frontend/src/shared/services/auth.service.ts new file mode 100644 index 0000000..70cf3ec --- /dev/null +++ b/frontend/src/shared/services/auth.service.ts @@ -0,0 +1,36 @@ +import { + getJWTToken, + getRefreshToken, + setJWTToken, + setRefreshToken +} from "@shared/services/localStorage.service.ts"; +import {axiosService} from "@shared/services/axios.service.ts"; + +/// Header with access token for axios requests +export const authHeader = () => { + const token = getJWTToken(); + + if (token) { + return {Authorization: 'Bearer ' + token, "Content-Type": "application/json"}; + } else { + return {}; + } +} + +/// Refresh expired access token +export const refreshToken = async () => { + const refresh = getRefreshToken() + const access = getJWTToken() + const response = await axiosService + .post("auth-api/refresh/", {refreshToken: refresh, token: access}); + if (response.data.token && response.data.refreshToken) { + setJWTToken(response.data.token) + setRefreshToken(response.data.refreshToken) + } +} + +export const logout = () => { + setJWTToken("") + setRefreshToken("") + window.location.assign("/login"); +} \ No newline at end of file diff --git a/frontend/src/shared/services/axios.service.ts b/frontend/src/shared/services/axios.service.ts new file mode 100644 index 0000000..2fcd103 --- /dev/null +++ b/frontend/src/shared/services/axios.service.ts @@ -0,0 +1,85 @@ +import axios, {AxiosHeaders} from "axios"; +import {authHeader} from "@shared/services/auth.service.ts"; +import {InputTheme, Theme} from "../../entities/Theme.ts"; +import {setRefreshToken, setJWTToken} from "@shared/services/localStorage.service.ts"; +import {refreshToken} from "@shared/services/auth.service.ts"; +import {InputPractice, Practice } from "@/entities/Practice.ts"; + +// Axios service for API requesting +export const axiosService = axios.create({ + baseURL: "http://localhost:5000/", + headers: undefined, + data: undefined +}); + +/// Config for axios requests +axiosService.interceptors.request + .use(function (config) { + if (config.url?.includes("api/")) { + config.headers = {...authHeader()} as AxiosHeaders + } + return config; + }); + +/// Expired token and unauthorized handler +axiosService.interceptors.response + .use(function (response) { + return response; + }, async function (error) { + const loginUrl = "/login" + try { + if (error.response.status === 401) { + if (error.config.url === "/refresh") { + setJWTToken(""); + setRefreshToken(""); + window.location.assign(loginUrl); + return Promise.reject(error); + } + + await refreshToken() + return axiosService(error.config); + } + return Promise.reject(error); + } catch { + window.location.assign(loginUrl); + return; + } + }); + +export const login = (email: string, password: string) => axiosService.post(`auth-api/login`, { + email: email, + password: password +}) + +export const register = (data: RegisterData) => axiosService.post(`auth-api/register`, { + email: data.email, + password: data.password, + firstName: data.firstName, + lastName: data.lastName, + middleName: data.middleName, + roles: data.roles +}) + +export const getThemes = () => axiosService.get("core-api/themes") + +export const getTheme = (id: number) => axiosService.get(`core-api/themes?id=${id}`) + +export const postTheme = (inputTheme: InputTheme) => axiosService.post("core-api/themes", inputTheme) + +export const putTheme = (theme: Theme) => axiosService.put("core-api/themes", theme) + +export const getLecturers = () => axiosService.get("core-api/lecturers") +export const getConsultants = () => axiosService.get("core-api/consultants") + +export const getPractices = () => axiosService.get("core-api/practices") + +export const getUserPractices = (userId: string) => axiosService.get(`core-api/practices/query?userId=${userId}`) + +export const postPractice = (inputPractice: Practice) => axiosService.post("core-api/practices", inputPractice) + +export const putPractice = (practice: InputPractice) => axiosService.put("core-api/practices", practice) + +export const getStudentByUserId = (userId: string) => axiosService.get(`core-api/students/byUserId?userId=${userId}`) + +export const getAllUsers = () => axiosService.get(`auth-api/users`) +export const getMe = () => axiosService.get("core-api/me") \ No newline at end of file diff --git a/frontend/src/shared/services/localStorage.service.ts b/frontend/src/shared/services/localStorage.service.ts new file mode 100644 index 0000000..a4474f0 --- /dev/null +++ b/frontend/src/shared/services/localStorage.service.ts @@ -0,0 +1,9 @@ +export const setJWTToken = (token: string) => localStorage.setItem("JWTToken", token) + +export const getJWTToken = () => localStorage.getItem("JWTToken") + +export const getMyId = () => localStorage.getItem("me") + +export const setRefreshToken = (token: string) => localStorage.setItem("refreshToken", token) + +export const getRefreshToken = () => localStorage.getItem("refreshToken") \ No newline at end of file diff --git a/frontend/src/shared/ui/layout/Header.tsx b/frontend/src/shared/ui/layout/Header.tsx new file mode 100644 index 0000000..2e0a82d --- /dev/null +++ b/frontend/src/shared/ui/layout/Header.tsx @@ -0,0 +1,217 @@ +import React, { useEffect, useState } from "react"; +import { + AppBar, + Toolbar, + Typography, + Button, + IconButton, + Menu, + MenuItem, + Container, + Box, + Avatar, + useTheme, + useMediaQuery, + Divider, + Badge +} from "@mui/material"; +import { + AccountCircle, + AdminPanelSettings, + MenuBook, + WorkOutline, + KeyboardArrowDown +} from "@mui/icons-material"; +import { useNavigate } from "react-router-dom"; +import { logout } from "@shared/services/auth.service.ts"; +import { getMe } from "@/shared/services/axios.service.ts"; +import { User } from "@/entities/User"; +import {UserRole} from "../../../entities/UserRoles"; + +export default function Header() { + const navigate = useNavigate(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const [anchorEl, setAnchorEl] = useState(null); + const [me, setMe] = useState(); + const isMenuOpen = Boolean(anchorEl); + + const handleMenuOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + }; + + const handleNavigate = (path: string) => { + navigate(path); + handleMenuClose(); + }; + + useEffect(() => { + if (window.location.pathname === "/login" || window.location.pathname === "/register") return; + + getMe().then(res => { + const user: User = res.data; + setMe(user); + }); + }, [navigate]); + + return ( + + + + navigate("/")} + sx={{ + display: 'flex', + alignItems: 'center', + gap: 1, + cursor: 'pointer', + '&:hover': { opacity: 0.9 } + }} + > + + PracticesService + + + + {(window.location.pathname !== "/login" && window.location.pathname !== "/register") && ( + + {me?.roles.includes(UserRole.ADMIN) && ( + + )} + + + + + + + + + } + > + + + + + + )} + + + + + handleNavigate("/profile")}> + + + Профиль + + + + + + { logout(); handleMenuClose(); }} + sx={{ color: 'error.light', display: 'flex', alignItems: 'center' }} + > + Выйти + + + + ); +} \ No newline at end of file diff --git a/frontend/src/shared/ui/layout/Layout.tsx b/frontend/src/shared/ui/layout/Layout.tsx new file mode 100644 index 0000000..63dbee8 --- /dev/null +++ b/frontend/src/shared/ui/layout/Layout.tsx @@ -0,0 +1,12 @@ +import Header from "@shared/ui/layout/Header"; +import {FunctionComponent, ReactElement} from "react"; + +// Base layout realisation +export const Layout: FunctionComponent<{ children: ReactElement }> = props => { + return ( +
+
+ {props.children} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..358ca9b --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..4eb3bd2 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@/*": ["src/*"], + "@app/*": ["src/app/*"], + "@entities/*": ["src/entities/*"], + "@pages/*": ["src/pages/*"], + "@shared/*": ["src/shared/*"], + "@widgets/*": ["src/widgets/*"], + }, + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..db0becc --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..ee39500 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@app': path.resolve(__dirname, './src/app'), + '@entities': path.resolve(__dirname, './src/entities'), + '@pages': path.resolve(__dirname, './src/pages'), + '@shared': path.resolve(__dirname, './src/shared'), + '@widgets': path.resolve(__dirname, './src/widgets') + }, + }, + define: { + 'process.env': `"${process.env}"` + }, + server: { + host: true, + port: 8000, + watch: { + usePolling: true + } + }, + plugins: [react()], +}) diff --git a/global.json b/global.json new file mode 100644 index 0000000..f4fd385 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "9.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/Services/CoreService.Api/stylecop.json b/stylecop.json similarity index 100% rename from Services/CoreService.Api/stylecop.json rename to stylecop.json