namespace marlin.web.Controllers; using System.Text.Json; using core; using core.models; using Data; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Models; using Services; [Route("vcs")] public class VCSController(ApplicationDbContext dbContext, VersionControlSyncService syncService) : Controller { [HttpGet("list")] public async Task List(CancellationToken ct) { var items = await dbContext.Sources .Include(x => x.Branches) .ToListAsync(ct); return this.View(items); } public record SecretOption(Guid Id, string Label); [HttpGet("details/{id}")] public async Task Details(Guid id, CancellationToken ct) { var item = await dbContext.Sources .Include(x => x.Branches) .ThenInclude(x => x.Commit) .FirstOrDefaultAsync(x => x.Id == id, ct); if (item == null) return this.NotFound(); // Only Id/Label projected — a secret's Content never needs to leave the secrets pages. var secretOptions = await dbContext.Secrets .Where(x => x.SourceId == id) .Select(x => new SecretOption(x.Id, x.Label)) .ToListAsync(ct); return this.View((item, secretOptions)); } [HttpPost("edit/{id}")] public async Task Edit(Guid id, VersionControlSource vcs, CancellationToken ct) { var item = await dbContext.Sources .FirstOrDefaultAsync(x => x.Id == id, ct); if (item == null) return this.NotFound(); item.UpstreamUrl = vcs.UpstreamUrl; item.BranchFilter = vcs.BranchFilter; item.MainBranch = vcs.MainBranch; item.MinPollingInterval = vcs.MinPollingInterval; item.AccessSecretId = vcs.AccessSecretId; await dbContext.SaveChangesAsync(ct); await syncService.FetchSources(ct); return this.RedirectToAction("List"); } [HttpGet("create")] public async Task Create() { return this.View(); } [HttpPost("create")] public async Task CreatePost(VersionControlSource vcs, CancellationToken ct) { vcs.Id = Guid.Empty; await dbContext.AddAsync(vcs, ct); await dbContext.SaveChangesAsync(ct); await syncService.FetchSources(ct); return this.RedirectToAction("Details", new { id = vcs.Id }); } public record SecretCreateRequest(string Label, string? Kind, bool MayReveal, string ContentJson); [HttpGet("details/{sourceId}/secrets/create")] public async Task CreateSecret(Guid sourceId, CancellationToken ct) { var source = await dbContext.Sources .FirstOrDefaultAsync(x => x.Id == sourceId, ct); if (source == null) return this.NotFound(); return this.View(source); } [HttpPost("details/{sourceId}/secrets/create")] public async Task CreateSecretPost(Guid sourceId, SecretCreateRequest request, CancellationToken ct) { var source = await dbContext.Sources .FirstOrDefaultAsync(x => x.Id == sourceId, ct); if (source == null) return this.NotFound(); var content = JsonSerializer.Deserialize>(request.ContentJson) ?? new(); var secret = new VersionControlSecret { SourceId = sourceId, Label = request.Label, Kind = request.Kind, MayReveal = request.MayReveal, Content = content }; await dbContext.Secrets.AddAsync(secret, ct); await dbContext.SaveChangesAsync(ct); return this.RedirectToAction("ListSecret", new { sourceId }); } [HttpGet("details/{sourceId}/secrets/list")] public async Task ListSecret(Guid sourceId, CancellationToken ct) { var source = await dbContext.Sources .FirstOrDefaultAsync(x => x.Id == sourceId, ct); if (source == null) return this.NotFound(); var items = await dbContext.Secrets .Where(x => x.SourceId == source.Id) .ToListAsync(ct); return this.View((source, items)); } [HttpPost("details/{sourceId}/secrets/delete/{id}")] public async Task DeleteSecret(Guid sourceId, Guid id, CancellationToken ct) { var secret = await dbContext.Secrets .FirstOrDefaultAsync(x => x.SourceId == sourceId && x.Id == id, ct); if (secret == null) return this.NotFound(); dbContext.Secrets.Remove(secret); await dbContext.SaveChangesAsync(ct); return this.RedirectToAction("ListSecret", new { sourceId = sourceId }); } }