Compare commits
2 Commits
71a0fb48fe
...
2b1357e981
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b1357e981 | |||
| cae6a7795b |
137
README.md
137
README.md
@@ -1,37 +1,25 @@
|
||||
# Dex Demo Application
|
||||
|
||||
Демонстрационное приложение для аутентификации через DexAuthenticator в Kubernetes кластере с Deckhouse.
|
||||
Демонстрационное приложение для аутентификации через DexAuthenticator в Kubernetes (Deckhouse).
|
||||
|
||||

|
||||
|
||||
## Описание
|
||||
|
||||
Простое приложение, демонстрирующее интеграцию с 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 и возвращает информацию о пользователе и доступных ресурсах
|
||||
|
||||

|
||||
|
||||
## Архитектура
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
README.pdf
BIN
README.pdf
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
*.suo
|
||||
.vs/
|
||||
|
||||
12
backend-dotnet/.gitignore
vendored
12
backend-dotnet/.gitignore
vendored
@@ -1,12 +0,0 @@
|
||||
## .NET
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
*.suo
|
||||
.vs/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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 массив)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
451
frontend/package-lock.json
generated
451
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 }) => (
|
||||
<div className="empty-state">
|
||||
<Text weight="bold">{title}</Text>
|
||||
{description ? (
|
||||
<Text size="s" view="secondary">
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="container">
|
||||
<div className="loading">Загрузка...</div>
|
||||
<div className="app">
|
||||
<Card className="section status">
|
||||
<Text size="l" weight="bold">
|
||||
Загрузка профиля
|
||||
</Text>
|
||||
<Text size="s" view="secondary">
|
||||
Получаем данные о пользователе и доступных ресурсах.
|
||||
</Text>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="error">
|
||||
<h2>Ошибка</h2>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
<div className="app">
|
||||
<Card className="section status">
|
||||
<Text size="l" weight="bold">
|
||||
Не удалось загрузить данные
|
||||
</Text>
|
||||
<Text size="s" view="secondary">
|
||||
{error}
|
||||
</Text>
|
||||
<div className="status-actions">
|
||||
<Button label="Повторить" onClick={reload} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const roles = data?.roles ?? [];
|
||||
const links = data?.available_links ?? [];
|
||||
const organization = data?.organization;
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<header className="header">
|
||||
<h1>Dex Authentication Demo</h1>
|
||||
<p className="subtitle">Демонстрация аутентификации через DexAuthenticator</p>
|
||||
</header>
|
||||
|
||||
<div className="user-card">
|
||||
<h2>Информация о пользователе</h2>
|
||||
<div className="info-grid">
|
||||
<div className="info-item">
|
||||
<span className="label">Email:</span>
|
||||
<span className="value">{userInfo.email || 'Не указан'}</span>
|
||||
<div className="app">
|
||||
<Card className="section">
|
||||
<div className="profile-summary">
|
||||
<div className="profile-info">
|
||||
<Text size="l" weight="bold">
|
||||
{data?.full_name || 'Пользователь'}
|
||||
</Text>
|
||||
<Text size="s" view="secondary">
|
||||
{data?.email || 'Email не указан'}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="label">Полное имя:</span>
|
||||
<span className="value">{userInfo.full_name || 'Не указано'}</span>
|
||||
<div className="profile-meta">
|
||||
<Text size="s" view="secondary">
|
||||
Организация
|
||||
</Text>
|
||||
<Text weight="bold">
|
||||
{organization?.name || 'Не указана'}
|
||||
</Text>
|
||||
</div>
|
||||
{userInfo.organization && (
|
||||
<div className="info-item">
|
||||
<span className="label">Организация:</span>
|
||||
<span className="value">{userInfo.organization.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="roles-section">
|
||||
<h3>Роли</h3>
|
||||
<div className="roles-list">
|
||||
{userInfo.roles && userInfo.roles.map(role => (
|
||||
<div key={role.id} className="role-badge">
|
||||
<span className="role-name">{role.name}</span>
|
||||
{role.description && (
|
||||
<span className="role-description">{role.description}</span>
|
||||
)}
|
||||
<Card className="section">
|
||||
<div className="section-header">
|
||||
<Text size="m" weight="bold">
|
||||
Роли и доступ
|
||||
</Text>
|
||||
<Text size="s" view="secondary">
|
||||
Список доступов пользователя внутри организации.
|
||||
</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>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="Роли не назначены"
|
||||
description="Свяжитесь с администратором для выдачи доступа."
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<div className="links-section">
|
||||
<h3>Доступные ресурсы</h3>
|
||||
{userInfo.available_links && userInfo.available_links.length > 0 ? (
|
||||
<div className="links-grid">
|
||||
{userInfo.available_links.map(link => (
|
||||
<a
|
||||
key={link.id}
|
||||
href={link.url}
|
||||
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>
|
||||
)}
|
||||
<Card className="section">
|
||||
<div className="section-header">
|
||||
<Text size="m" weight="bold">
|
||||
Доступные сервисы
|
||||
</Text>
|
||||
<Text size="s" view="secondary">
|
||||
Быстрые ссылки на инструменты и ресурсы.
|
||||
</Text>
|
||||
</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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<Theme preset={presetGpnDefault}>
|
||||
<App />
|
||||
</Theme>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
preview.png
BIN
preview.png
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 66 KiB |
@@ -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 |
Reference in New Issue
Block a user