187 lines
7.6 KiB
C#
187 lines
7.6 KiB
C#
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using JobTrackerApi.Data;
|
|
using JobTrackerApi.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace JobTrackerApi.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/admin/system")]
|
|
[Authorize(Roles = "Admin")]
|
|
public sealed class AdminSystemController : ControllerBase
|
|
{
|
|
private readonly IConfiguration _cfg;
|
|
private readonly AppPaths _paths;
|
|
private readonly JobTrackerContext _db;
|
|
private readonly ISummarizerService _summarizer;
|
|
private readonly IWebHostEnvironment _env;
|
|
|
|
public AdminSystemController(IConfiguration cfg, AppPaths paths, JobTrackerContext db, ISummarizerService summarizer, IWebHostEnvironment env)
|
|
{
|
|
_cfg = cfg;
|
|
_paths = paths;
|
|
_db = db;
|
|
_summarizer = summarizer;
|
|
_env = env;
|
|
}
|
|
|
|
public sealed record StorageStatusDto(string DataRoot, string DbPath, bool DbExists, long? DbSizeBytes, int CompanyCount, int JobCount, int DeletedCount);
|
|
public sealed record EmailStatusDto(bool Enabled, string? Host, int Port, bool EnableSsl, string? From, string? FromName);
|
|
public sealed record DatabaseStatusDto(string Provider, bool LooksConfigured, bool CanConnect, string? Target, bool UsesFileStorage, string? Warning);
|
|
public sealed record RuntimeStatusDto(string Framework, string OSDescription, string ProcessArchitecture, string? MachineName);
|
|
public sealed record AuthStatusDto(bool Required, bool HasJwtKey, bool GoogleConfigured, bool GmailConfigured);
|
|
public sealed record SystemStatusDto(
|
|
string Environment,
|
|
string ContentRoot,
|
|
string Version,
|
|
string? CommitSha,
|
|
string? BuildStamp,
|
|
StorageStatusDto Storage,
|
|
EmailStatusDto Email,
|
|
DatabaseStatusDto Database,
|
|
RuntimeStatusDto Runtime,
|
|
AuthStatusDto Auth,
|
|
SummarizerMetrics Summarizer
|
|
);
|
|
|
|
private static string? NormalizeBuildMetadata(string? value)
|
|
{
|
|
var trimmed = (value ?? string.Empty).Trim();
|
|
if (string.IsNullOrWhiteSpace(trimmed)) return null;
|
|
|
|
// Ignore unresolved shell/compose placeholders that would otherwise leak
|
|
// directly into the admin UI, e.g. $(git rev-parse --short HEAD) or ${APP_COMMIT_SHA}.
|
|
if ((trimmed.StartsWith("$(") && trimmed.EndsWith(")")) || (trimmed.StartsWith("${") && trimmed.EndsWith("}")))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return trimmed;
|
|
}
|
|
|
|
[HttpPost("summarizer/probe")]
|
|
public async Task<IActionResult> RunSummarizerProbe(CancellationToken cancellationToken)
|
|
{
|
|
await _summarizer.RunProbeAsync(cancellationToken);
|
|
return NoContent();
|
|
}
|
|
|
|
[HttpGet]
|
|
public async Task<ActionResult<SystemStatusDto>> Get(CancellationToken cancellationToken)
|
|
{
|
|
var provider = (_cfg["Database:Provider"] ?? "sqlite").Trim().ToLowerInvariant();
|
|
var usesFileStorage = provider is not "mysql" and not "mariadb";
|
|
var dbPath = _paths.GetDbPath();
|
|
var dbFile = new FileInfo(dbPath);
|
|
|
|
var jobs = await _db.JobApplications.AsNoTracking().ToListAsync(cancellationToken);
|
|
var companies = await _db.Companies.AsNoTracking().CountAsync(cancellationToken);
|
|
var summarizer = await _summarizer.GetMetricsAsync(cancellationToken);
|
|
|
|
var version = NormalizeBuildMetadata(_cfg["App:Version"]);
|
|
if (string.IsNullOrWhiteSpace(version))
|
|
{
|
|
version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
|
|
}
|
|
|
|
var commitSha = NormalizeBuildMetadata(_cfg["App:CommitSha"]);
|
|
var buildStamp = NormalizeBuildMetadata(_cfg["App:BuildStamp"]);
|
|
var connectionString = _cfg.GetConnectionString("JobTracker") ?? string.Empty;
|
|
var looksConfigured = !string.IsNullOrWhiteSpace(connectionString) || usesFileStorage;
|
|
|
|
string? dbTarget;
|
|
if (usesFileStorage)
|
|
{
|
|
dbTarget = dbPath;
|
|
}
|
|
else if (string.IsNullOrWhiteSpace(connectionString))
|
|
{
|
|
dbTarget = null;
|
|
}
|
|
else
|
|
{
|
|
dbTarget = connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(part => part.Trim())
|
|
.FirstOrDefault(part => part.StartsWith("server=", StringComparison.OrdinalIgnoreCase)
|
|
|| part.StartsWith("host=", StringComparison.OrdinalIgnoreCase)
|
|
|| part.StartsWith("database=", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
bool canConnect;
|
|
try
|
|
{
|
|
canConnect = await _db.Database.CanConnectAsync(cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
canConnect = false;
|
|
}
|
|
|
|
string? dbWarning = null;
|
|
if (!looksConfigured)
|
|
{
|
|
dbWarning = "Connection string is missing.";
|
|
}
|
|
else if (!canConnect)
|
|
{
|
|
dbWarning = "Database connection failed.";
|
|
}
|
|
else if (!usesFileStorage && jobs.Count == 0 && companies == 0)
|
|
{
|
|
dbWarning = "Connected, but no data is present yet. Check whether this is the intended production database.";
|
|
}
|
|
|
|
var gmailConfigured = !string.IsNullOrWhiteSpace((_cfg["Google:GmailClientSecret"] ?? string.Empty).Trim())
|
|
&& !string.IsNullOrWhiteSpace((_cfg["Google:GmailRedirectUri"] ?? string.Empty).Trim());
|
|
|
|
return Ok(new SystemStatusDto(
|
|
Environment: _env.EnvironmentName,
|
|
ContentRoot: _env.ContentRootPath,
|
|
Version: version,
|
|
CommitSha: string.IsNullOrWhiteSpace(commitSha) ? null : commitSha,
|
|
BuildStamp: string.IsNullOrWhiteSpace(buildStamp) ? null : buildStamp,
|
|
Storage: new StorageStatusDto(
|
|
DataRoot: _paths.DataRoot,
|
|
DbPath: dbPath,
|
|
DbExists: dbFile.Exists,
|
|
DbSizeBytes: dbFile.Exists ? dbFile.Length : null,
|
|
CompanyCount: companies,
|
|
JobCount: jobs.Count,
|
|
DeletedCount: jobs.Count(x => x.IsDeleted)
|
|
),
|
|
Email: new EmailStatusDto(
|
|
Enabled: _cfg.GetValue("Email:Enabled", false),
|
|
Host: (_cfg["Email:SmtpHost"] ?? string.Empty).Trim(),
|
|
Port: _cfg.GetValue("Email:SmtpPort", 587),
|
|
EnableSsl: _cfg.GetValue("Email:SmtpEnableSsl", true),
|
|
From: (_cfg["Email:From"] ?? string.Empty).Trim(),
|
|
FromName: (_cfg["Email:FromName"] ?? string.Empty).Trim()
|
|
),
|
|
Database: new DatabaseStatusDto(
|
|
Provider: provider,
|
|
LooksConfigured: looksConfigured,
|
|
CanConnect: canConnect,
|
|
Target: dbTarget,
|
|
UsesFileStorage: usesFileStorage,
|
|
Warning: dbWarning
|
|
),
|
|
Runtime: new RuntimeStatusDto(
|
|
Framework: RuntimeInformation.FrameworkDescription,
|
|
OSDescription: RuntimeInformation.OSDescription,
|
|
ProcessArchitecture: RuntimeInformation.ProcessArchitecture.ToString(),
|
|
MachineName: Environment.MachineName
|
|
),
|
|
Auth: new AuthStatusDto(
|
|
Required: _cfg.GetValue("Auth:Require", false),
|
|
HasJwtKey: !string.IsNullOrWhiteSpace((_cfg["Auth:JwtKey"] ?? string.Empty).Trim()),
|
|
GoogleConfigured: !string.IsNullOrWhiteSpace((_cfg["Auth:GoogleClientId"] ?? string.Empty).Trim()),
|
|
GmailConfigured: gmailConfigured
|
|
),
|
|
Summarizer: summarizer
|
|
));
|
|
}
|
|
}
|