diff --git a/README.md b/README.md index c4bb024..8a937b6 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,25 @@ # Dex Demo Application -Демонстрационное приложение для аутентификации через DexAuthenticator в Kubernetes кластере с Deckhouse. +Демонстрационное приложение для аутентификации через DexAuthenticator в Kubernetes (Deckhouse). ![preview](preview.png) ## Описание Простое приложение, демонстрирующее интеграцию с DexAuthenticator: -- **Backend (Python/FastAPI)**: Валидирует JWT токены, получает данные пользователя из PostgreSQL -- **Frontend (React/Vite)**: Отображает информацию о пользователе и доступные ресурсы на основе ролей -- **PostgreSQL**: Хранит пользователей, роли и доступные ссылки -- **DexAuthenticator**: Обеспечивает аутентификацию через Dex +- **Backend (Python/FastAPI)**: валидирует JWT, берет данные пользователя и роли из PostgreSQL +- **Backend (.NET)**: функционально идентичен Python backend +- **Frontend (React/Vite)**: отображает информацию о пользователе и доступные ресурсы +- **PostgreSQL**: хранит пользователей, роли и ссылки +- **DexAuthenticator**: обеспечивает аутентификацию через Dex ## Работа приложения -1. Пользователь открывает `https://python-navigator-demo.127.0.0.1.sslip.io` -2. Ingress перенаправляет на DexAuthenticator для аутентификации -3. Если не аутентифицирован происходит редирект на Dex (HTTP 302) для входа -4. Если не аутентифицирован в Dex происходит редирект на Blitz IdP (HTTP 302) для входа -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 отображает информацию о пользователе и доступные ресурсы +1. Ingress/DexAuthenticator выполняет аутентификацию через Dex/IdP +2. DexAuthenticator проксирует запросы и добавляет заголовки `X-Auth-Request-Email`, `X-Auth-Request-User`, `Authorization` +3. Frontend запрашивает `/api/user-info` +4. Backend валидирует JWT, получает данные из PostgreSQL и возвращает информацию о пользователе и доступных ресурсах -![Диаграмма последовательности](sequenceDiagram.png) ## Архитектура @@ -58,10 +46,8 @@ ## Предварительные требования - Kubernetes кластер с Deckhouse -- Настроенный Dex по адресу `https://dex.127.0.0.1.sslip.io` -- Docker -- kubectl -- make +- Dex по адресу `https://dex.127.0.0.1.sslip.io` +- Docker, kubectl, make ## Быстрый старт @@ -124,61 +110,23 @@ make clean ``` . -├── backend/ # Python бэкенд -│ ├── main.py # FastAPI приложение -│ ├── requirements.txt # Python зависимости -│ └── Dockerfile # Docker образ -├── frontend/ # React фронтенд -│ ├── src/ -│ │ ├── App.jsx # Главный компонент -│ │ └── 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/ # Python backend (FastAPI) +├── backend-dotnet/ # .NET backend (идентичен по логике) +├── frontend/ # React frontend +├── db/ # PostgreSQL init.sql +├── k8s/ # Kubernetes манифесты +├── Makefile +└── README.md ``` ## Компоненты -### Backend (FastAPI) +### Backend (Python/.NET) -**Эндпоинты:** -- `GET /api/health` - Проверка здоровья сервиса -- `GET /api/user-info` - Получение информации о пользователе - -**Функциональность:** -- Валидация JWT токенов от Dex (проверка подписи, issuer, exp) -- Извлечение email пользователя из токена или заголовков -- Получение данных пользователя из PostgreSQL (организация, полное имя) -- Получение ролей пользователя -- Получение доступных ссылок на основе ролей - -### Frontend (React + Vite) - -**Функциональность:** -- Отображение информации о пользователе -- Список ролей -- Доступные ресурсы на основе ролей пользователя -- Никакой логики аутентификации (вся аутентификация на стороне DexAuthenticator) - -### База данных (PostgreSQL) - -**Схема:** -- `organizations` - Организации -- `users` - Пользователи -- `roles` - Роли -- `user_roles` - Связь пользователей и ролей -- `links` - Доступные ссылки -- `role_links` - Связь ролей и ссылок +- `GET /api/health` — проверка здоровья +- `GET /api/user-info` — информация о пользователе и доступных ресурсах +- Валидирует JWT (подпись, issuer, exp) и читает email/id +- Забирает пользователя, роли и ссылки из PostgreSQL ### Тестовые данные @@ -210,19 +158,15 @@ make clean ### Backend переменные окружения -- `DB_HOST` - Хост PostgreSQL (по умолчанию: `postgres`) -- `DB_PORT` - Порт PostgreSQL (по умолчанию: `5432`) -- `DB_NAME` - Имя БД (по умолчанию: `dexdemo`) -- `DB_USER` - Пользователь БД (по умолчанию: `dexdemo`) -- `DB_PASSWORD` - Пароль БД -- `DEX_ISSUER` - URL Dex issuer (по умолчанию: `https://dex.127.0.0.1.sslip.io/`) +- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` +- `DEX_ISSUER` (по умолчанию: `https://dex.127.0.0.1.sslip.io/`) ### DexAuthenticator Настройки в `k8s/dex-authenticator.yaml`: -- `applicationDomain` - Домен приложения -- `sendAuthorizationHeader` - Отправка заголовка Authorization с JWT -- `keepUsersLoggedInFor` - Время сессии (24h) +- `applicationDomain` — домен приложения +- `sendAuthorizationHeader` — отправка заголовка Authorization с JWT +- `keepUsersLoggedInFor` — время сессии ## Разработка @@ -276,23 +220,6 @@ Frontend будет доступен на `http://localhost:5173` с прокс ## Дополнительная настройка -### Изменение тестовых пользователей - -Отредактируйте `db/init.sql` или `k8s/postgres.yaml` (ConfigMap `postgres-init`), затем: - -```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 +- Изменение тестовых пользователей: редактируйте `db/init.sql` или `k8s/postgres.yaml`, затем `kubectl delete pod -n navigator-demo -l app=postgres` +- Собственный домен: обновите `applicationDomain` в `k8s/dex-authenticator.yaml` и `host` в `k8s/ingress.yaml` +- Production: secrets для БД, TLS, resource limits, HPA, внешний PostgreSQL diff --git a/README.pdf b/README.pdf deleted file mode 100644 index 8f70254..0000000 Binary files a/README.pdf and /dev/null differ diff --git a/backend-dotnet/.dockerignore b/backend-dotnet/.dockerignore deleted file mode 100644 index 80f1ef6..0000000 --- a/backend-dotnet/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -bin/ -obj/ -*.user -*.suo -.vs/ - diff --git a/backend-dotnet/.gitignore b/backend-dotnet/.gitignore deleted file mode 100644 index d64c777..0000000 --- a/backend-dotnet/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -## .NET -bin/ -obj/ -*.user -*.suo -.vs/ -.vscode/ -*.swp -*.swo -*~ -.DS_Store - diff --git a/backend-dotnet/AppConfig.cs b/backend-dotnet/AppConfig.cs deleted file mode 100644 index a1fec85..0000000 --- a/backend-dotnet/AppConfig.cs +++ /dev/null @@ -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}"; -} - - - diff --git a/backend-dotnet/Dockerfile b/backend-dotnet/Dockerfile deleted file mode 100644 index a32c594..0000000 --- a/backend-dotnet/Dockerfile +++ /dev/null @@ -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"] - diff --git a/backend-dotnet/InsecureDevAuthenticationHandler.cs b/backend-dotnet/InsecureDevAuthenticationHandler.cs deleted file mode 100644 index 3c18fcf..0000000 --- a/backend-dotnet/InsecureDevAuthenticationHandler.cs +++ /dev/null @@ -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 -{ - private readonly AppConfig _config; - - public InsecureDevAuthenticationHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - AppConfig config) : base(options, logger, encoder) - { - _config = config; - } - - protected override Task 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)); - } -} diff --git a/backend-dotnet/Models.cs b/backend-dotnet/Models.cs deleted file mode 100644 index 5f08a1d..0000000 --- a/backend-dotnet/Models.cs +++ /dev/null @@ -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 Roles, - List AvailableLinks -); - diff --git a/backend-dotnet/Program.cs b/backend-dotnet/Program.cs deleted file mode 100644 index 8630cfd..0000000 --- a/backend-dotnet/Program.cs +++ /dev/null @@ -1,276 +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(builder.Configuration.GetSection("AppConfig")); -builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); - -var config = builder.Configuration.GetSection("AppConfig").Get() ?? new AppConfig(); - -ValidateConfiguration(config); - -ConfigureJwtAuthentication(builder.Services, config); - -builder.Services.AddCors(options => -{ - options.AddDefaultPolicy(policy => - { - policy.WithOrigins(config.AllowedOrigins) - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials(); - }); -}); - -builder.Services.AddAuthorization(); - -builder.Services.ConfigureHttpJsonOptions(options => -{ - options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower; -}); - -var app = builder.Build(); -Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; - -app.UseCors(); -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(@" - 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("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( - 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 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 Claims -); diff --git a/backend-dotnet/README.md b/backend-dotnet/README.md deleted file mode 100644 index 8e480be..0000000 --- a/backend-dotnet/README.md +++ /dev/null @@ -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 массив) - diff --git a/backend-dotnet/appsettings.Development.json b/backend-dotnet/appsettings.Development.json deleted file mode 100644 index 3900e2e..0000000 --- a/backend-dotnet/appsettings.Development.json +++ /dev/null @@ -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"] - } -} - diff --git a/backend-dotnet/appsettings.Production.json b/backend-dotnet/appsettings.Production.json deleted file mode 100644 index df7eb3a..0000000 --- a/backend-dotnet/appsettings.Production.json +++ /dev/null @@ -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"] - } -} - - diff --git a/backend-dotnet/appsettings.json b/backend-dotnet/appsettings.json deleted file mode 100644 index a4b9390..0000000 --- a/backend-dotnet/appsettings.json +++ /dev/null @@ -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"] - } -} - diff --git a/backend-dotnet/backend-dotnet.csproj b/backend-dotnet/backend-dotnet.csproj deleted file mode 100644 index eb36e33..0000000 --- a/backend-dotnet/backend-dotnet.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - net8.0 - enable - enable - DexDemoBackend - - - - - - - - - - - - diff --git a/docker-compose.yml b/docker-compose.yml index 191cf03..31009fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.8' services: python-navigator-demo-postgres: - image: postgres:15-alpine + image: postgres:13.1-alpine environment: POSTGRES_DB: dexdemo POSTGRES_USER: dexdemo @@ -19,20 +19,37 @@ services: timeout: 5s retries: 5 - python-navigator-demo-backend: - build: ./backend + # python-navigator-demo-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: - - "8000:8000" + - "8001: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" + AppConfig__DbHost: python-navigator-demo-postgres + AppConfig__DbPort: "5432" + AppConfig__DbName: dexdemo + AppConfig__DbUser: dexdemo + AppConfig__DbPassword: dexdemo + AppConfig__Issuer: https://dex.127.0.0.1.sslip.io/ + AppConfig__InsecureDevMode: "true" + AppConfig__InsecureDevEmail: developer@example.com depends_on: python-navigator-demo-postgres: condition: service_healthy @@ -40,7 +57,9 @@ services: python-navigator-demo-frontend: build: ./frontend ports: - - "8080:80" + - "8091:80" + depends_on: + - backend-dotnet volumes: python-navigator-demo-postgres_data: diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 68f1e32..a0a092f 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -4,6 +4,17 @@ server { root /usr/share/nginx/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 location ~* \.js$ { add_header Content-Type application/javascript; @@ -19,5 +30,4 @@ server { gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss; -} - +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 881f516..ca79f7b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,13 +8,14 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "@vitejs/plugin-react-swc": "3.7.0", + "@consta/uikit": "^5.29.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react-swc": "3.7.0", "eslint": "^8.57.0", "eslint-plugin-react": "^7.33.0", "eslint-plugin-react-hooks": "^4.6.0", @@ -22,6 +23,85 @@ "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": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -29,6 +109,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -45,6 +126,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -61,6 +143,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -77,6 +160,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -93,6 +177,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -109,6 +194,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -125,6 +211,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -141,6 +228,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -157,6 +245,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -173,6 +262,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -189,6 +279,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -205,6 +296,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -221,6 +313,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -237,6 +330,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -253,6 +347,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -269,6 +364,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -285,6 +381,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -301,6 +398,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -317,6 +415,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -333,6 +432,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -349,6 +449,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -365,6 +466,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -381,6 +483,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -529,6 +632,85 @@ "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": { "version": "4.52.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", @@ -536,6 +718,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -549,6 +732,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -562,6 +746,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -575,6 +760,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -588,6 +774,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -601,6 +788,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -614,6 +802,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -627,6 +816,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -640,6 +830,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -653,6 +844,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -666,6 +858,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -679,6 +872,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -692,6 +886,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -705,6 +900,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -718,6 +914,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -731,6 +928,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -744,6 +942,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -757,6 +956,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -770,6 +970,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -783,6 +984,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -796,6 +998,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -809,6 +1012,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -819,6 +1023,7 @@ "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -860,6 +1065,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -876,6 +1082,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -892,6 +1099,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -908,6 +1116,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -924,6 +1133,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -940,6 +1150,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -956,6 +1167,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -972,6 +1184,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -988,6 +1201,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1004,6 +1218,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1017,12 +1232,14 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, "license": "Apache-2.0" }, "node_modules/@swc/types": { "version": "0.1.25", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -1032,6 +1249,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/@types/prop-types": { @@ -1073,6 +1291,7 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz", "integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==", + "dev": true, "license": "MIT", "dependencies": { "@swc/core": "^1.5.7" @@ -1302,6 +1521,16 @@ "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": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1433,6 +1662,13 @@ "dev": true, "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": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1440,6 +1676,18 @@ "dev": true, "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": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1459,7 +1707,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -1516,6 +1763,23 @@ "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": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1590,6 +1854,17 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1786,6 +2061,7 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -2087,6 +2363,19 @@ "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": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2153,6 +2442,7 @@ "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, "license": "MIT", "optional": true, @@ -2463,6 +2753,19 @@ "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": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -3097,6 +3400,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -3122,7 +3426,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3358,6 +3661,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/possible-typed-array-names": { @@ -3374,6 +3678,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3412,7 +3717,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -3476,13 +3780,82 @@ "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": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "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": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -3587,6 +3960,7 @@ "version": "4.52.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -3874,6 +4248,7 @@ "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, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4050,6 +4425,13 @@ "dev": true, "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": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4183,10 +4565,69 @@ "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": { "version": "5.4.20", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", diff --git a/frontend/package.json b/frontend/package.json index 3f906d0..1b3dbce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,11 +10,12 @@ "preview": "vite preview" }, "dependencies": { - "@vitejs/plugin-react-swc": "3.7.0", + "@consta/uikit": "^5.29.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { + "@vitejs/plugin-react-swc": "3.7.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "eslint": "^8.57.0", diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index 01997df..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -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; - } -} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 12e5780..3c3c243 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,113 +1,208 @@ -import { useState, useEffect } from 'react' -import './App.css' +import { useCallback, useEffect, useState } from 'react'; +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 }) => ( +
+ {title} + {description ? ( + + {description} + + ) : null} +
+); function App() { - const [userInfo, setUserInfo] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) + const [data, setData] = useState(null); + const [status, setStatus] = useState('loading'); + const [error, setError] = useState(''); + const [reloadKey, setReloadKey] = useState(0); + + const reload = useCallback(() => { + setReloadKey((value) => value + 1); + }, []); useEffect(() => { - // Получаем информацию о пользователе от бэкенда - fetch('/api/user-info') - .then(response => { + let mounted = true; + const controller = new AbortController(); + + const load = async () => { + setStatus('loading'); + setError(''); + + try { + const response = await fetch(API_URL, { signal: controller.signal }); if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) + throw new Error(`Ошибка загрузки: ${response.status}`); } - return response.json() - }) - .then(data => { - setUserInfo(data) - setLoading(false) - }) - .catch(err => { - setError(err.message) - setLoading(false) - }) - }, []) + const payload = await response.json(); + if (!mounted) { + return; + } + setData(payload); + setStatus('success'); + } catch (err) { + if (!mounted || err?.name === 'AbortError') { + return; + } + setError(formatError(err)); + setStatus('error'); + } + }; - if (loading) { + load(); + + return () => { + mounted = false; + controller.abort(); + }; + }, [reloadKey]); + + if (status === 'loading') { return ( -
-
Загрузка...
+
+ + + Загрузка профиля + + + Получаем данные о пользователе и доступных ресурсах. + +
- ) + ); } - if (error) { + if (status === 'error') { return ( -
-
-

Ошибка

-

{error}

-
+
+ + + Не удалось загрузить данные + + + {error} + +
+
+
- ) + ); } + const roles = data?.roles ?? []; + const links = data?.available_links ?? []; + const organization = data?.organization; + return ( -
-
-

Dex Authentication Demo

-

Демонстрация аутентификации через DexAuthenticator

-
- -
-

Информация о пользователе

-
-
- Email: - {userInfo.email || 'Не указан'} +
+ +
+
+ + {data?.full_name || 'Пользователь'} + + + {data?.email || 'Email не указан'} +
-
- Полное имя: - {userInfo.full_name || 'Не указано'} +
+ + Организация + + + {organization?.name || 'Не указана'} +
- {userInfo.organization && ( -
- Организация: - {userInfo.organization.name} -
- )}
+ -
-

Роли

-
- {userInfo.roles && userInfo.roles.map(role => ( -
- {role.name} - {role.description && ( - {role.description} - )} + +
+ + Роли и доступ + + + Список доступов пользователя внутри организации. + +
+ {roles.length ? ( +
+ {roles.map((role) => ( +
+ +
+ + {role.description || 'Описание роли отсутствует.'} + +
))}
-
+ ) : ( + + )} + -
-

Доступные ресурсы

- {userInfo.available_links && userInfo.available_links.length > 0 ? ( -
- {userInfo.available_links.map(link => ( - -

{link.title}

- {link.description &&

{link.description}

} -
- ))} -
- ) : ( -

У вас нет доступных ресурсов

- )} + +
+ + Доступные сервисы + + + Быстрые ссылки на инструменты и ресурсы. +
-
+ {links.length ? ( +
+ {links.map((link) => ( + +
+ {link.title} + + {link.description || 'Описание отсутствует'} + +
+
+ + Перейти + +
+
+ ))} +
+ ) : ( + + )} +
- ) + ); } -export default App +export default App; diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index 3739253..4e8abb9 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,10 +1,125 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +*, +*::before, +*::after { + box-sizing: border-box; } body { margin: 0; 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; } diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index b9a1a6d..67d0c11 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,10 +1,13 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import './index.css' +import { Theme, presetGpnDefault } from '@consta/uikit/Theme' import App from './App.jsx' +import './index.css'; createRoot(document.getElementById('root')).render( - + + + , ) diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 8e0fce0..fbd2ca1 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -7,7 +7,7 @@ export default defineConfig({ server: { proxy: { '/api': { - target: process.env.VITE_API_TARGET || 'http://localhost:8000', + target: process.env.VITE_API_TARGET || 'http://localhost:8001', changeOrigin: true, } } diff --git a/k8s/backend-dotnet.yaml b/k8s/backend-dotnet.yaml index 9b17d2a..a4b6b41 100644 --- a/k8s/backend-dotnet.yaml +++ b/k8s/backend-dotnet.yaml @@ -42,25 +42,12 @@ spec: env: - name: ASPNETCORE_URLS value: "http://0.0.0.0:8000" - # - name: ASPNETCORE_HTTP_PORTS - # value: "8000" + envFrom: - configMapRef: name: backend-dotnet-config - secretRef: 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 diff --git a/preview.png b/preview.png index 051f809..b28410c 100644 Binary files a/preview.png and b/preview.png differ diff --git a/sequenceDiagram.md b/sequenceDiagram.md deleted file mode 100644 index b87df27..0000000 --- a/sequenceDiagram.md +++ /dev/null @@ -1,85 +0,0 @@ -# Диаграмма последовательности аутентификации - -```mermaid -sequenceDiagram - participant User as Пользователь - participant Browser as Браузер - participant Ingress as Nginx Ingress - participant DexAuth as DexAuthenticator
(OAuth2 Proxy) - participant Frontend as Frontend
(React SPA) - participant Backend as Backend
(FastAPI) - participant Dex as Dex
(Proxy IdP) - participant BlitzIdP as Blitz IdP
(Основной IdP) - participant LDAP as LDAP
(Резервный 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 не хранит токены
✅ Автоматическое обновление токенов
✅ Безопасность на уровне инфраструктуры
✅ Прозрачность для приложения
✅ JWT валидация на Backend
✅ Ролевая модель в PostgreSQL -``` diff --git a/sequenceDiagram.png b/sequenceDiagram.png deleted file mode 100644 index 170539c..0000000 Binary files a/sequenceDiagram.png and /dev/null differ