Changes
Build and Push Docker Images / build (push) Failing after 0s

This commit is contained in:
2026-05-08 14:09:58 +03:00
parent 51e668bf1d
commit c2896ce77b
6 changed files with 157 additions and 3 deletions
+1
View File
@@ -6,5 +6,6 @@ public sealed class JobMatchRequest
public string? JobUrl { get; set; }
public string? JobDescription { get; set; }
public bool GdprConsent { get; set; }
public string? Email { get; set; }
public string? CaptchaToken { get; set; }
}
+78
View File
@@ -1,8 +1,11 @@
using Api.Clients.Api.Contracts;
using Models.Requests;
using Models.Settings;
using Api.Services.Contracts;
using Api.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.Annotations;
using Shared.Models.Responses;
@@ -18,15 +21,21 @@ public sealed class CvMatcherController : ControllerBase
{
private readonly ICvMatcherApi _cvApi;
private readonly ICaptchaVerifier _captcha;
private readonly FileStorageSettings _fileStorageSettings;
private readonly IEmailSender _emailSender;
private readonly ILogger<CvMatcherController> _logger;
public CvMatcherController(
ICvMatcherApi cvApi,
ICaptchaVerifier captcha,
IOptions<FileStorageSettings> fileStorageSettings,
IEmailSender emailSender,
ILogger<CvMatcherController> logger)
{
_cvApi = cvApi;
_captcha = captcha;
_fileStorageSettings = fileStorageSettings.Value;
_emailSender = emailSender;
_logger = logger;
}
@@ -75,6 +84,8 @@ public sealed class CvMatcherController : ControllerBase
var stream = cv.OpenReadStream();
var part = new Refit.StreamPart(stream, cv.FileName, "application/pdf");
var res = await _cvApi.Upload(part, ct);
await CacheUploadedCvAsync(cv, res.DocumentId, ct);
return Ok(res);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
@@ -120,6 +131,18 @@ public sealed class CvMatcherController : ControllerBase
!string.IsNullOrWhiteSpace(request.JobUrl),
!string.IsNullOrWhiteSpace(request.JobDescription));
var res = await _cvApi.MatchJob(request, ct);
var attachmentPath = TryGetCachedCvPath(request.CvDocumentId);
var jobLabel = !string.IsNullOrWhiteSpace(request.JobUrl)
? request.JobUrl
: "Manual job description";
await _emailSender.SendMatchAsync(
request.Email,
SmtpEmailSender.BuildMatchEmailSubject(res.Score, jobLabel),
SmtpEmailSender.BuildMatchEmailBody(request.CvDocumentId ?? "N/A", res, jobLabel),
attachmentPath,
ct);
return Ok(res);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
@@ -133,4 +156,59 @@ public sealed class CvMatcherController : ControllerBase
return StatusCode(StatusCodes.Status502BadGateway, new ErrorResponse { Error = "CV matcher API request failed.", Code = "upstream_request_failed" });
}
}
private async Task CacheUploadedCvAsync(IFormFile file, string documentId, CancellationToken ct)
{
try
{
var storagePath = GetFileStoragePath();
Directory.CreateDirectory(storagePath);
var targetPath = BuildCvPath(documentId);
await using var fileStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true);
await file.CopyToAsync(fileStream, ct);
_logger.LogInformation("Cached uploaded CV for email attachment. DocumentId={DocumentId}", documentId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not cache uploaded CV for attachment. DocumentId={DocumentId}", documentId);
}
}
private string? TryGetCachedCvPath(string? cvDocumentId)
{
if (string.IsNullOrWhiteSpace(cvDocumentId))
{
return null;
}
var path = BuildCvPath(cvDocumentId);
return System.IO.File.Exists(path) ? path : null;
}
private string BuildCvPath(string documentId)
{
var safeId = string.Concat(documentId.Where(char.IsLetterOrDigit));
if (string.IsNullOrWhiteSpace(safeId))
{
safeId = "cv";
}
return Path.Combine(GetFileStoragePath(), $"{safeId}.pdf");
}
private string GetFileStoragePath()
{
var fileStoragePath = _fileStorageSettings.Path;
if (!Path.IsPathRooted(fileStoragePath))
{
var solutionRoot = Directory.GetCurrentDirectory();
if (solutionRoot.EndsWith("api", StringComparison.OrdinalIgnoreCase))
{
solutionRoot = Directory.GetParent(solutionRoot)?.FullName ?? solutionRoot;
}
fileStoragePath = Path.Combine(solutionRoot, fileStoragePath);
}
return fileStoragePath;
}
}
+1 -1
View File
@@ -7,6 +7,6 @@ namespace Api.Services.Contracts
Task SendContactAsync(ContactRequest req, CancellationToken ct);
Task SendSubscribeAsync(SubscribeRequest req, CancellationToken ct);
Task SendFileDownloadNotificationAsync(string fileName, string? userIp, CancellationToken ct);
Task SendMatchAsync(string? explicitTo, string subject, string body, CancellationToken ct);
Task SendMatchAsync(string? explicitTo, string subject, string body, string? attachmentPath, CancellationToken ct);
}
}
+71 -2
View File
@@ -5,6 +5,7 @@ using MailKit.Security;
using MimeKit;
using Models.Settings;
using Models.Requests;
using CvMatcher.Models.Responses;
namespace Api.Services
{
@@ -167,9 +168,77 @@ namespace Api.Services
await client.DisconnectAsync(true, ct);
}
public Task SendMatchAsync(string? explicitTo, string subject, string body, CancellationToken ct)
public Task SendMatchAsync(string? explicitTo, string subject, string body, string? attachmentPath, CancellationToken ct)
{
throw new NotImplementedException();
return SendMatchInternalAsync(explicitTo, subject, body, attachmentPath, ct);
}
private async Task SendMatchInternalAsync(string? explicitTo, string subject, string body, string? attachmentPath, CancellationToken ct)
{
var recipients = new List<string>();
if (!string.IsNullOrWhiteSpace(explicitTo))
{
recipients.Add(explicitTo);
}
if (!string.IsNullOrWhiteSpace(_contact.ToEmail) &&
!recipients.Any(x => string.Equals(x, _contact.ToEmail, StringComparison.OrdinalIgnoreCase)))
{
recipients.Add(_contact.ToEmail);
}
if (recipients.Count == 0)
{
_log.LogDebug("Match email skipped - no recipients configured (user email and Contact:ToEmail missing)");
return;
}
foreach (var recipient in recipients)
{
_log.LogInformation("Preparing CV match email to {RecipientEmail}", recipient);
var msg = new MimeMessage();
msg.From.Add(MailboxAddress.Parse(_smtp.Username));
msg.To.Add(MailboxAddress.Parse(recipient));
msg.Subject = $"[{_environmentName}] {subject}".Trim();
var builder = new BodyBuilder
{
TextBody = body
};
if (!string.IsNullOrWhiteSpace(attachmentPath) && File.Exists(attachmentPath))
{
builder.Attachments.Add(attachmentPath);
}
msg.Body = builder.ToMessageBody();
await SendEmailAsync(msg, "cv match email", ct);
_log.LogInformation("CV match email sent successfully to {RecipientEmail}", recipient);
}
}
public static string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel) => $@"CV Matcher result
CV Document ID: {cvDocumentId}
Job: {jobLabel ?? "N/A"}
Job URL: {result.JobUrl ?? "N/A"}
Score: {result.Score}%
Summary:
{result.Summary}
Strengths:
- {string.Join("\n- ", result.Strengths)}
Gaps:
- {string.Join("\n- ", result.Gaps)}
Recommendations:
- {string.Join("\n- ", result.Recommendations)}";
public static string BuildMatchEmailSubject(int score, string? jobLabel)
=> $"MyAi.ro CV Match: {score}% - {jobLabel ?? "Job"}";
}
}
+4
View File
@@ -105,6 +105,10 @@
<span data-i18n="cv.jobDescription">Or paste job description</span>
<textarea id="jobDescription" rows="8" data-i18n-placeholder="cv.jobPlaceholder" placeholder="Paste the job description if the page cannot be crawled."></textarea>
</label>
<label>
<span data-i18n="form.email">Email</span>
<input type="email" id="matchEmail" data-i18n-placeholder="form.emailPlaceholder" placeholder="name@company.com" />
</label>
<div class="consent-inline">
<input type="checkbox" id="gdprConsent" required />
<label for="gdprConsent" data-i18n="cv.gdpr">I agree that my CV is processed and stored.</label>
+2
View File
@@ -437,6 +437,7 @@
var file = $('#cvFile')[0] && $('#cvFile')[0].files[0];
var jobUrl = $('#jobUrl').val();
var jobDescription = $('#jobDescription').val();
var matchEmail = $('#matchEmail').val();
var consent = $('#gdprConsent').is(':checked');
var $msg = $('#matcherMsg'),
$button = $('#matchSubmit'),
@@ -505,6 +506,7 @@
cvDocumentId: cvData.documentId || cvData.cvDocumentId,
jobUrl: jobUrl,
jobDescription: jobDescription,
email: matchEmail,
gdprConsent: consent,
captchaToken: matchToken
})