382 lines
13 KiB
C#
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 { }
|
|
}
|