using System.Text.Json; using Inventory.Api.Models; using Inventory.Api.Services; using Inventory.Core; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using QuestPDF.Infrastructure; QuestPDF.Settings.License = LicenseType.Community; var builder = WebApplication.CreateBuilder(args); // Setup secrets from .env or Vault EnvironmentBuilder.SetupEnvironment(builder.Environment); var dbCon = Secrets.DbConnectionString; if (string.IsNullOrWhiteSpace(dbCon)) { throw new InvalidOperationException("FATAL ERROR: DB_CONNECTION_STRING is not configured."); } builder.Services.AddDbContext(options => options.UseSqlServer(dbCon!)); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddCors(options => { options.AddPolicy("AllowFlutterApp", policy => { // In production, you should lock this down to your Flutter app's domain. policy.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); }); }); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } // Add a redirect from the root URL to the Swagger UI for convenience in development. app.MapGet("/", (HttpContext context) => { context.Response.Redirect("/swagger"); return Task.CompletedTask; }).ExcludeFromDescription(); // Exclude this from the OpenAPI spec // Only use HTTPS redirection in non-development environments. // This prevents the "Failed to determine the https port for redirect" warning during local development. if (!app.Environment.IsDevelopment()) { app.UseHttpsRedirection(); } app.UseCors("AllowFlutterApp"); var api = app.MapGroup("/api"); var dashboardApi = api.MapGroup("/dashboard"); var devicesApi = api.MapGroup("/devices"); var exportApi = api.MapGroup("/export"); // --- Dashboard Endpoints --- dashboardApi.MapGet("/summary", async (InventoryContext db) => { var devices = await db.Devices.AsNoTracking().ToListAsync(); var printersWithSerial = 0; var printersWithoutSerial = 0; foreach (var device in devices.Where(d => !string.IsNullOrEmpty(d.Printers))) { try { var printers = JsonSerializer.Deserialize>(device.Printers!); printersWithSerial += printers?.Count(p => !string.IsNullOrWhiteSpace(p.SerialNumber)) ?? 0; printersWithoutSerial += printers?.Count(p => string.IsNullOrWhiteSpace(p.SerialNumber)) ?? 0; } catch { /* Ignore deserialization errors */ } } var drivesFailing = 0; foreach (var device in devices.Where(d => !string.IsNullOrEmpty(d.DriveHealth))) { try { var drives = JsonSerializer.Deserialize>(device.DriveHealth!); if (drives?.Any(d => d.IsFailing == true) == true) { drivesFailing++; } } catch { /* Ignore deserialization errors */ } } return Results.Ok(new { TotalDevices = devices.Count, TotalLaptops = devices.Count(d => d.DeviceType == "Laptop"), TotalDesktops = devices.Count(d => d.DeviceType == "Desktop"), TotalServers = devices.Count(d => d.DeviceType == "Server"), PrintersWithSerial = printersWithSerial, PrintersWithoutSerial = printersWithoutSerial, DrivesFailing = drivesFailing }); }); dashboardApi.MapGet("/os-distribution", async (InventoryContext db) => { var osDistribution = await db.Devices .AsNoTracking() .GroupBy(d => d.OSVersion) .Select(g => new { OSVersion = g.Key, Count = g.Count() }) .OrderByDescending(x => x.Count) .ToListAsync(); return Results.Ok(osDistribution); }); dashboardApi.MapGet("/storage-by-type", async (InventoryContext db) => { var devicesWithStorage = await db.Devices .AsNoTracking() .Where(d => !string.IsNullOrEmpty(d.StorageDevices)) .Select(d => d.StorageDevices) .ToListAsync(); ulong totalSsd = 0; ulong totalHdd = 0; foreach (var storageJson in devicesWithStorage) { try { var drives = JsonSerializer.Deserialize>(storageJson!); if (drives == null) continue; totalSsd += (ulong)drives.Where(d => d.MediaType == "SSD").Sum(d => (long)d.Size); totalHdd += (ulong)drives.Where(d => d.MediaType == "HDD").Sum(d => (long)d.Size); } catch { /* Ignore deserialization errors */ } } return Results.Ok(new { TotalSsdBytes = totalSsd, TotalHddBytes = totalHdd }); }); dashboardApi.MapGet("/admin-accounts", async (InventoryContext db) => { var devicesWithAdmins = await db.Devices .AsNoTracking() .Where(d => !string.IsNullOrEmpty(d.LocalAdmins)) .Select(d => d.LocalAdmins) .ToListAsync(); var adminCounts = new Dictionary(); foreach (var adminJson in devicesWithAdmins) { try { var admins = JsonSerializer.Deserialize>(adminJson!); if (admins == null) continue; foreach (var admin in admins.Where(a => !string.IsNullOrWhiteSpace(a.Name))) { adminCounts.TryGetValue(admin.Name!, out var currentCount); adminCounts[admin.Name!] = currentCount + 1; } } catch { /* Ignore deserialization errors */ } } var topAdmins = adminCounts.OrderByDescending(kv => kv.Value) .Take(5) .Select(kv => new { AccountName = kv.Key, Count = kv.Value }); return Results.Ok(topAdmins); }); // --- Device Endpoints --- devicesApi.MapGet("/", async (InventoryContext db, string? deviceType, string? processor, string? ram, string? location, string? sortBy, bool sortAscending = true, int page = 1, int pageSize = 25) => { var query = db.Devices.AsNoTracking(); // Filtering if (!string.IsNullOrWhiteSpace(deviceType)) query = query.Where(d => d.DeviceType == deviceType); if (!string.IsNullOrWhiteSpace(processor)) query = query.Where(d => d.Processor != null && d.Processor.Contains(processor)); if (!string.IsNullOrWhiteSpace(ram)) query = query.Where(d => d.RAM == ram); if (!string.IsNullOrWhiteSpace(location)) query = query.Where(d => d.Location != null && d.Location.Contains(location)); // Sorting if (!string.IsNullOrWhiteSpace(sortBy)) { // This is a simplified sort. A more robust solution would use reflection or a dictionary. query = sortBy.ToLowerInvariant() switch { "computername" => sortAscending ? query.OrderBy(d => d.ComputerName) : query.OrderByDescending(d => d.ComputerName), "devicetype" => sortAscending ? query.OrderBy(d => d.DeviceType) : query.OrderByDescending(d => d.DeviceType), "location" => sortAscending ? query.OrderBy(d => d.Location) : query.OrderByDescending(d => d.Location), "lastseen" => sortAscending ? query.OrderBy(d => d.LastSeen) : query.OrderByDescending(d => d.LastSeen), _ => query.OrderBy(d => d.Id) }; } else { query = query.OrderBy(d => d.Id); } var totalCount = await query.CountAsync(); var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); return Results.Ok(new PaginatedResult { PageNumber = page, PageSize = pageSize, TotalCount = totalCount, Items = items }); }); devicesApi.MapGet("/{id:int}", async (InventoryContext db, int id) => { var device = await db.Devices.AsNoTracking().FirstOrDefaultAsync(d => d.Id == id); return device is not null ? Results.Ok(device) : Results.NotFound(); }); // --- Export Endpoints --- async Task> GetFilteredDevicesQuery(InventoryContext db, string? deviceType, string? processor, string? ram, string? location) { var query = db.Devices.AsNoTracking(); if (!string.IsNullOrWhiteSpace(deviceType)) query = query.Where(d => d.DeviceType == deviceType); if (!string.IsNullOrWhiteSpace(processor)) query = query.Where(d => d.Processor != null && d.Processor.Contains(processor)); if (!string.IsNullOrWhiteSpace(ram)) query = query.Where(d => d.RAM == ram); if (!string.IsNullOrWhiteSpace(location)) query = query.Where(d => d.Location != null && d.Location.Contains(location)); return await Task.FromResult(query.OrderBy(d => d.ComputerName)); } exportApi.MapGet("/excel", async (InventoryContext db, [FromQuery] string? deviceType, [FromQuery] string? processor, [FromQuery] string? ram, [FromQuery] string? location) => { var query = await GetFilteredDevicesQuery(db, deviceType, processor, ram, location); var devices = await query.ToListAsync(); var fileBytes = ReportGenerator.GenerateExcel(devices); return Results.File(fileBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "DeviceInventory.xlsx"); }); exportApi.MapGet("/pdf", async (InventoryContext db, [FromQuery] string? deviceType, [FromQuery] string? processor, [FromQuery] string? ram, [FromQuery] string? location) => { var query = await GetFilteredDevicesQuery(db, deviceType, processor, ram, location); var devices = await query.ToListAsync(); var fileBytes = ReportGenerator.GeneratePdf(devices); return Results.File(fileBytes, "application/pdf", "DeviceInventory.pdf"); }); app.Run();