using System.Security.Claims; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using TheCr3w.Shared.Crypto; using TheCr3w.Shared.Data; using TheCr3w.Shared.Models; using TheCr3w.Shared.Storage; var builder = WebApplication.CreateBuilder(args); // Konfiguracja Bazy Danych PostgreSQL if (!builder.Environment.IsEnvironment("Testing")) { var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? "Host=localhost;Port=5432;Database=thecr3w_db;Username=thecr3w_user;Password=thecr3w_pass"; builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); } // Konfiguracja Cloudflare R2 Storage var r2Options = new R2StorageOptions { Endpoint = builder.Configuration["R2:Endpoint"] ?? "https://bf705752e38169fedb83efd5b747390e.r2.cloudflarestorage.com", BucketName = builder.Configuration["R2:Bucket"] ?? "thecr3w-app", Region = builder.Configuration["R2:Region"] ?? "auto", AccessKeyId = builder.Configuration["R2:AccessKeyId"] ?? "dev_access_key", SecretAccessKey = builder.Configuration["R2:SecretAccessKey"] ?? "dev_secret_key" }; builder.Services.AddSingleton(r2Options); builder.Services.AddSingleton(); // Konfiguracja Kluczy Podpisujących Ed25519 var signingPrivateKey = builder.Configuration["Signing:Ed25519PrivateKeyHex"]; var signingPublicKey = builder.Configuration["Signing:Ed25519PublicKeyHex"]; // Jeśli klucze nie są zdefiniowane w środowisku, generujemy parę deweloperską (w produkcji podawane przez env) if (string.IsNullOrEmpty(signingPrivateKey) || string.IsNullOrEmpty(signingPublicKey)) { var (priv, pub) = Ed25519Service.GenerateKeyPair(); signingPrivateKey = priv; signingPublicKey = pub; } // Konfiguracja Uwierzytelniania Keycloak OIDC (Realm master) var keycloakAuthority = builder.Configuration["Keycloak:Authority"] ?? "https://login.thecr3wnet.com/realms/master"; var keycloakAudience = builder.Configuration["Keycloak:Audience"] ?? "thecr3w-backend"; builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = keycloakAuthority; options.Audience = keycloakAudience; options.RequireHttpsMetadata = !builder.Environment.IsDevelopment(); options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = keycloakAuthority, ValidateAudience = true, ValidAudience = keycloakAudience, ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(1) }; }); builder.Services.AddAuthorization(options => { options.AddPolicy("AdminOnly", policy => policy.RequireClaim(ClaimTypes.Role, "thecr3w:admin")); options.AddPolicy("ReleaseManager", policy => policy.RequireClaim(ClaimTypes.Role, "thecr3w:release-manager", "thecr3w:admin")); }); builder.Services.AddCors(options => { options.AddPolicy("AllowPublic", p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); }); var app = builder.Build(); app.UseCors("AllowPublic"); app.UseAuthentication(); app.UseAuthorization(); // ------------------------------------------------------------------------------ // ENDPOINTY PUBLICZNE DLA KLIENTÓW I DOWNLOAD CENTER // ------------------------------------------------------------------------------ app.MapGet("/health", () => Results.Ok(new { status = "Healthy", timestamp = DateTimeOffset.UtcNow })); // 1. Sprawdzanie aktualizacji dla aplikacji natywnych (Android & Windows) app.MapGet("/api/v1/updates/check", async ( string app_id, string current_version, int version_code, string platform, string architecture, string? channel, TheCr3wDbContext db, IR2StorageService r2) => { var targetChannel = (channel ?? "stable").ToLowerInvariant(); var plat = platform.ToLowerInvariant(); var arch = architecture.ToLowerInvariant(); var latestRelease = await db.Releases .Where(r => r.AppId == app_id && r.Platform == plat && (r.Architecture == arch || r.Architecture == "universal") && r.Channel == targetChannel && r.Status == "published") .OrderByDescending(r => r.VersionCode) .FirstOrDefaultAsync(); if (latestRelease == null || latestRelease.VersionCode <= version_code) { return Results.Ok(new { app_id, update_available = false, latest_version = current_version, version_code }); } // Generujemy Pre-signed URL do pobrania z Cloudflare R2 var downloadUrl = r2.GeneratePreSignedDownloadUrl(latestRelease.R2ObjectKey, TimeSpan.FromMinutes(15)); return Results.Ok(new { app_id = latestRelease.AppId, update_available = true, release_id = latestRelease.ReleaseId, latest_version = latestRelease.Version, version_code = latestRelease.VersionCode, release_channel = latestRelease.Channel, release_notes = latestRelease.ReleaseNotes, mandatory = latestRelease.Mandatory, minimum_supported_version = latestRelease.MinimumSupportedVersion, file_size = latestRelease.FileSizeBytes, sha256 = latestRelease.Sha256, signature_ed25519 = latestRelease.SignatureEd25519, download_url = downloadUrl, published_at = latestRelease.PublishedAt }); }); // 2. Pobranie aktywnego klucza publicznego Ed25519 app.MapGet("/api/v1/keys/signing-public-key", () => { return Results.Ok(new { active_key_id = "master-ed25519-v1", algorithm = "Ed25519", public_key_hex = signingPublicKey }); }); // 3. Pobranie listy publicznych aplikacji dla The Cr3w Download Center app.MapGet("/api/v1/apps", async (TheCr3wDbContext db) => { var apps = await db.Applications .Where(a => a.IsPublic) .Include(a => a.Platforms) .Select(a => new { a.AppId, a.Name, a.Description, a.IconUrl, Platforms = a.Platforms.Select(p => p.Platform).ToList(), a.DefaultChannel, a.SystemRequirements }) .ToListAsync(); return Results.Ok(apps); }); // 4. Pobranie szczegółów aplikacji dla Download Center app.MapGet("/api/v1/apps/{app_id}", async (string app_id, TheCr3wDbContext db) => { var appEntity = await db.Applications .Include(a => a.Platforms) .FirstOrDefaultAsync(a => a.AppId == app_id && a.IsPublic); if (appEntity == null) return Results.NotFound(new { error = "Application not found" }); var latestRelease = await db.Releases .Where(r => r.AppId == app_id && r.Status == "published" && r.Channel == "stable") .OrderByDescending(r => r.VersionCode) .FirstOrDefaultAsync(); return Results.Ok(new { appEntity.AppId, appEntity.Name, appEntity.Description, appEntity.IconUrl, Platforms = appEntity.Platforms.Select(p => p.Platform).ToList(), appEntity.DefaultChannel, appEntity.SystemRequirements, LatestStableRelease = latestRelease != null ? new { latestRelease.ReleaseId, latestRelease.Version, latestRelease.VersionCode, latestRelease.Platform, latestRelease.Architecture, latestRelease.FileSizeBytes, latestRelease.Sha256, latestRelease.PublishedAt, latestRelease.ReleaseNotes } : null }); }); // 5. Pobranie historii wydań dla Download Center app.MapGet("/api/v1/apps/{app_id}/releases", async (string app_id, string? channel, TheCr3wDbContext db) => { var targetChannel = (channel ?? "stable").ToLowerInvariant(); var releases = await db.Releases .Where(r => r.AppId == app_id && r.Channel == targetChannel && r.Status == "published") .OrderByDescending(r => r.VersionCode) .Select(r => new { r.ReleaseId, r.Version, r.VersionCode, r.Platform, r.Architecture, r.Channel, r.PublishedAt, file_size = r.FileSizeBytes, r.Sha256, r.ReleaseNotes }) .ToListAsync(); return Results.Ok(releases); }); // 6. Pobranie bezpiecznego (Pre-signed S3) URL pobierania dla Download Center app.MapGet("/api/v1/apps/{app_id}/releases/{release_id:guid}/download", async ( string app_id, Guid release_id, TheCr3wDbContext db, IR2StorageService r2) => { var release = await db.Releases .FirstOrDefaultAsync(r => r.AppId == app_id && r.ReleaseId == release_id && r.Status == "published"); if (release == null) return Results.NotFound(new { error = "Release not found or not published" }); var downloadUrl = r2.GeneratePreSignedDownloadUrl(release.R2ObjectKey, TimeSpan.FromMinutes(15)); return Results.Ok(new { download_url = downloadUrl, expires_at = DateTimeOffset.UtcNow.AddMinutes(15), file_size = release.FileSizeBytes, sha256 = release.Sha256, signature_ed25519 = release.SignatureEd25519 }); }); // 7. Raportowanie instalacji z aplikacji klienckiej app.MapPost("/api/v1/apps/{app_id}/reports/install", async ( string app_id, InstallationReportEntity report, TheCr3wDbContext db) => { report.ReportId = Guid.NewGuid(); report.ReportedAt = DateTimeOffset.UtcNow; db.InstallationReports.Add(report); await db.SaveChangesAsync(); return Results.Accepted(); }); // 8. Raportowanie błędów aktualizacji app.MapPost("/api/v1/apps/{app_id}/reports/error", async ( string app_id, ErrorReportEntity report, TheCr3wDbContext db) => { report.ErrorId = Guid.NewGuid(); report.AppId = app_id; report.ReportedAt = DateTimeOffset.UtcNow; db.ErrorReports.Add(report); await db.SaveChangesAsync(); return Results.Accepted(); }); // ------------------------------------------------------------------------------ // ENDPOINTY ADMINISTRACYJNE (Weryfikacja ról Keycloak OIDC) // ------------------------------------------------------------------------------ // Publikacja wydania z wyliczeniem podpisu Ed25519 app.MapPost("/api/v1/admin/apps/{app_id}/releases/{release_id:guid}/publish", async ( string app_id, Guid release_id, TheCr3wDbContext db) => { var release = await db.Releases.FirstOrDefaultAsync(r => r.AppId == app_id && r.ReleaseId == release_id); if (release == null) return Results.NotFound(new { error = "Release not found" }); // Przygotowanie kanonicznych metadanych var metadata = new ReleaseMetadata { ReleaseId = release.ReleaseId, AppId = release.AppId, Version = release.Version, VersionCode = release.VersionCode, Platform = release.Platform, Architecture = release.Architecture, Channel = release.Channel, Mandatory = release.Mandatory, MinimumSupportedVersion = release.MinimumSupportedVersion ?? release.Version, FileSize = release.FileSizeBytes, Sha256 = release.Sha256, PublishedAt = DateTimeOffset.UtcNow, ReleaseNotes = release.ReleaseNotes ?? string.Empty }; var canonicalBytes = Ed25519Service.GetCanonicalBytes(metadata); var signature = Ed25519Service.Sign(canonicalBytes, signingPrivateKey); release.SignatureEd25519 = signature; release.PublishedAt = metadata.PublishedAt; release.Status = "published"; release.SigningKeyId = "master-ed25519-v1"; db.AuditEvents.Add(new AuditEventEntity { Actor = "admin", Action = "RELEASE_PUBLISHED", ResourceType = "release", ResourceId = release.ReleaseId.ToString(), CorrelationId = Guid.NewGuid(), DetailsJson = System.Text.Json.JsonSerializer.Serialize(new { release.AppId, release.Version, release.Channel }) }); await db.SaveChangesAsync(); return Results.Ok(new { release.ReleaseId, release.AppId, release.Version, release.VersionCode, release.Status, release.SignatureEd25519, release.PublishedAt }); }); // Generowanie Pre-signed Upload URL do Cloudflare R2 app.MapPost("/api/v1/admin/storage/upload-url", ( string app_id, string filename, string content_type, IR2StorageService r2) => { var objectKey = $"apps/{app_id}/uploads/{Guid.NewGuid()}_{filename}"; var uploadUrl = r2.GeneratePreSignedUploadUrl(objectKey, content_type, TimeSpan.FromMinutes(30)); return Results.Ok(new { upload_url = uploadUrl, object_key = objectKey, expires_in_seconds = 1800 }); }); app.Run(); namespace NativeUpdate.Api { public partial class Program { } }