feat: complete phase 3 career workspace
This commit is contained in:
@@ -24,8 +24,9 @@ public sealed class AuthController : ControllerBase
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
private readonly ITwoFactorPendingTokenService _twoFactorPending;
|
||||
private readonly JobTrackerContext _db;
|
||||
private readonly string _avatarDataRoot;
|
||||
|
||||
public AuthController(IConfiguration cfg, UserManager<ApplicationUser> users, ITokenService tokens, IAppEmailSender email, IGoogleTokenValidator googleTokens, IMicrosoftTokenValidator microsoftTokens, ILogger<AuthController> logger, ITwoFactorPendingTokenService twoFactorPending, JobTrackerContext db)
|
||||
public AuthController(IConfiguration cfg, UserManager<ApplicationUser> users, ITokenService tokens, IAppEmailSender email, IGoogleTokenValidator googleTokens, IMicrosoftTokenValidator microsoftTokens, ILogger<AuthController> logger, ITwoFactorPendingTokenService twoFactorPending, JobTrackerContext db, IHostEnvironment? environment = null)
|
||||
{
|
||||
_cfg = cfg;
|
||||
_users = users;
|
||||
@@ -36,6 +37,7 @@ public sealed class AuthController : ControllerBase
|
||||
_logger = logger;
|
||||
_twoFactorPending = twoFactorPending;
|
||||
_db = db;
|
||||
_avatarDataRoot = Path.GetFullPath((_cfg["Data:Root"] ?? environment?.ContentRootPath ?? AppContext.BaseDirectory).Trim());
|
||||
}
|
||||
|
||||
[HttpGet("config")]
|
||||
@@ -575,8 +577,7 @@ public sealed class AuthController : ControllerBase
|
||||
return BadRequest("Only PNG, JPEG, or WebP images are supported.");
|
||||
}
|
||||
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
user.AvatarImageDataUrl = $"data:{detectedContentType};base64,{base64}";
|
||||
user.AvatarImageDataUrl = await AvatarStorage.StoreAsync(_avatarDataRoot, user.Id, bytes, detectedContentType, HttpContext.RequestAborted);
|
||||
|
||||
var result = await _users.UpdateAsync(user);
|
||||
if (!result.Succeeded)
|
||||
@@ -584,7 +585,7 @@ public sealed class AuthController : ControllerBase
|
||||
return BadRequest(string.Join("; ", result.Errors.Select(e => e.Description)));
|
||||
}
|
||||
|
||||
return Ok(new { avatarImageDataUrl = user.AvatarImageDataUrl });
|
||||
return Ok(new { avatarImageDataUrl = AvatarStorage.Resolve(user.AvatarImageDataUrl) });
|
||||
}
|
||||
|
||||
[HttpDelete("avatar")]
|
||||
@@ -597,6 +598,7 @@ public sealed class AuthController : ControllerBase
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var storedAvatar = user.AvatarImageDataUrl;
|
||||
user.AvatarImageDataUrl = null;
|
||||
var result = await _users.UpdateAsync(user);
|
||||
if (!result.Succeeded)
|
||||
@@ -604,6 +606,7 @@ public sealed class AuthController : ControllerBase
|
||||
return BadRequest(string.Join("; ", result.Errors.Select(e => e.Description)));
|
||||
}
|
||||
|
||||
AvatarStorage.Delete(storedAvatar);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -876,7 +879,7 @@ public sealed class AuthController : ControllerBase
|
||||
DisplayName: user.DisplayName,
|
||||
ProfileCvText: user.ProfileCvText,
|
||||
ProfileCvStructureJson: user.ProfileCvStructureJson,
|
||||
AvatarImageDataUrl: user.AvatarImageDataUrl,
|
||||
AvatarImageDataUrl: AvatarStorage.Resolve(user.AvatarImageDataUrl),
|
||||
Roles: roles,
|
||||
GoogleLink: new GoogleLinkDto(
|
||||
Linked: !string.IsNullOrWhiteSpace(user.GoogleSubject),
|
||||
|
||||
@@ -229,7 +229,7 @@ public sealed class CvVariantController : ControllerBase
|
||||
if (string.IsNullOrWhiteSpace(name)) name = user.UserName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name)) name = user.Email?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name)) name = "Your Name";
|
||||
return new CvRenderPerson(name!, user.AvatarImageDataUrl);
|
||||
return new CvRenderPerson(name!, AvatarStorage.Resolve(user.AvatarImageDataUrl));
|
||||
}
|
||||
|
||||
private static VariantDto ToDto(CvVariant v) => new(
|
||||
|
||||
@@ -1821,7 +1821,7 @@ Candidate master CV:
|
||||
? request!.PhotoDataUrl
|
||||
: request?.UseProfileAvatar == false
|
||||
? null
|
||||
: user.AvatarImageDataUrl;
|
||||
: AvatarStorage.Resolve(user.AvatarImageDataUrl);
|
||||
var rendered = RenderTailoredCv(job, document, user, photoDataUrl);
|
||||
return Ok(new TailoredCvPreviewDto(rendered.TemplateId, rendered.Html, rendered.SuggestedFileName));
|
||||
}
|
||||
@@ -1852,7 +1852,7 @@ Candidate master CV:
|
||||
? request!.PhotoDataUrl
|
||||
: request?.UseProfileAvatar == false
|
||||
? null
|
||||
: user.AvatarImageDataUrl;
|
||||
: AvatarStorage.Resolve(user.AvatarImageDataUrl);
|
||||
var rendered = RenderTailoredCv(job, document, user, photoDataUrl);
|
||||
var artifact = await _cvPdfExporter.ExportAsync(rendered, cancellationToken);
|
||||
return File(artifact.Bytes, "application/pdf", artifact.FileName);
|
||||
|
||||
@@ -53,9 +53,18 @@ public sealed class ProfileCvController : ControllerBase
|
||||
["languages"] = "Languages",
|
||||
["interests"] = "Interests",
|
||||
["hobbies"] = "Interests",
|
||||
["awards"] = "Awards",
|
||||
["honours"] = "Awards",
|
||||
["honors"] = "Awards",
|
||||
["publications"] = "Publications",
|
||||
["organisations"] = "Organisations",
|
||||
["organizations"] = "Organisations",
|
||||
["memberships"] = "Organisations",
|
||||
["references"] = "References",
|
||||
};
|
||||
|
||||
private const long MaxFileSizeBytes = 5 * 1024 * 1024;
|
||||
private const int ExtractionRunRetentionCount = 20;
|
||||
private const string ParserVersion = "m005-s01";
|
||||
private const string NormalizerVersion = "m005-s01";
|
||||
private const string LlmPromptVersion = "m005-s01";
|
||||
@@ -168,6 +177,7 @@ public sealed class ProfileCvController : ControllerBase
|
||||
run.Status = "pending_review";
|
||||
run.CompletedAtUtc = DateTimeOffset.UtcNow;
|
||||
await _db.SaveChangesAsync(HttpContext.RequestAborted);
|
||||
await PruneExtractionRunsAsync(user.Id, HttpContext.RequestAborted);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
@@ -185,6 +195,7 @@ public sealed class ProfileCvController : ControllerBase
|
||||
run.ErrorMessage = ex.Message;
|
||||
run.CompletedAtUtc = DateTimeOffset.UtcNow;
|
||||
await _db.SaveChangesAsync(HttpContext.RequestAborted);
|
||||
await PruneExtractionRunsAsync(user.Id, HttpContext.RequestAborted);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -625,7 +636,7 @@ public sealed class ProfileCvController : ControllerBase
|
||||
if (string.IsNullOrWhiteSpace(candidateName)) candidateName = user.UserName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(candidateName)) candidateName = user.Email?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(candidateName)) candidateName = "Your Name";
|
||||
return _cvTemplateRenderer.Render(document, document.TemplateId, candidateName!, targetRole, companyName, user.AvatarImageDataUrl);
|
||||
return _cvTemplateRenderer.Render(document, document.TemplateId, candidateName!, targetRole, companyName, AvatarStorage.Resolve(user.AvatarImageDataUrl));
|
||||
}
|
||||
|
||||
private static TailoredCvDocument BuildMasterCvDocument(StructuredCvProfile structuredCv, string templateId, string? targetRole, string? fallbackHeadline, string? companyName)
|
||||
@@ -903,6 +914,7 @@ public sealed class ProfileCvController : ControllerBase
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
await PruneExtractionRunsAsync(user.Id, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<CvExtractionRun> CreateQueuedRunAsync(string ownerUserId, int? artifactId, string trigger, CancellationToken cancellationToken)
|
||||
@@ -1011,6 +1023,7 @@ public sealed class ProfileCvController : ControllerBase
|
||||
run.ErrorMessage = ex.Message;
|
||||
run.CompletedAtUtc = DateTimeOffset.UtcNow;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
await PruneExtractionRunsAsync(user.Id, cancellationToken);
|
||||
await SendRunCompletionEmailAsync(user, run, false, cancellationToken);
|
||||
_logger.LogWarning(ex, "CV processing run {RunId} failed for user {UserId}", run.Id, user.Id);
|
||||
}
|
||||
@@ -1024,6 +1037,19 @@ public sealed class ProfileCvController : ControllerBase
|
||||
run.Status = "pending_review";
|
||||
run.CompletedAtUtc = DateTimeOffset.UtcNow;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
await PruneExtractionRunsAsync(run.OwnerUserId, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task PruneExtractionRunsAsync(string ownerUserId, CancellationToken cancellationToken)
|
||||
{
|
||||
var expired = await _db.CvExtractionRuns
|
||||
.Where(x => x.OwnerUserId == ownerUserId && x.Status != "queued" && x.Status != "running")
|
||||
.OrderByDescending(x => x.StartedAtUtc)
|
||||
.Skip(ExtractionRunRetentionCount)
|
||||
.ToListAsync(cancellationToken);
|
||||
if (expired.Count == 0) return;
|
||||
_db.CvExtractionRuns.RemoveRange(expired);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task SendRunCompletionEmailAsync(ApplicationUser user, CvExtractionRun run, bool success, CancellationToken cancellationToken)
|
||||
|
||||
@@ -44,6 +44,6 @@ public sealed class PublicCvController : ControllerBase
|
||||
if (string.IsNullOrWhiteSpace(name)) name = user.DisplayName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name)) name = user.UserName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name)) name = "Candidate";
|
||||
return new CvRenderPerson(name!, user.AvatarImageDataUrl);
|
||||
return new CvRenderPerson(name!, AvatarStorage.Resolve(user.AvatarImageDataUrl));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user