Compare commits

...

2 Commits

Author SHA1 Message Date
2b1357e981 front to consta
clean dotnet backend
2026-01-20 17:17:22 +05:00
cae6a7795b fix CORS 2026-01-20 15:56:52 +05:00
28 changed files with 831 additions and 1080 deletions

137
README.md
View File

@@ -1,37 +1,25 @@
# Dex Demo Application # Dex Demo Application
Демонстрационное приложение для аутентификации через DexAuthenticator в Kubernetes кластере с Deckhouse. Демонстрационное приложение для аутентификации через DexAuthenticator в Kubernetes (Deckhouse).
![preview](preview.png) ![preview](preview.png)
## Описание ## Описание
Простое приложение, демонстрирующее интеграцию с DexAuthenticator: Простое приложение, демонстрирующее интеграцию с DexAuthenticator:
- **Backend (Python/FastAPI)**: Валидирует JWT токены, получает данные пользователя из PostgreSQL - **Backend (Python/FastAPI)**: валидирует JWT, берет данные пользователя и роли из PostgreSQL
- **Frontend (React/Vite)**: Отображает информацию о пользователе и доступные ресурсы на основе ролей - **Backend (.NET)**: функционально идентичен Python backend
- **PostgreSQL**: Хранит пользователей, роли и доступные ссылки - **Frontend (React/Vite)**: отображает информацию о пользователе и доступные ресурсы
- **DexAuthenticator**: Обеспечивает аутентификацию через Dex - **PostgreSQL**: хранит пользователей, роли и ссылки
- **DexAuthenticator**: обеспечивает аутентификацию через Dex
## Работа приложения ## Работа приложения
1. Пользователь открывает `https://python-navigator-demo.127.0.0.1.sslip.io` 1. Ingress/DexAuthenticator выполняет аутентификацию через Dex/IdP
2. Ingress перенаправляет на DexAuthenticator для аутентификации 2. DexAuthenticator проксирует запросы и добавляет заголовки `X-Auth-Request-Email`, `X-Auth-Request-User`, `Authorization`
3. Если не аутентифицирован происходит редирект на Dex (HTTP 302) для входа 3. Frontend запрашивает `/api/user-info`
4. Если не аутентифицирован в Dex происходит редирект на Blitz IdP (HTTP 302) для входа 4. Backend валидирует JWT, получает данные из PostgreSQL и возвращает информацию о пользователе и доступных ресурсах
5. После аутентификации в Blitz IdP → возврат в Dex
6. После успешной аутентификации Dex возвращает токен в DexAuthenticator
7. DexAuthenticator устанавливает заголовки (`X-Auth-Request-Email`,`X-Auth-Request-User`, `Authorization`) и cookie
8. После успешной аутентификации Frontend загружается
9. Frontend делает запрос к `/api/user-info`
10. DexAuthenticator устанавливает заголовки (`X-Auth-Request-Email`,`X-Auth-Request-User`, `Authorization`) и cookie
11. Backend:
- Валидирует JWT токен из заголовка `Authorization`
- Извлекает email/id пользователя
- Получает данные из PostgreSQL
- Возвращает информацию о пользователе и доступных ресурсах
12. Frontend отображает информацию о пользователе и доступные ресурсы
![Диаграмма последовательности](sequenceDiagram.png)
## Архитектура ## Архитектура
@@ -58,10 +46,8 @@
## Предварительные требования ## Предварительные требования
- Kubernetes кластер с Deckhouse - Kubernetes кластер с Deckhouse
- Настроенный Dex по адресу `https://dex.127.0.0.1.sslip.io` - Dex по адресу `https://dex.127.0.0.1.sslip.io`
- Docker - Docker, kubectl, make
- kubectl
- make
## Быстрый старт ## Быстрый старт
@@ -124,61 +110,23 @@ make clean
``` ```
. .
├── backend/ # Python бэкенд ├── backend/ # Python backend (FastAPI)
│ ├── main.py # FastAPI приложение ├── backend-dotnet/ # .NET backend (идентичен по логике)
│ ├── requirements.txt # Python зависимости ├── frontend/ # React frontend
│ └── Dockerfile # Docker образ ├── db/ # PostgreSQL init.sql
├── frontend/ # React фронтенд ├── k8s/ # Kubernetes манифесты
│ ├── src/ ├── Makefile
│ │ ├── App.jsx # Главный компонент └── README.md
│ │ └── App.css # Стили
│ ├── nginx.conf # Nginx конфигурация
│ └── Dockerfile # Docker образ
├── db/ # База данных
│ └── init.sql # SQL скрипт инициализации
├── k8s/ # Kubernetes манифесты
│ ├── namespace.yaml
│ ├── postgres.yaml
│ ├── backend.yaml
│ ├── frontend.yaml
│ ├── dex-authenticator.yaml
│ └── ingress.yaml
├── Makefile # Команды для сборки и развертывания
└── README.md # Документация
``` ```
## Компоненты ## Компоненты
### Backend (FastAPI) ### Backend (Python/.NET)
**Эндпоинты:** - `GET /api/health` — проверка здоровья
- `GET /api/health` - Проверка здоровья сервиса - `GET /api/user-info` — информация о пользователе и доступных ресурсах
- `GET /api/user-info` - Получение информации о пользователе - Валидирует JWT (подпись, issuer, exp) и читает email/id
- Забирает пользователя, роли и ссылки из PostgreSQL
**Функциональность:**
- Валидация JWT токенов от Dex (проверка подписи, issuer, exp)
- Извлечение email пользователя из токена или заголовков
- Получение данных пользователя из PostgreSQL (организация, полное имя)
- Получение ролей пользователя
- Получение доступных ссылок на основе ролей
### Frontend (React + Vite)
**Функциональность:**
- Отображение информации о пользователе
- Список ролей
- Доступные ресурсы на основе ролей пользователя
- Никакой логики аутентификации (вся аутентификация на стороне DexAuthenticator)
### База данных (PostgreSQL)
**Схема:**
- `organizations` - Организации
- `users` - Пользователи
- `roles` - Роли
- `user_roles` - Связь пользователей и ролей
- `links` - Доступные ссылки
- `role_links` - Связь ролей и ссылок
### Тестовые данные ### Тестовые данные
@@ -210,19 +158,15 @@ make clean
### Backend переменные окружения ### Backend переменные окружения
- `DB_HOST` - Хост PostgreSQL (по умолчанию: `postgres`) - `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
- `DB_PORT` - Порт PostgreSQL (по умолчанию: `5432`) - `DEX_ISSUER` (по умолчанию: `https://dex.127.0.0.1.sslip.io/`)
- `DB_NAME` - Имя БД (по умолчанию: `dexdemo`)
- `DB_USER` - Пользователь БД (по умолчанию: `dexdemo`)
- `DB_PASSWORD` - Пароль БД
- `DEX_ISSUER` - URL Dex issuer (по умолчанию: `https://dex.127.0.0.1.sslip.io/`)
### DexAuthenticator ### DexAuthenticator
Настройки в `k8s/dex-authenticator.yaml`: Настройки в `k8s/dex-authenticator.yaml`:
- `applicationDomain` - Домен приложения - `applicationDomain` — домен приложения
- `sendAuthorizationHeader` - Отправка заголовка Authorization с JWT - `sendAuthorizationHeader` отправка заголовка Authorization с JWT
- `keepUsersLoggedInFor` - Время сессии (24h) - `keepUsersLoggedInFor` — время сессии
## Разработка ## Разработка
@@ -276,23 +220,6 @@ Frontend будет доступен на `http://localhost:5173` с прокс
## Дополнительная настройка ## Дополнительная настройка
### Изменение тестовых пользователей - Изменение тестовых пользователей: редактируйте `db/init.sql` или `k8s/postgres.yaml`, затем `kubectl delete pod -n navigator-demo -l app=postgres`
- Собственный домен: обновите `applicationDomain` в `k8s/dex-authenticator.yaml` и `host` в `k8s/ingress.yaml`
Отредактируйте `db/init.sql` или `k8s/postgres.yaml` (ConfigMap `postgres-init`), затем: - Production: secrets для БД, TLS, resource limits, HPA, внешний PostgreSQL
```bash
kubectl delete pod -n navigator-demo -l app=postgres
```
### Использование собственного домена
Измените `applicationDomain` в `k8s/dex-authenticator.yaml` и `host` в `k8s/ingress.yaml`
### Production deployment
Для production окружения:
1. Используйте secrets для паролей БД
3. Включите TLS сертификаты
4. Настройте resource limits
5. Добавьте HorizontalPodAutoscaler
6. Используйте внешний PostgreSQL

Binary file not shown.

View File

@@ -1,6 +0,0 @@
bin/
obj/
*.user
*.suo
.vs/

View File

@@ -1,12 +0,0 @@
## .NET
bin/
obj/
*.user
*.suo
.vs/
.vscode/
*.swp
*.swo
*~
.DS_Store

View File

@@ -1,27 +0,0 @@
namespace DexDemoBackend;
public class AppConfig
{
public string DbHost { get; set; } = "postgres";
public string DbPort { get; set; } = "5440";
public string DbName { get; set; } = "dexdemo";
public string DbUser { get; set; } = "dexdemo";
public string DbPassword { get; set; } = "dexdemo";
public string Issuer { get; set; } = "https://dex.127.0.0.1.sslip.io/";
public bool InsecureDevMode { get; set; }
public string? InsecureDevEmail { get; set; }
public string[] AllowedOrigins { get; set; } = ["http://localhost:3000", "https://localhost:3000"];
// JWT configuration
public string NameClaimType { get; set; } = "name";
public string RoleClaimType { get; set; } = "role";
public string EmailClaimType { get; set; } = "email";
public string AuthRequestEmailHeader { get; set; } = "X-Auth-Request-Email";
public TimeSpan ClockSkew { get; set; } = TimeSpan.FromMinutes(5);
public string ConnectionString =>
$"Host={DbHost};Port={DbPort};Database={DbName};Username={DbUser};Password={DbPassword}";
}

View File

@@ -1,18 +0,0 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8000
ENV ASPNETCORE_URLS=http://+:8000
ENTRYPOINT ["dotnet", "backend-dotnet.dll"]

View File

@@ -1,36 +0,0 @@
using DexDemoBackend;
using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;
using Microsoft.Extensions.Options;
using System.Text.Encodings.Web;
public class InsecureDevAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly AppConfig _config;
public InsecureDevAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
AppConfig config) : base(options, logger, encoder)
{
_config = config;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var email = _config.InsecureDevEmail ?? "dev@example.com";
var claims = new[]
{
new Claim(ClaimTypes.Name, email),
new Claim(_config.EmailClaimType, email)
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}

View File

@@ -1,16 +0,0 @@
namespace DexDemoBackend;
public record Organization(int Id, string Name);
public record Role(int Id, string Name, string? Description);
public record Link(int Id, string Title, string Url, string? Description);
public record UserInfo(
string Email,
string FullName,
Organization? Organization,
List<Role> Roles,
List<Link> AvailableLinks
);

View File

@@ -1,273 +0,0 @@
using Dapper;
using DexDemoBackend;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
using Npgsql;
using System.Security.Claims;
using Microsoft.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<AppConfig>(builder.Configuration.GetSection("AppConfig"));
builder.Services.AddSingleton(sp => sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<AppConfig>>().Value);
var config = builder.Configuration.GetSection("AppConfig").Get<AppConfig>() ?? new AppConfig();
ValidateConfiguration(config);
ConfigureJwtAuthentication(builder.Services, config);
builder.Services.AddAuthorization();
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower;
});
var app = builder.Build();
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
var currentConfig = app.Services.GetRequiredService<AppConfig>();
app.UseCors(policy =>
{
policy.WithOrigins(currentConfig.AllowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
app.UseAuthentication();
app.UseAuthorization();
app.Use(async (context, next) =>
{
try
{
await next();
}
catch (UnauthorizedAccessException)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new { error = "Unauthorized" });
}
catch (Exception)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new { error = "Internal server error" });
}
});
app.MapGet("/api/health", () => Results.Ok(new HealthResponse("ok")));
app.MapGet("/api/user-identity", [Authorize] (HttpContext context) =>
{
return Results.Ok(new UserIdentityResponse(
context.User.Identity?.Name,
context.User.Identity?.IsAuthenticated ?? false,
context.User.Identity?.AuthenticationType,
context.User.Claims.Select(c => new ClaimResponse(c.Type, c.Value)).ToList()
));
});
app.MapGet("/api/user-info", [Authorize] async (HttpContext context, [FromServices] AppConfig cfg) =>
{
try
{
var email = await GetUserEmail(context, cfg);
await using var conn = new NpgsqlConnection(cfg.ConnectionString);
await conn.OpenAsync();
var userData = await conn.QueryAsync<UserDataResult>(@"
SELECT
u.email, u.full_name,
o.id as org_id, o.name as org_name,
r.id as role_id, r.name as role_name, r.description as role_description,
l.id as link_id, l.title as link_title, l.url as link_url, l.description as link_description
FROM users u
LEFT JOIN organizations o ON u.organization_id = o.id
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id
LEFT JOIN role_links rl ON r.id = rl.role_id
LEFT JOIN links l ON rl.link_id = l.id
WHERE u.email = @email
ORDER BY l.id",
new { email });
if (!userData.Any())
{
return Results.NotFound(new { detail = "User not found in database" });
}
var firstRecord = userData.First();
var organization = firstRecord.OrgId.HasValue && firstRecord.OrgName != null
? new Organization(firstRecord.OrgId.Value, firstRecord.OrgName)
: null;
var roles = userData
.Where(x => x.RoleId.HasValue)
.GroupBy(x => x.RoleId)
.Select(g => g.First())
.Select(x => new Role(x.RoleId!.Value, x.RoleName!, x.RoleDescription))
.ToList();
var links = userData
.Where(x => x.LinkId.HasValue)
.GroupBy(x => x.LinkId)
.Select(g => g.First())
.Select(x => new Link(x.LinkId!.Value, x.LinkTitle!, x.LinkUrl!, x.LinkDescription))
.ToList();
return Results.Ok(new UserInfo(firstRecord.Email, firstRecord.FullName, organization, roles, links));
}
catch (NpgsqlException ex) when (ex.IsTransient)
{
return Results.Json(new { error = "Database temporarily unavailable" }, statusCode: 503);
}
catch (NpgsqlException)
{
return Results.Json(new { error = "Database error" }, statusCode: 500);
}
});
app.Run();
static void ConfigureJwtAuthentication(IServiceCollection services, AppConfig config)
{
if (config.InsecureDevMode)
{
services.AddAuthentication("InsecureDev")
.AddScheme<AuthenticationSchemeOptions, InsecureDevAuthenticationHandler>("InsecureDev", _ => { });
return;
}
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = config.Issuer;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = config.Issuer,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = config.ClockSkew,
NameClaimType = config.NameClaimType,
RoleClaimType = config.RoleClaimType
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var nameClaim = context.Principal?.FindFirst(config.NameClaimType);
var emailClaim = context.Principal?.FindFirst(config.EmailClaimType);
if (nameClaim == null && emailClaim != null)
{
var identity = context.Principal?.Identity as ClaimsIdentity;
identity?.AddClaim(new Claim(config.NameClaimType, emailClaim.Value));
}
return Task.CompletedTask;
}
};
var httpClientHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
var discoveryUrl = $"{config.Issuer}.well-known/openid-configuration";
options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
discoveryUrl,
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever(new HttpClient(httpClientHandler))
{
RequireHttps = !config.InsecureDevMode
}
);
});
}
static void ValidateConfiguration(AppConfig config)
{
if (string.IsNullOrWhiteSpace(config.DbHost))
throw new InvalidOperationException("Configuration parameter 'AppConfig:DbHost' is required");
if (string.IsNullOrWhiteSpace(config.DbPort))
throw new InvalidOperationException("Configuration parameter 'AppConfig:DbPort' is required");
if (string.IsNullOrWhiteSpace(config.DbName))
throw new InvalidOperationException("Configuration parameter 'AppConfig:DbName' is required");
if (string.IsNullOrWhiteSpace(config.DbUser))
throw new InvalidOperationException("Configuration parameter 'AppConfig:DbUser' is required");
if (string.IsNullOrWhiteSpace(config.DbPassword))
throw new InvalidOperationException("Configuration parameter 'AppConfig:DbPassword' is required");
if (string.IsNullOrWhiteSpace(config.Issuer))
throw new InvalidOperationException("Configuration parameter 'AppConfig:Issuer' is required");
if (!Uri.TryCreate(config.Issuer, UriKind.Absolute, out _))
throw new InvalidOperationException("Configuration parameter 'AppConfig:Issuer' must be a valid URL");
if (config.InsecureDevMode && string.IsNullOrWhiteSpace(config.InsecureDevEmail))
throw new InvalidOperationException("Configuration parameter 'AppConfig:InsecureDevEmail' is required when 'AppConfig:InsecureDevMode' is enabled");
}
static Task<string> GetUserEmail(HttpContext context, AppConfig config)
{
if (context.User.Identity?.IsAuthenticated == true)
{
var emailClaim = context.User.FindFirst(config.EmailClaimType);
if (emailClaim != null)
{
return Task.FromResult(emailClaim.Value);
}
}
if (context.Request.Headers.TryGetValue(config.AuthRequestEmailHeader, out var emailHeader))
{
return Task.FromResult(emailHeader.ToString());
}
throw new UnauthorizedAccessException("No authentication information found");
}
record UserDataResult(
string Email,
string FullName,
int? OrgId,
string? OrgName,
int? RoleId,
string? RoleName,
string? RoleDescription,
int? LinkId,
string? LinkTitle,
string? LinkUrl,
string? LinkDescription
);
// API Response Models
record HealthResponse(string Status);
record ClaimResponse(string Type, string Value);
record UserIdentityResponse(
string? Name,
bool IsAuthenticated,
string? AuthenticationType,
List<ClaimResponse> Claims
);

View File

@@ -1,53 +0,0 @@
# .NET 8 Backend
Функционально эквивалентный бэкенд на .NET 8 для Dex Demo.
## Запуск
### Локально
```bash
dotnet run
```
### Docker
```bash
docker build -t dex-demo-backend-dotnet:latest .
docker run -p 8000:8000 \
-e AppConfig__DbHost=postgres \
-e AppConfig__DbPort=5440 \
-e AppConfig__Issuer=https://dex.127.0.0.1.sslip.io/ \
dex-demo-backend-dotnet:latest
```
## Особенности реализации
- **Minimal APIs**: современный подход ASP.NET Core без лишнего бойлерплейта
- **Records**: для моделей данных (immutable, concise)
- **Dapper**: микро-ORM для чистой и производительной работы с БД
- **Npgsql**: официальный PostgreSQL provider для .NET
- **OpenIdConnect**: стандартный механизм получения JWKS
## API
- `GET /api/health` - проверка здоровья
- `GET /api/user-info` - информация о пользователе (требует авторизацию)
## Переменные окружения
### База данных
- `AppConfig__DbHost` - хост PostgreSQL (по умолчанию: postgres)
- `AppConfig__DbPort` - порт PostgreSQL (по умолчанию: 5440)
- `AppConfig__DbName` - имя базы данных (по умолчанию: dexdemo)
- `AppConfig__DbUser` - пользователь базы данных (по умолчанию: dexdemo)
- `AppConfig__DbPassword` - пароль базы данных (по умолчанию: dexdemo)
### Аутентификация
- `AppConfig__Issuer` - URL Dex сервера (по умолчанию: https://dex.127.0.0.1.sslip.io/)
### Режим разработки
- `AppConfig__InsecureDevMode` - включить небезопасный режим разработки (true/false)
- `AppConfig__InsecureDevEmail` - email для тестирования в режиме разработки
### CORS
- `AppConfig__AllowedOrigins` - разрешенные origins для CORS (JSON массив)

View File

@@ -1,30 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore.Authentication": "Trace",
"Microsoft.AspNetCore.Authorization": "Trace",
"Microsoft.IdentityModel": "Trace",
"System.Net.Http.HttpClient": "Trace",
"Microsoft.AspNetCore.Authentication.JwtBearer": "Trace",
"Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler": "Trace",
"Microsoft.AspNetCore.Authorization.AuthorizationMiddleware": "Trace",
"Microsoft.AspNetCore.Authorization.DefaultAuthorizationService": "Trace",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore": "Information"
}
},
"Urls": "http://localhost:8000",
"AppConfig": {
"DbHost": "localhost",
"DbPort": "5432",
"DbName": "dexdemo",
"DbUser": "dexdemo",
"DbPassword": "dexdemo",
"Issuer": "https://dex.127.0.0.1.sslip.io/",
"InsecureDevMode": true,
"InsecureDevEmail": "test@example.com",
"AllowedOrigins": ["http://localhost:3000", "https://localhost:3000", "http://localhost:5173", "https://localhost:5173"]
}
}

View File

@@ -1,30 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore.Authentication": "Warning",
"Microsoft.AspNetCore.Authorization": "Warning",
"Microsoft.IdentityModel": "Warning",
"System.Net.Http.HttpClient": "Warning",
"Microsoft.AspNetCore.Authentication.JwtBearer": "Information",
"Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler": "Information",
"Microsoft.AspNetCore.Authorization.AuthorizationMiddleware": "Information",
"Microsoft.AspNetCore.Authorization.DefaultAuthorizationService": "Information",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AppConfig": {
"DbHost": "postgres",
"DbPort": "5432",
"DbName": "dexdemo",
"DbUser": "dexdemo",
"DbPassword": "dexdemo",
"Issuer": "https://dex.127.0.0.1.sslip.io/",
"InsecureDevMode": false,
"AllowedOrigins": ["https://yourdomain.com"]
}
}

View File

@@ -1,28 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore.Authentication": "Debug",
"Microsoft.AspNetCore.Authorization": "Debug",
"Microsoft.IdentityModel": "Debug",
"System.Net.Http.HttpClient": "Debug",
"Microsoft.AspNetCore.Authentication.JwtBearer": "Debug",
"Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler": "Debug",
"Microsoft.AspNetCore.Authorization.AuthorizationMiddleware": "Debug",
"Microsoft.AspNetCore.Authorization.DefaultAuthorizationService": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"AppConfig": {
"DbHost": "postgres",
"DbPort": "5440",
"DbName": "dexdemo",
"DbUser": "dexdemo",
"DbPassword": "dexdemo",
"Issuer": "https://dex.127.0.0.1.sslip.io/",
"InsecureDevMode": false,
"AllowedOrigins": ["http://localhost:3000", "https://localhost:3000"]
}
}

View File

@@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>DexDemoBackend</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.14.0" />
<PackageReference Include="Npgsql" Version="8.0.7" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
</ItemGroup>
</Project>

View File

@@ -3,7 +3,7 @@ version: '3.8'
services: services:
python-navigator-demo-postgres: python-navigator-demo-postgres:
image: postgres:15-alpine image: postgres:13.1-alpine
environment: environment:
POSTGRES_DB: dexdemo POSTGRES_DB: dexdemo
POSTGRES_USER: dexdemo POSTGRES_USER: dexdemo
@@ -19,20 +19,37 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
python-navigator-demo-backend: # python-navigator-demo-backend:
build: ./backend # build: ./backend
# ports:
# - "8000:8000"
# environment:
# DB_HOST: postgres
# DB_PORT: 5432
# DB_NAME: dexdemo
# DB_USER: dexdemo
# DB_PASSWORD: dexdemo
# DEX_ISSUER: https://dex.127.0.0.1.sslip.io
# # Режим разработки - установите INSECURE_DEV_MODE=true для локальной разработки без OIDC
# INSECURE_DEV_MODE: "true"
# INSECURE_DEV_EMAIL: "developer@example.com"
# depends_on:
# python-navigator-demo-postgres:
# condition: service_healthy
backend-dotnet:
build: ./backend-dotnet
ports: ports:
- "8000:8000" - "8001:8000"
environment: environment:
DB_HOST: postgres AppConfig__DbHost: python-navigator-demo-postgres
DB_PORT: 5432 AppConfig__DbPort: "5432"
DB_NAME: dexdemo AppConfig__DbName: dexdemo
DB_USER: dexdemo AppConfig__DbUser: dexdemo
DB_PASSWORD: dexdemo AppConfig__DbPassword: dexdemo
DEX_ISSUER: https://dex.127.0.0.1.sslip.io AppConfig__Issuer: https://dex.127.0.0.1.sslip.io/
# Режим разработки - установите INSECURE_DEV_MODE=true для локальной разработки без OIDC AppConfig__InsecureDevMode: "true"
INSECURE_DEV_MODE: "true" AppConfig__InsecureDevEmail: developer@example.com
INSECURE_DEV_EMAIL: "developer@example.com"
depends_on: depends_on:
python-navigator-demo-postgres: python-navigator-demo-postgres:
condition: service_healthy condition: service_healthy
@@ -40,7 +57,9 @@ services:
python-navigator-demo-frontend: python-navigator-demo-frontend:
build: ./frontend build: ./frontend
ports: ports:
- "8080:80" - "8091:80"
depends_on:
- backend-dotnet
volumes: volumes:
python-navigator-demo-postgres_data: python-navigator-demo-postgres_data:

View File

@@ -4,6 +4,17 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
# Проксирование API-запросов во внутренний сервис backend-dotnet
# Важно: без завершающего слэша, чтобы путь /api/... не обрезался до /...
location /api/ {
proxy_pass http://backend-dotnet:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# MIME types for JavaScript modules # MIME types for JavaScript modules
location ~* \.js$ { location ~* \.js$ {
add_header Content-Type application/javascript; add_header Content-Type application/javascript;
@@ -19,5 +30,4 @@ server {
gzip_vary on; gzip_vary on;
gzip_min_length 1024; gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss; gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss;
} }

View File

@@ -8,13 +8,14 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@vitejs/plugin-react-swc": "3.7.0", "@consta/uikit": "^5.29.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.0", "@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0", "@types/react-dom": "^18.2.0",
"@vitejs/plugin-react-swc": "3.7.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-react": "^7.33.0", "eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
@@ -22,6 +23,85 @@
"vite": "^5.1.5" "vite": "^5.1.5"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime-corejs3": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.6.tgz",
"integrity": "sha512-kz2fAQ5UzjV7X7D3ySxmj3vRq89dTpqOZWv76Z6pNPztkwb/0Yj1Mtx1xFrYj6mbIHysxtBot8J4o0JLCblcFw==",
"license": "MIT",
"peer": true,
"dependencies": {
"core-js-pure": "^3.43.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@bem-react/classname": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@bem-react/classname/-/classname-1.7.0.tgz",
"integrity": "sha512-WNZAJEVNHFpQ1eyR3SKxXUDHaXbTyMieFfC65tqEGvGxx9pMcaKf65v/IINdDBe6xIt6WgGu0EHgFQ5KH4lwZQ==",
"license": "MPL-2.0",
"peer": true
},
"node_modules/@bem-react/classnames": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@bem-react/classnames/-/classnames-1.4.0.tgz",
"integrity": "sha512-nPjAkqp3TUZmHrGOt6io/a7jPt6/9lIA21QbiRicyCmCcg41vBFs25wu7RdI+MIiI9VaiXhKnzobL9qbqUTyjw==",
"license": "MPL-2.0",
"peer": true
},
"node_modules/@consta/icons": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@consta/icons/-/icons-1.5.0.tgz",
"integrity": "sha512-LfoTGjuPMgC/M8NN00tbbf1xJfhiiAZeZjgg/nAiM7ab4WzkAPkQy9yCkRrml40bcjBXgJxDjZOHaSfitLddaA==",
"peer": true,
"peerDependencies": {
"@consta/uikit": "^5.0.0"
}
},
"node_modules/@consta/table": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@consta/table/-/table-0.7.3.tgz",
"integrity": "sha512-W48QJfh1ni408z5mxF3KIaFcMe+R56BFJPHeA10OTn5Pu9z5hq7k/8NYOa3JlfxRQv1CIz675/1TlBTB/uuZuw==",
"peer": true,
"peerDependencies": {
"@consta/icons": "^1.1.1",
"@consta/uikit": "^5.26.0",
"@reatom/core": "3.10.1",
"@reatom/npm-react": "3.10.6"
}
},
"node_modules/@consta/uikit": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@consta/uikit/-/uikit-5.29.0.tgz",
"integrity": "sha512-RevKFdMLuO9q0vJepKlgVuhMPe+MJc70BXHtTgpmfi7BT/Q6j28uwbJGQbjMSjByLObvjjA+cSYuGzjnNbzU4g==",
"peerDependencies": {
"@bem-react/classname": "^1.6.0",
"@bem-react/classnames": "^1.3.10",
"@consta/icons": "^1.3.0",
"@consta/table": "^0.7.0",
"@reatom/core": "^3.10.1",
"@reatom/npm-react": "^3.10.6",
"compute-scroll-into-view": "^3.1.1",
"date-fns": "^2.30.0",
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0",
"react-dropzone": "^14.2.3",
"react-imask": "^7.2.1",
"react-textarea-autosize": "^8.5.3",
"react-transition-group": "^4.4.5"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -29,6 +109,7 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -45,6 +126,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -61,6 +143,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -77,6 +160,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -93,6 +177,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -109,6 +194,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -125,6 +211,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -141,6 +228,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -157,6 +245,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -173,6 +262,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -189,6 +279,7 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -205,6 +296,7 @@
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -221,6 +313,7 @@
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -237,6 +330,7 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -253,6 +347,7 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -269,6 +364,7 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -285,6 +381,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -301,6 +398,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -317,6 +415,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -333,6 +432,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -349,6 +449,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -365,6 +466,7 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -381,6 +483,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -529,6 +632,85 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@reatom/core": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@reatom/core/-/core-3.10.1.tgz",
"integrity": "sha512-A5vx+akCGkc+YCYhqPaAnR46uvqe70pQ2JB82JCLgOrj+YmnStIGkiaiWG43wn30qUjatXjejJmGkqQbjtri+A==",
"license": "MIT",
"peer": true
},
"node_modules/@reatom/effects": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@reatom/effects/-/effects-3.11.3.tgz",
"integrity": "sha512-0qxr7m6e+GrOvt0pESONl4aRZxGjsU1HWXIsDR2Ghw0mNGjuStnEDUZnO+MVbKOArMIAvZ8ZoMrQWqXEBfOrVg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@reatom/core": "^3.2.0",
"@reatom/utils": "^3.5.0"
}
},
"node_modules/@reatom/hooks": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/@reatom/hooks/-/hooks-3.6.1.tgz",
"integrity": "sha512-1q8qXAOkQlDKc/Y94alPHWqMnXvJhCG4Rr9hQxPMPG1Qf3WpeKm7Zdxs4v3DC2Kcw6oG6djVk3i5duIjPygGWA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@reatom/core": "^3.2.0",
"@reatom/effects": "^3.7.0",
"@reatom/utils": "^3.3.0"
}
},
"node_modules/@reatom/lens": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/@reatom/lens/-/lens-3.12.0.tgz",
"integrity": "sha512-YsEnYYHi58ePDscXomnbDPC9NSggfJQHvhcpqEAmvZuHH4nbsUpoQj2W69nSQyUDH/+X+TxURvsGUOHaXMl9pQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@reatom/core": "^3.4.0",
"@reatom/effects": "^3.2.0",
"@reatom/hooks": "^3.3.1",
"@reatom/primitives": "^3.6.0",
"@reatom/utils": "^3.1.0"
}
},
"node_modules/@reatom/npm-react": {
"version": "3.10.6",
"resolved": "https://registry.npmjs.org/@reatom/npm-react/-/npm-react-3.10.6.tgz",
"integrity": "sha512-lyoJD+pF2/P6B5bzSYjUQCzUSa0zkUQNAm1Lj8VXmVbILuiWid+n/9o/fJa8eEyH+w6KaFRunz1WZcOg/NW91g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@reatom/core": "^3.5.0",
"@reatom/effects": "^3.7.3",
"@reatom/lens": "^3.1.0",
"@reatom/utils": "^3.9.0",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@reatom/primitives": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@reatom/primitives/-/primitives-3.11.0.tgz",
"integrity": "sha512-b+jtK7qpQxSP83mYQXpPRMlFyg+C9WPv4sZDWSmm00mwmwetW0KbltftkWiwQjQM1TLwpDLZ6R7DX7fcTraIgg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@reatom/core": "^3.1.1",
"@reatom/utils": "^3.1.1"
}
},
"node_modules/@reatom/utils": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@reatom/utils/-/utils-3.11.3.tgz",
"integrity": "sha512-H2FQf9xra7Twf0PxS6L0DtuRRC79NfHRB3V/YhnhPHyUE/UFscXrin/I2eGj3FEKcgOfC2BWnzCOrkXjKWgECQ==",
"license": "MIT",
"peer": true
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.4", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
@@ -536,6 +718,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -549,6 +732,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -562,6 +746,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -575,6 +760,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -588,6 +774,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -601,6 +788,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -614,6 +802,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -627,6 +816,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -640,6 +830,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -653,6 +844,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -666,6 +858,7 @@
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -679,6 +872,7 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -692,6 +886,7 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -705,6 +900,7 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -718,6 +914,7 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -731,6 +928,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -744,6 +942,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -757,6 +956,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -770,6 +970,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -783,6 +984,7 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -796,6 +998,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -809,6 +1012,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -819,6 +1023,7 @@
"version": "1.13.5", "version": "1.13.5",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz",
"integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -860,6 +1065,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -876,6 +1082,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -892,6 +1099,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -908,6 +1116,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -924,6 +1133,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -940,6 +1150,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -956,6 +1167,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -972,6 +1184,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -988,6 +1201,7 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1004,6 +1218,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1017,12 +1232,14 @@
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@swc/types": { "node_modules/@swc/types": {
"version": "0.1.25", "version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@swc/counter": "^0.1.3" "@swc/counter": "^0.1.3"
@@ -1032,6 +1249,7 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
@@ -1073,6 +1291,7 @@
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz",
"integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==", "integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@swc/core": "^1.5.7" "@swc/core": "^1.5.7"
@@ -1302,6 +1521,16 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/attr-accept": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=4"
}
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -1433,6 +1662,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/compute-scroll-into-view": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
"license": "MIT",
"peer": true
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1440,6 +1676,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-js-pure": {
"version": "3.47.0",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz",
"integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1459,7 +1707,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/data-view-buffer": { "node_modules/data-view-buffer": {
@@ -1516,6 +1763,23 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1590,6 +1854,17 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1786,6 +2061,7 @@
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -2087,6 +2363,19 @@
"node": "^10.12.0 || >=12.0.0" "node": "^10.12.0 || >=12.0.0"
} }
}, },
"node_modules/file-selector": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.7.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/find-up": { "node_modules/find-up": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -2153,6 +2442,7 @@
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -2463,6 +2753,19 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/imask": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/imask/-/imask-7.6.1.tgz",
"integrity": "sha512-sJlIFM7eathUEMChTh9Mrfw/IgiWgJqBKq2VNbyXvBZ7ev/IlO6/KQTKlV/Fm+viQMLrFLG/zCuudrLIwgK2dg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.24.4"
},
"engines": {
"npm": ">=4.0.0"
}
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -3097,6 +3400,7 @@
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -3122,7 +3426,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -3358,6 +3661,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/possible-typed-array-names": { "node_modules/possible-typed-array-names": {
@@ -3374,6 +3678,7 @@
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -3412,7 +3717,6 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
@@ -3476,13 +3780,82 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-dropzone": {
"version": "14.3.8",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
"license": "MIT",
"peer": true,
"dependencies": {
"attr-accept": "^2.2.4",
"file-selector": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">= 10.13"
},
"peerDependencies": {
"react": ">= 16.8 || 18.0.0"
}
},
"node_modules/react-imask": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/react-imask/-/react-imask-7.6.1.tgz",
"integrity": "sha512-vLNfzcCz62Yzx/GRGh5tiCph9Gbh2cZu+Tz8OiO5it2eNuuhpA0DWhhSlOtVtSJ80+Bx+vFK5De8eQ9AmbkXzA==",
"license": "MIT",
"peer": true,
"dependencies": {
"imask": "^7.6.1",
"prop-types": "^15.8.1"
},
"engines": {
"npm": ">=4.0.0"
},
"peerDependencies": {
"react": ">=0.14.0"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-textarea-autosize": {
"version": "8.5.9",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz",
"integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.13",
"use-composed-ref": "^1.3.0",
"use-latest": "^1.2.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"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==",
"license": "BSD-3-Clause",
"peer": true,
"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/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -3587,6 +3960,7 @@
"version": "4.52.4", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "1.0.8" "@types/estree": "1.0.8"
@@ -3874,6 +4248,7 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -4050,6 +4425,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"peer": true
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -4183,10 +4565,69 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-composed-ref": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz",
"integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-isomorphic-layout-effect": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
"integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-latest": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz",
"integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"use-isomorphic-layout-effect": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.20", "version": "5.4.20",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz",
"integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",

View File

@@ -10,11 +10,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@vitejs/plugin-react-swc": "3.7.0", "@consta/uikit": "^5.29.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-react-swc": "3.7.0",
"@types/react": "^18.2.0", "@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0", "@types/react-dom": "^18.2.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",

View File

@@ -1,213 +0,0 @@
:root {
--primary-color: #4f46e5;
--secondary-color: #06b6d4;
--background: #f8fafc;
--card-background: #ffffff;
--text-primary: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
--success-color: #10b981;
--error-color: #ef4444;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: var(--background);
color: var(--text-primary);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header {
text-align: center;
margin-bottom: 3rem;
}
.header h1 {
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 0.5rem;
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
}
.user-card {
background: var(--card-background);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
.user-card h2 {
font-size: 1.75rem;
margin-bottom: 1.5rem;
color: var(--text-primary);
}
.info-grid {
display: grid;
gap: 1rem;
margin-bottom: 2rem;
}
.info-item {
display: flex;
padding: 0.75rem;
background: var(--background);
border-radius: 8px;
}
.info-item .label {
font-weight: 600;
color: var(--text-secondary);
min-width: 150px;
}
.info-item .value {
color: var(--text-primary);
font-weight: 500;
}
.roles-section {
margin: 2rem 0;
padding-top: 2rem;
border-top: 1px solid var(--border-color);
}
.roles-section h3 {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-primary);
}
.roles-list {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.role-badge {
display: inline-flex;
flex-direction: column;
padding: 0.5rem 1rem;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
border-radius: 8px;
font-weight: 500;
}
.role-name {
font-size: 0.95rem;
font-weight: 600;
}
.role-description {
font-size: 0.75rem;
opacity: 0.9;
margin-top: 0.25rem;
}
.links-section {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid var(--border-color);
}
.links-section h3 {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-primary);
}
.links-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.link-card {
display: block;
padding: 1.25rem;
background: var(--background);
border: 1px solid var(--border-color);
border-radius: 8px;
text-decoration: none;
color: inherit;
transition: all 0.2s ease;
}
.link-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
border-color: var(--primary-color);
}
.link-card h4 {
color: var(--primary-color);
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.link-card p {
color: var(--text-secondary);
font-size: 0.9rem;
}
.no-links {
color: var(--text-secondary);
font-style: italic;
}
.loading, .error {
text-align: center;
padding: 3rem;
font-size: 1.25rem;
}
.loading {
color: var(--text-secondary);
}
.error {
color: var(--error-color);
}
.error h2 {
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.header h1 {
font-size: 1.75rem;
}
.user-card {
padding: 1.5rem;
}
.links-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -1,113 +1,208 @@
import { useState, useEffect } from 'react' import { useCallback, useEffect, useState } from 'react';
import './App.css' import { Card } from '@consta/uikit/Card';
import { Text } from '@consta/uikit/Text';
import { Badge } from '@consta/uikit/Badge';
import { Button } from '@consta/uikit/Button';
const API_URL = '/api/user-info';
const formatError = (error) => {
if (error instanceof Error && error.message) {
return error.message;
}
return 'Не удалось получить данные профиля.';
};
const EmptyState = ({ title, description }) => (
<div className="empty-state">
<Text weight="bold">{title}</Text>
{description ? (
<Text size="s" view="secondary">
{description}
</Text>
) : null}
</div>
);
function App() { function App() {
const [userInfo, setUserInfo] = useState(null) const [data, setData] = useState(null);
const [loading, setLoading] = useState(true) const [status, setStatus] = useState('loading');
const [error, setError] = useState(null) const [error, setError] = useState('');
const [reloadKey, setReloadKey] = useState(0);
const reload = useCallback(() => {
setReloadKey((value) => value + 1);
}, []);
useEffect(() => { useEffect(() => {
// Получаем информацию о пользователе от бэкенда let mounted = true;
fetch('/api/user-info') const controller = new AbortController();
.then(response => {
const load = async () => {
setStatus('loading');
setError('');
try {
const response = await fetch(API_URL, { signal: controller.signal });
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`) throw new Error(`Ошибка загрузки: ${response.status}`);
} }
return response.json() const payload = await response.json();
}) if (!mounted) {
.then(data => { return;
setUserInfo(data) }
setLoading(false) setData(payload);
}) setStatus('success');
.catch(err => { } catch (err) {
setError(err.message) if (!mounted || err?.name === 'AbortError') {
setLoading(false) return;
}) }
}, []) setError(formatError(err));
setStatus('error');
}
};
if (loading) { load();
return () => {
mounted = false;
controller.abort();
};
}, [reloadKey]);
if (status === 'loading') {
return ( return (
<div className="container"> <div className="app">
<div className="loading">Загрузка...</div> <Card className="section status">
<Text size="l" weight="bold">
Загрузка профиля
</Text>
<Text size="s" view="secondary">
Получаем данные о пользователе и доступных ресурсах.
</Text>
</Card>
</div> </div>
) );
} }
if (error) { if (status === 'error') {
return ( return (
<div className="container"> <div className="app">
<div className="error"> <Card className="section status">
<h2>Ошибка</h2> <Text size="l" weight="bold">
<p>{error}</p> Не удалось загрузить данные
</div> </Text>
<Text size="s" view="secondary">
{error}
</Text>
<div className="status-actions">
<Button label="Повторить" onClick={reload} />
</div>
</Card>
</div> </div>
) );
} }
const roles = data?.roles ?? [];
const links = data?.available_links ?? [];
const organization = data?.organization;
return ( return (
<div className="container"> <div className="app">
<header className="header"> <Card className="section">
<h1>Dex Authentication Demo</h1> <div className="profile-summary">
<p className="subtitle">Демонстрация аутентификации через DexAuthenticator</p> <div className="profile-info">
</header> <Text size="l" weight="bold">
{data?.full_name || 'Пользователь'}
<div className="user-card"> </Text>
<h2>Информация о пользователе</h2> <Text size="s" view="secondary">
<div className="info-grid"> {data?.email || 'Email не указан'}
<div className="info-item"> </Text>
<span className="label">Email:</span>
<span className="value">{userInfo.email || 'Не указан'}</span>
</div> </div>
<div className="info-item"> <div className="profile-meta">
<span className="label">Полное имя:</span> <Text size="s" view="secondary">
<span className="value">{userInfo.full_name || 'Не указано'}</span> Организация
</Text>
<Text weight="bold">
{organization?.name || 'Не указана'}
</Text>
</div> </div>
{userInfo.organization && (
<div className="info-item">
<span className="label">Организация:</span>
<span className="value">{userInfo.organization.name}</span>
</div>
)}
</div> </div>
</Card>
<div className="roles-section"> <Card className="section">
<h3>Роли</h3> <div className="section-header">
<div className="roles-list"> <Text size="m" weight="bold">
{userInfo.roles && userInfo.roles.map(role => ( Роли и доступ
<div key={role.id} className="role-badge"> </Text>
<span className="role-name">{role.name}</span> <Text size="s" view="secondary">
{role.description && ( Список доступов пользователя внутри организации.
<span className="role-description">{role.description}</span> </Text>
)} </div>
{roles.length ? (
<div className="roles">
{roles.map((role) => (
<div className="role-item" key={role.id || role.name}>
<Badge label={role.name} />
<div className="role-content">
<Text size="s" view="secondary">
{role.description || 'Описание роли отсутствует.'}
</Text>
</div>
</div> </div>
))} ))}
</div> </div>
</div> ) : (
<EmptyState
title="Роли не назначены"
description="Свяжитесь с администратором для выдачи доступа."
/>
)}
</Card>
<div className="links-section"> <Card className="section">
<h3>Доступные ресурсы</h3> <div className="section-header">
{userInfo.available_links && userInfo.available_links.length > 0 ? ( <Text size="m" weight="bold">
<div className="links-grid"> Доступные сервисы
{userInfo.available_links.map(link => ( </Text>
<a <Text size="s" view="secondary">
key={link.id} Быстрые ссылки на инструменты и ресурсы.
href={link.url} </Text>
className="link-card"
target="_blank"
rel="noopener noreferrer"
>
<h4>{link.title}</h4>
{link.description && <p>{link.description}</p>}
</a>
))}
</div>
) : (
<p className="no-links">У вас нет доступных ресурсов</p>
)}
</div> </div>
</div> {links.length ? (
<div className="links-grid">
{links.map((link) => (
<Card className="link-card" key={link.id || link.url}>
<div className="link-header">
<Text weight="bold">{link.title}</Text>
<Text size="s" view="secondary">
{link.description || 'Описание отсутствует'}
</Text>
</div>
<div className="link-action">
<Text
as="a"
href={link.url}
view="link"
size="s"
target="_blank"
rel="noreferrer"
>
Перейти
</Text>
</div>
</Card>
))}
</div>
) : (
<EmptyState
title="Пока нет доступных ссылок"
description="Ресурсы появятся после подключения сервисов."
/>
)}
</Card>
</div> </div>
) );
} }
export default App export default App;

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,10 +1,125 @@
:root { *,
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; *::before,
line-height: 1.5; *::after {
font-weight: 400; box-sizing: border-box;
} }
body { body {
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
background: #f5f6f7;
color: #1f1f1f;
font-family: 'Inter', 'Segoe UI', -apple-system, sans-serif;
}
#root {
min-height: 100vh;
}
.app {
max-width: 1100px;
margin: 0 auto;
padding: 32px 20px 64px;
display: flex;
flex-direction: column;
gap: 24px;
}
.section {
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.section-header {
display: flex;
flex-direction: column;
gap: 6px;
}
.profile-summary {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.profile-info,
.profile-meta {
display: flex;
flex-direction: column;
gap: 6px;
}
.roles {
display: grid;
gap: 12px;
}
.role-item {
display: flex;
align-items: flex-start;
gap: 12px;
}
.links-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
.link-card {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.link-header {
display: flex;
flex-direction: column;
gap: 6px;
}
.link-action {
margin-top: auto;
}
.status {
text-align: center;
align-items: center;
}
.status-actions {
margin-top: 8px;
}
.empty-state {
padding: 12px 0;
display: flex;
flex-direction: column;
gap: 6px;
}
:root {
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
min-width: 320px;
}
code {
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, 'Liberation Mono', 'Courier New', monospace;
} }

View File

@@ -1,10 +1,13 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import { Theme, presetGpnDefault } from '@consta/uikit/Theme'
import App from './App.jsx' import App from './App.jsx'
import './index.css';
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>
<App /> <Theme preset={presetGpnDefault}>
<App />
</Theme>
</StrictMode>, </StrictMode>,
) )

View File

@@ -7,7 +7,7 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: process.env.VITE_API_TARGET || 'http://localhost:8000', target: process.env.VITE_API_TARGET || 'http://localhost:8001',
changeOrigin: true, changeOrigin: true,
} }
} }

View File

@@ -42,25 +42,12 @@ spec:
env: env:
- name: ASPNETCORE_URLS - name: ASPNETCORE_URLS
value: "http://0.0.0.0:8000" value: "http://0.0.0.0:8000"
# - name: ASPNETCORE_HTTP_PORTS
# value: "8000"
envFrom: envFrom:
- configMapRef: - configMapRef:
name: backend-dotnet-config name: backend-dotnet-config
- secretRef: - secretRef:
name: backend-dotnet-secret name: backend-dotnet-secret
# livenessProbe:
# httpGet:
# path: /api/health
# port: 8000
# initialDelaySeconds: 10
# periodSeconds: 10
# readinessProbe:
# httpGet:
# path: /api/health
# port: 8000
# initialDelaySeconds: 5
# periodSeconds: 5
--- ---
apiVersion: v1 apiVersion: v1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,85 +0,0 @@
# Диаграмма последовательности аутентификации
```mermaid
sequenceDiagram
participant User as Пользователь
participant Browser as Браузер
participant Ingress as Nginx Ingress
participant DexAuth as DexAuthenticator<br/>(OAuth2 Proxy)
participant Frontend as Frontend<br/>(React SPA)
participant Backend as Backend<br/>(FastAPI)
participant Dex as Dex<br/>(Proxy IdP)
participant BlitzIdP as Blitz IdP<br/>(Основной IdP)
participant LDAP as LDAP<br/>(Резервный IdP)
participant PostgreSQL as PostgreSQL
Note over User, PostgreSQL: Процесс аутентификации пользователя
%% Пользователь обращается к приложению
User->>Browser: 1. Открывает python-navigator-demo.127.0.0.1.sslip.io
Browser->>Ingress: 2. HTTPS запрос к приложению
Ingress->>DexAuth: 3. Проверка аутентификации через auth-url
alt Сессия не найдена или истекла
DexAuth->>DexAuth: 4. Проверка session cookie
DexAuth->>Dex: 5. Перенаправление на аутентификацию (HTTP 302)
Dex->>BlitzIdP: 6. Запрос аутентификации
alt Blitz IdP недоступен
Dex->>LDAP: 7a. Переключение на резервный IdP
LDAP-->>Dex: 7b. Ответ от LDAP
else Blitz IdP доступен
BlitzIdP-->>Dex: 7c. Ответ от Blitz IdP
end
Dex-->>DexAuth: 8. Возврат токенов (access_token, refresh_token)
DexAuth->>DexAuth: 9. Сохранение токенов в сессии
DexAuth-->>Ingress: 10. Установка заголовков (X-Auth-Request-User, X-Auth-Request-Email, Authorization)
end
Ingress->>Frontend: 11. Перенаправление на Frontend с заголовками
Frontend-->>Browser: 12. Загрузка React приложения
Browser-->>User: 13. Отображение приложения
Note over User, PostgreSQL: Взаимодействие с Backend через API
%% Пользователь взаимодействует с приложением
User->>Browser: 14. Взаимодействие с интерфейсом
Browser->>Frontend: 15. Клик/действие пользователя
Frontend->>Ingress: 16. API запрос к /api/user-info
Ingress->>DexAuth: 17. Проверка аутентификации для API
DexAuth->>DexAuth: 18. Проверка session cookie
DexAuth->>DexAuth: 19. Извлечение токенов из сессии
alt Токен истек
DexAuth->>Dex: 20. Обновление access token через refresh_token
Dex->>BlitzIdP: 21. Обновление через основной IdP
alt Blitz IdP недоступен
Dex->>LDAP: 22a. Обновление через LDAP
LDAP-->>Dex: 22b. Новый токен от LDAP
else Blitz IdP доступен
BlitzIdP-->>Dex: 22c. Новый токен от Blitz IdP
end
Dex-->>DexAuth: 23. Новый access token
DexAuth->>DexAuth: 24. Обновление сессии
end
DexAuth-->>Ingress: 25. Установка заголовков (Authorization: Bearer JWT)
Ingress->>Backend: 26. Проксирование запроса с Authorization header
Backend->>Backend: 27. Валидация JWT токена через Dex JWKS
Backend->>Backend: 28. Извлечение email из токена
Backend->>PostgreSQL: 29. Запрос данных пользователя
PostgreSQL-->>Backend: 30. Данные пользователя (роли, организация, ссылки)
Backend-->>Ingress: 31. JSON ответ с данными пользователя
Ingress-->>Frontend: 32. Ответ с данными
Frontend-->>Browser: 33. Обновление интерфейса
Browser-->>User: 34. Отображение информации о пользователе
Note over User, PostgreSQL: Преимущества архитектуры
Note right of DexAuth: ✅ SPA не хранит токены<br/>✅ Автоматическое обновление токенов<br/>✅ Безопасность на уровне инфраструктуры<br/>✅ Прозрачность для приложения<br/>✅ JWT валидация на Backend<br/>✅ Ролевая модель в PostgreSQL
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB