Compare commits

..

2 Commits

Author SHA1 Message Date
641e1fc14d fix 2026-01-16 10:38:54 +05:00
f0b8ed240f mini fix 2025-10-10 15:04:01 +05:00
16 changed files with 3111 additions and 1317 deletions

1
.gitignore vendored
View File

@@ -35,4 +35,5 @@ yarn-error.log*
# Kubernetes
*.local.yaml
*.zip

View File

@@ -7,7 +7,7 @@ public class AppConfig
public string DbName { get; init; } = default!;
public string DbUser { get; init; } = default!;
public string DbPassword { get; init; } = default!;
public string DexIssuer { get; init; } = default!;
public string Issuer { get; init; } = default!;
public bool InsecureDevMode { get; init; }
public string? InsecureDevEmail { get; init; }

View File

@@ -1,58 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
namespace DexDemoBackend;
public class JwtValidator
{
private readonly AppConfig _config;
private readonly HttpClient _httpClient;
private ConfigurationManager<OpenIdConnectConfiguration>? _configManager;
public JwtValidator(AppConfig config)
{
_config = config;
_httpClient = new HttpClient(new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
});
}
public async Task<IDictionary<string, object>> ValidateToken(string token)
{
try
{
// Lazy init OIDC configuration manager
_configManager ??= new ConfigurationManager<OpenIdConnectConfiguration>(
$"{_config.DexIssuer}.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever(_httpClient) { RequireHttps = false }
);
var oidcConfig = await _configManager.GetConfigurationAsync(CancellationToken.None);
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = _config.DexIssuer,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = oidcConfig.SigningKeys
};
var handler = new JwtSecurityTokenHandler();
var principal = handler.ValidateToken(token, validationParameters, out _);
var jwtToken = handler.ReadJwtToken(token);
return jwtToken.Payload;
}
catch (Exception ex)
{
throw new UnauthorizedAccessException($"Token validation error: {ex.Message}", ex);
}
}
}

View File

@@ -1,10 +1,27 @@
using Dapper;
using DexDemoBackend;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
using Npgsql;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
var builder = WebApplication.CreateBuilder(args);
// Logging configuration is now handled in appsettings.json
// Only add console logging if not already configured
if (!builder.Logging.Services.Any(s => s.ServiceType == typeof(ILoggerProvider)))
{
builder.Logging.AddConsole();
}
// Logger will be created after app is built
// // Force binding to all interfaces in Kubernetes
// Console.WriteLine($"ASPNETCORE_URLS env var: {Environment.GetEnvironmentVariable("ASPNETCORE_URLS")}");
// Console.WriteLine($"ASPNETCORE_HTTP_PORTS env var: {Environment.GetEnvironmentVariable("ASPNETCORE_HTTP_PORTS")}");
@@ -23,13 +40,125 @@ var config = new AppConfig
DbName = Environment.GetEnvironmentVariable("DB_NAME") ?? "dexdemo",
DbUser = Environment.GetEnvironmentVariable("DB_USER") ?? "dexdemo",
DbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? "dexdemo",
DexIssuer = Environment.GetEnvironmentVariable("DEX_ISSUER") ?? "https://dex.127.0.0.1.sslip.io/",
Issuer = Environment.GetEnvironmentVariable("DEX_ISSUER") ?? "https://dex.127.0.0.1.sslip.io/",
InsecureDevMode = Environment.GetEnvironmentVariable("INSECURE_DEV_MODE")?.ToLower() == "true",
InsecureDevEmail = Environment.GetEnvironmentVariable("INSECURE_DEV_EMAIL")
};
// Configuration will be logged after logger is created
builder.Services.AddSingleton(config);
builder.Services.AddSingleton<JwtValidator>();
// Configure JWT Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = config.Issuer;
options.RequireHttpsMetadata = false; // For development
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = config.Issuer,
ValidateAudience = false, // Можно включить, если DEX выдает audience
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(5), // Допуск 5 минут на разницу времени
// Настройка для заполнения User.Identity.Name
NameClaimType = "name", // Используем claim "name" для User.Identity.Name
RoleClaimType = "role" // Используем claim "role" для ролей
};
// Add detailed event logging for JWT authentication
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
var jwtLogger = context.HttpContext.RequestServices.GetRequiredService<ILogger<JwtBearerHandler>>();
jwtLogger.LogError(context.Exception, "[JWT Auth Failed] Exception: {Message}", context.Exception?.Message);
jwtLogger.LogDebug("[JWT Auth Failed] StackTrace: {StackTrace}", context.Exception?.StackTrace);
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
var jwtLogger = context.HttpContext.RequestServices.GetRequiredService<ILogger<JwtBearerHandler>>();
// Логируем все доступные claims для отладки
var claims = string.Join(", ", context.Principal?.Claims?.Select(c => $"{c.Type}={c.Value}") ?? new string[0]);
jwtLogger.LogDebug("[JWT Token Validated] All claims: {Claims}", claims);
// Проверяем, есть ли name claim, если нет - используем email как fallback
var nameClaim = context.Principal?.FindFirst("name");
var emailClaim = context.Principal?.FindFirst("email");
var subClaim = context.Principal?.FindFirst("sub");
if (nameClaim == null && emailClaim != null)
{
// Если нет name claim, но есть email - добавляем его как name claim
var identity = context.Principal?.Identity as ClaimsIdentity;
identity?.AddClaim(new Claim("name", emailClaim.Value));
jwtLogger.LogDebug("[JWT Token Validated] Added email as name claim: {Email}", emailClaim.Value);
}
jwtLogger.LogInformation("[JWT Token Validated] User.Identity.Name: {UserName}", context.Principal?.Identity?.Name);
jwtLogger.LogInformation("[JWT Token Validated] Email claim: {Email}", emailClaim?.Value ?? "Not found");
jwtLogger.LogInformation("[JWT Token Validated] Sub claim: {Sub}", subClaim?.Value ?? "Not found");
return Task.CompletedTask;
},
OnMessageReceived = context =>
{
var jwtLogger = context.HttpContext.RequestServices.GetRequiredService<ILogger<JwtBearerHandler>>();
jwtLogger.LogDebug("[JWT Message Received] Scheme: {Scheme}, Path: {Path}, Method: {Method}",
context.Scheme.Name, context.Request.Path, context.Request.Method);
// Log Authorization header (without sensitive token data)
if (context.Request.Headers.TryGetValue("Authorization", out var authHeader))
{
var authValue = authHeader.ToString();
if (authValue.StartsWith("Bearer "))
{
var tokenPreview = authValue.Substring(7, Math.Min(20, authValue.Length - 7)) + "...";
jwtLogger.LogDebug("[JWT Message Received] Token preview: {TokenPreview}", tokenPreview);
}
}
return Task.CompletedTask;
},
OnChallenge = context =>
{
var jwtLogger = context.HttpContext.RequestServices.GetRequiredService<ILogger<JwtBearerHandler>>();
jwtLogger.LogWarning("[JWT Challenge] Error: {Error}, ErrorDescription: {ErrorDescription}, ErrorUri: {ErrorUri}",
context.Error, context.ErrorDescription, context.ErrorUri);
return Task.CompletedTask;
}
};
// Configure OpenID Connect discovery
var httpClientHandler = new HttpClientHandler();
// В режиме разработки отключаем проверку SSL
// В продакшене это должно быть настроено правильно!
if (config.InsecureDevMode)
{
// Logging will be done after logger is created
httpClientHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
}
var discoveryUrl = $"{config.Issuer}.well-known/openid-configuration";
// Logging will be done after logger is created
options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
discoveryUrl,
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever(new HttpClient(httpClientHandler))
{
RequireHttps = !config.InsecureDevMode
}
);
});
builder.Services.AddAuthorization();
// Configure JSON serialization to use snake_case
builder.Services.ConfigureHttpJsonOptions(options =>
@@ -50,92 +179,205 @@ builder.Services.AddCors(options =>
var app = builder.Build();
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
// Create logger after app is built
var logger = app.Services.GetRequiredService<ILogger<Program>>();
// Log configuration (without sensitive data)
logger.LogInformation("[Config] Application configuration:");
logger.LogInformation("[Config] DB_HOST: {DbHost}", config.DbHost);
logger.LogInformation("[Config] DB_PORT: {DbPort}", config.DbPort);
logger.LogInformation("[Config] DB_NAME: {DbName}", config.DbName);
logger.LogInformation("[Config] DB_USER: {DbUser}", config.DbUser);
logger.LogInformation("[Config] DB_PASSWORD: [HIDDEN]");
logger.LogInformation("[Config] DEX_ISSUER: {Issuer}", config.Issuer);
logger.LogInformation("[Config] INSECURE_DEV_MODE: {InsecureDevMode}", config.InsecureDevMode);
logger.LogInformation("[Config] INSECURE_DEV_EMAIL: {InsecureDevEmail}", config.InsecureDevEmail ?? "[NOT SET]");
// Log JWT configuration
if (config.InsecureDevMode)
{
logger.LogWarning("[JWT Config] InsecureDevMode enabled - disabling SSL certificate validation");
}
logger.LogInformation("[JWT Config] OpenID Connect discovery URL: {DiscoveryUrl}", $"{config.Issuer}.well-known/openid-configuration");
logger.LogInformation("[JWT Config] RequireHttps: {RequireHttps}", !config.InsecureDevMode);
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
logger.LogInformation("[App] Application middleware configured:");
logger.LogInformation("[App] CORS: Enabled");
logger.LogInformation("[App] Authentication: JWT Bearer");
logger.LogInformation("[App] Authorization: Enabled");
// Health check endpoint
app.MapGet("/api/health", () => Results.Ok(new { status = "ok" }));
// User info endpoint
app.MapGet("/api/user-info", async (HttpContext context, [FromServices] AppConfig cfg, [FromServices] JwtValidator jwtValidator) =>
// User identity demo endpoint
app.MapGet("/api/user-identity", [Authorize] (HttpContext context, [FromServices] ILogger<Program> logger) =>
{
var email = await GetUserEmail(context, cfg, jwtValidator);
logger.LogInformation("[UserIdentity] User.Identity.Name: {Name}", context.User.Identity?.Name ?? "NULL");
logger.LogInformation("[UserIdentity] User.Identity.IsAuthenticated: {IsAuthenticated}", context.User.Identity?.IsAuthenticated);
logger.LogInformation("[UserIdentity] User.Identity.AuthenticationType: {AuthType}", context.User.Identity?.AuthenticationType);
await using var conn = new NpgsqlConnection(cfg.ConnectionString);
await conn.OpenAsync();
// Get user info
var userResult = await conn.QuerySingleOrDefaultAsync<UserQueryResult>(@"
SELECT u.email, u.full_name, u.organization_id, o.name as org_name
FROM users u
LEFT JOIN organizations o ON u.organization_id = o.id
WHERE u.email = @email",
new { email });
if (userResult is null)
{
return Results.NotFound(new { detail = "User not found in database" });
}
var organization = userResult.OrganizationId.HasValue && userResult.OrgName != null
? new Organization(userResult.OrganizationId.Value, userResult.OrgName)
: null;
// Get user roles
var roles = (await conn.QueryAsync<Role>(@"
SELECT r.id, r.name, r.description
FROM roles r
JOIN user_roles ur ON r.id = ur.role_id
JOIN users u ON ur.user_id = u.id
WHERE u.email = @email",
new { email })).ToList();
// Get available links
var links = (await conn.QueryAsync<Link>(@"
SELECT DISTINCT l.id, l.title, l.url, l.description
FROM links l
JOIN role_links rl ON l.id = rl.link_id
JOIN user_roles ur ON rl.role_id = ur.role_id
JOIN users u ON ur.user_id = u.id
WHERE u.email = @email
ORDER BY l.id",
new { email })).ToList();
return Results.Ok(new UserInfo(userResult.Email, userResult.FullName, organization, roles, links));
return Results.Ok(new
{
name = context.User.Identity?.Name,
isAuthenticated = context.User.Identity?.IsAuthenticated,
authenticationType = context.User.Identity?.AuthenticationType,
claims = context.User.Claims.Select(c => new { type = c.Type, value = c.Value }).ToList()
});
});
// User info endpoint
app.MapGet("/api/user-info", [Authorize] async (HttpContext context, [FromServices] AppConfig cfg, [FromServices] ILogger<Program> userInfoLogger) =>
{
userInfoLogger.LogInformation("[UserInfo] Starting user info request for path: {Path}", context.Request.Path);
userInfoLogger.LogInformation("[UserInfo] User authenticated: {IsAuthenticated}", context.User.Identity?.IsAuthenticated);
try
{
var email = await GetUserEmail(context, cfg, userInfoLogger);
userInfoLogger.LogInformation("[UserInfo] Retrieved email: {Email}", email);
await using var conn = new NpgsqlConnection(cfg.ConnectionString);
await conn.OpenAsync();
userInfoLogger.LogDebug("[UserInfo] Database connection opened successfully");
// Get user info
userInfoLogger.LogDebug("[UserInfo] Querying user info for email: {Email}", email);
var userResult = await conn.QuerySingleOrDefaultAsync<UserQueryResult>(@"
SELECT u.email, u.full_name, u.organization_id, o.name as org_name
FROM users u
LEFT JOIN organizations o ON u.organization_id = o.id
WHERE u.email = @email",
new { email });
if (userResult is null)
{
userInfoLogger.LogWarning("[UserInfo] User not found in database for email: {Email}", email);
return Results.NotFound(new { detail = "User not found in database" });
}
userInfoLogger.LogInformation("[UserInfo] User found: {Email}, FullName: {FullName}, OrgId: {OrgId}",
userResult.Email, userResult.FullName, userResult.OrganizationId);
var organization = userResult.OrganizationId.HasValue && userResult.OrgName != null
? new Organization(userResult.OrganizationId.Value, userResult.OrgName)
: null;
// Get user roles
userInfoLogger.LogDebug("[UserInfo] Querying user roles for email: {Email}", email);
var roles = (await conn.QueryAsync<Role>(@"
SELECT r.id, r.name, r.description
FROM roles r
JOIN user_roles ur ON r.id = ur.role_id
JOIN users u ON ur.user_id = u.id
WHERE u.email = @email",
new { email })).ToList();
userInfoLogger.LogInformation("[UserInfo] Found {RoleCount} roles for user", roles.Count);
// Get available links
userInfoLogger.LogDebug("[UserInfo] Querying available links for email: {Email}", email);
var links = (await conn.QueryAsync<Link>(@"
SELECT DISTINCT l.id, l.title, l.url, l.description
FROM links l
JOIN role_links rl ON l.id = rl.link_id
JOIN user_roles ur ON rl.role_id = ur.role_id
JOIN users u ON ur.user_id = u.id
WHERE u.email = @email
ORDER BY l.id",
new { email })).ToList();
userInfoLogger.LogInformation("[UserInfo] Found {LinkCount} links for user", links.Count);
userInfoLogger.LogInformation("[UserInfo] Returning user info successfully");
return Results.Ok(new UserInfo(userResult.Email, userResult.FullName, organization, roles, links));
}
catch (Exception ex)
{
userInfoLogger.LogError(ex, "[UserInfo] Error occurred: {Message}", ex.Message);
throw;
}
});
logger.LogInformation("[App] Application starting...");
logger.LogInformation("[App] Available endpoints:");
logger.LogInformation("[App] GET /api/health - Health check");
logger.LogInformation("[App] GET /api/user-identity - User identity information (requires authentication)");
logger.LogInformation("[App] GET /api/user-info - User information (requires authentication)");
app.Run();
static async Task<string> GetUserEmail(HttpContext context, AppConfig config, JwtValidator jwtValidator)
static Task<string> GetUserEmail(HttpContext context, AppConfig config, ILogger logger)
{
// Dev mode
if (config.InsecureDevMode)
logger.LogDebug("[GetUserEmail] Starting authentication check for path: {Path}", context.Request.Path);
logger.LogDebug("[GetUserEmail] Request method: {Method}", context.Request.Method);
logger.LogDebug("[GetUserEmail] User.Identity.IsAuthenticated: {IsAuthenticated}", context.User.Identity?.IsAuthenticated);
logger.LogInformation("[GetUserEmail] User.Identity.Name: {Name}", context.User.Identity?.Name ?? "NULL");
logger.LogDebug("[GetUserEmail] User.Identity.AuthenticationType: {AuthType}", context.User.Identity?.AuthenticationType);
// Log all claims
if (context.User.Identity?.IsAuthenticated == true)
{
Console.WriteLine($"INSECURE_DEV_MODE: Using email {config.InsecureDevEmail}");
return config.InsecureDevEmail!;
}
// Try Authorization header
if (context.Request.Headers.TryGetValue("Authorization", out var authHeader))
{
var headerValue = authHeader.ToString();
if (headerValue.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
logger.LogDebug("[GetUserEmail] All user claims:");
foreach (var claim in context.User.Claims)
{
var token = headerValue[7..];
var payload = await jwtValidator.ValidateToken(token);
if (payload.TryGetValue("email", out var emailClaim))
{
return emailClaim.ToString()!;
}
logger.LogDebug("[GetUserEmail] {ClaimType} = {ClaimValue}", claim.Type, claim.Value);
}
}
// Log all headers
logger.LogDebug("[GetUserEmail] Request headers:");
foreach (var header in context.Request.Headers)
{
if (header.Key.ToLower().Contains("auth") || header.Key.ToLower().Contains("email"))
{
logger.LogDebug("[GetUserEmail] {HeaderKey} = {HeaderValue}", header.Key, header.Value);
}
}
// Try OAuth2 proxy header
if (context.Request.Headers.TryGetValue("X-Auth-Request-Email", out var emailHeader))
// Dev mode
if (config.InsecureDevMode)
{
return emailHeader.ToString();
logger.LogInformation("[GetUserEmail] INSECURE_DEV_MODE: Using email {Email}", config.InsecureDevEmail);
return Task.FromResult(config.InsecureDevEmail!);
}
// Try JWT token from authenticated user
if (context.User.Identity?.IsAuthenticated == true)
{
logger.LogDebug("[GetUserEmail] Checking JWT token claims for email");
var emailClaim = context.User.FindFirst("email");
if (emailClaim != null)
{
logger.LogInformation("[GetUserEmail] Found email in JWT claim: {Email}", emailClaim.Value);
return Task.FromResult(emailClaim.Value);
}
else
{
logger.LogWarning("[GetUserEmail] No email claim found in JWT token");
}
}
else
{
logger.LogDebug("[GetUserEmail] User is not authenticated via JWT");
}
// Try OAuth2 proxy header
logger.LogDebug("[GetUserEmail] Checking OAuth2 proxy headers");
if (context.Request.Headers.TryGetValue("X-Auth-Request-Email", out var emailHeader))
{
logger.LogInformation("[GetUserEmail] Found email in OAuth2 proxy header: {Email}", emailHeader.ToString());
return Task.FromResult(emailHeader.ToString());
}
else
{
logger.LogDebug("[GetUserEmail] No X-Auth-Request-Email header found");
}
logger.LogError("[GetUserEmail] No authentication information found - throwing UnauthorizedAccessException");
throw new UnauthorizedAccessException("No authentication information found");
}

View File

@@ -24,11 +24,8 @@ docker run -p 8000:8000 \
- **Minimal APIs**: современный подход ASP.NET Core без лишнего бойлерплейта
- **Records**: для моделей данных (immutable, concise)
- **Dapper**: микро-ORM для чистой и производительной работы с БД
- **Dommel**: расширение Dapper для CRUD операций
- **OpenIdConnect**: стандартный механизм получения JWKS
- **Npgsql**: официальный PostgreSQL provider для .NET
- **Async/await**: полностью асинхронный код
- **Top-level statements**: без Program/Main класса
- **OpenIdConnect**: стандартный механизм получения JWKS
## API

View File

@@ -1,8 +1,17 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"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"

View File

@@ -0,0 +1,20 @@
{
"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": "*"
}

View File

@@ -1,8 +1,16 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"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": "*"

View File

@@ -9,6 +9,7 @@
<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" />

16
frontend/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,16 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
}

View File

@@ -1,29 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

View File

@@ -4,6 +4,11 @@ server {
root /usr/share/nginx/html;
index index.html;
# MIME types for JavaScript modules
location ~* \.js$ {
add_header Content-Type application/javascript;
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;

File diff suppressed because it is too large Load Diff

View File

@@ -10,18 +10,17 @@
"preview": "vite preview"
},
"dependencies": {
"react": "^19.1.1",
"react-dom": "^19.1.1"
"@vitejs/plugin-react-swc": "3.7.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.3",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0",
"vite": "^7.1.7"
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0",
"globals": "^13.0.0",
"vite": "^5.1.5"
}
}

View File

@@ -1,5 +1,5 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import react from '@vitejs/plugin-react-swc'
// https://vite.dev/config/
export default defineConfig({
@@ -7,7 +7,7 @@ export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
target: process.env.VITE_API_TARGET || 'http://localhost:8000',
changeOrigin: true,
}
}

View File

@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "backend-dotnet", "backend-dotnet\backend-dotnet.csproj", "{B64A07E7-3E98-4140-9DC1-8D250A28AA37}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B64A07E7-3E98-4140-9DC1-8D250A28AA37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B64A07E7-3E98-4140-9DC1-8D250A28AA37}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B64A07E7-3E98-4140-9DC1-8D250A28AA37}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B64A07E7-3E98-4140-9DC1-8D250A28AA37}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B1D8661E-6857-4C1D-B730-52D71E82A6A9}
EndGlobalSection
EndGlobal