using Microsoft.EntityFrameworkCore; using w4c_workflows.Data; using w4c_workflows.Models; namespace w4c_workflows.Services; /// /// Reads and updates the tenant's selected workflow repository name. The repo name /// is a basename (e.g. workflows by default, or a user-selected repo like /// wiz4apps). The full repo is {login}/{repoName}; its local clone dir /// is {CopiesRoot}/{tenantId}/{login}_{repoName} (see /// ). /// /// The selection is durable per tenant (table WorkflowRepos) and falls back /// to WorkflowSource:WorkflowRepoName (default workflows) when no row /// exists — so a fresh tenant automatically uses the repository named "workflow". /// public sealed class WorkflowRepoStore { private readonly WorkflowsDbContext _db; private readonly string _defaultName; public WorkflowRepoStore(WorkflowsDbContext db, Microsoft.Extensions.Configuration.IConfiguration config) { _db = db; var configured = config["WorkflowSource:WorkflowRepoName"]?.Trim(); _defaultName = string.IsNullOrWhiteSpace(configured) ? "workflows" : configured; } /// The configured default repo name (used when no per-tenant row exists). public string DefaultName => _defaultName; /// Resolves the tenant's current workflow repo name (stored or default). public async Task GetNameAsync(string tenantId, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(tenantId)) return _defaultName; var row = await _db.WorkflowRepos.AsNoTracking().FirstOrDefaultAsync(r => r.TenantId == tenantId, ct); return string.IsNullOrWhiteSpace(row?.RepoName) ? _defaultName : row!.RepoName; } /// /// Persists the tenant's workflow repo name. Returns the normalized name (trimmed, /// no leading/trailing slashes). /// public async Task SetNameAsync(string tenantId, string repoName, CancellationToken ct = default) { var clean = (repoName ?? string.Empty).Trim().Trim('/'); if (string.IsNullOrWhiteSpace(clean)) throw new ArgumentException("Workflow repo name is required", nameof(repoName)); var row = await _db.WorkflowRepos.FirstOrDefaultAsync(r => r.TenantId == tenantId, ct); if (row == null) { _db.WorkflowRepos.Add(new TenantWorkflowRepo { TenantId = tenantId, RepoName = clean, UpdatedAt = DateTime.UtcNow, }); } else { row.RepoName = clean; row.UpdatedAt = DateTime.UtcNow; } await _db.SaveChangesAsync(ct); return clean; } /// /// Validates a workflow repo basename. The repo is always owned by the /// caller's own Forgejo account, so only a single safe name segment is /// accepted — no owner prefix, path separators or traversal. Returns false /// for anything else. /// public static bool TryNormalizeName(string? repoName, out string normalized) { normalized = string.Empty; var clean = (repoName ?? string.Empty).Trim(); if (clean.Length == 0 || clean.Length > 120) return false; foreach (var ch in clean) { if (!(char.IsLetterOrDigit(ch) || ch is '.' or '_' or '-')) return false; } // A name made only of dots ("..", ".") is not a repo. if (clean.Trim('.').Length == 0) return false; normalized = clean; return true; } }