thecr3w-dev/backend/NativeUpdate.Api/Program.cs

382 lines
13 KiB
C#

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<TheCr3wDbContext>(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<IR2StorageService, R2StorageService>();
// 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 { }
}