feat: Add GDPR compliance and improve UI/UX

GDPR Compliance:
- Delete CV files immediately after processing
- Add DataRetentionJob to auto-purge data after 30 days
- Add DeleteAllUserDataAsync for right to erasure
- Add Privacy Policy page with GDPR information
- Add privacy link and GDPR badge to footer

UI/UX Improvements:
- Add "Why Choose RealCV" benefits section to homepage
- Fix pricing page: Professional card highlight, consistent button styles
- Improve text contrast on dark backgrounds (0.75 → 0.85 opacity)
- Fix auth page messaging consistency
- Fix CSS --realcv-accent variable (was self-referencing)

UK Terminology:
- Change "hiring" to "recruitment" throughout
- Change "bad hires" to "poor appointments"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-22 02:00:39 +00:00
parent 2b29e19306
commit 358b0328e7
13 changed files with 483 additions and 23 deletions

View File

@@ -11,4 +11,9 @@ public interface ICVCheckService
Task<List<CVCheckDto>> GetUserChecksAsync(Guid userId);
Task<VeracityReport?> GetReportAsync(Guid checkId, Guid userId);
Task<bool> DeleteCheckAsync(Guid checkId, Guid userId);
/// <summary>
/// GDPR: Delete all CV checks and associated data for a user (right to erasure).
/// </summary>
Task<int> DeleteAllUserDataAsync(Guid userId);
}

View File

@@ -0,0 +1,106 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using RealCV.Application.Interfaces;
using RealCV.Domain.Enums;
using RealCV.Infrastructure.Data;
namespace RealCV.Infrastructure.Jobs;
/// <summary>
/// GDPR compliance job that automatically deletes old CV check data
/// based on configured retention period.
/// </summary>
public sealed class DataRetentionJob
{
private readonly ApplicationDbContext _dbContext;
private readonly IFileStorageService _fileStorageService;
private readonly ILogger<DataRetentionJob> _logger;
private readonly int _retentionDays;
public DataRetentionJob(
ApplicationDbContext dbContext,
IFileStorageService fileStorageService,
IConfiguration configuration,
ILogger<DataRetentionJob> logger)
{
_dbContext = dbContext;
_fileStorageService = fileStorageService;
_logger = logger;
_retentionDays = configuration.GetValue<int>("DataRetention:CVCheckRetentionDays", 30);
}
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
_logger.LogInformation("Starting GDPR data retention job (retention: {Days} days)", _retentionDays);
try
{
var cutoffDate = DateTime.UtcNow.AddDays(-_retentionDays);
// Find old completed CV checks that should be deleted
var oldChecks = await _dbContext.CVChecks
.Include(c => c.Flags)
.Where(c => c.CompletedAt != null && c.CompletedAt < cutoffDate)
.Where(c => c.Status == CheckStatus.Completed || c.Status == CheckStatus.Failed)
.ToListAsync(cancellationToken);
if (oldChecks.Count == 0)
{
_logger.LogInformation("No CV checks older than {Days} days found for deletion", _retentionDays);
return;
}
_logger.LogInformation("Found {Count} CV checks older than {Days} days for deletion", oldChecks.Count, _retentionDays);
var deletedCount = 0;
var fileDeletedCount = 0;
foreach (var check in oldChecks)
{
try
{
// Delete any remaining files (should already be deleted after processing, but be thorough)
if (!string.IsNullOrWhiteSpace(check.BlobUrl))
{
try
{
await _fileStorageService.DeleteAsync(check.BlobUrl);
fileDeletedCount++;
_logger.LogDebug("Deleted orphaned file for CV check {CheckId}", check.Id);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete file for CV check {CheckId}", check.Id);
}
}
// Delete associated flags
_dbContext.CVFlags.RemoveRange(check.Flags);
// Delete the CV check record
_dbContext.CVChecks.Remove(check);
deletedCount++;
_logger.LogDebug("Marked CV check {CheckId} for deletion (created: {Created})",
check.Id, check.CreatedAt);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing CV check {CheckId} for deletion", check.Id);
}
}
await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"GDPR data retention job completed. Deleted {DeletedCount} CV checks and {FileCount} orphaned files",
deletedCount, fileDeletedCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in GDPR data retention job");
throw;
}
}
}

View File

@@ -265,6 +265,12 @@ public sealed class ProcessCVCheckJob
cvCheckId, score);
await _auditService.LogAsync(cvCheck.UserId, AuditActions.CVProcessed, "CVCheck", cvCheckId, $"Score: {score}");
// GDPR: Delete the uploaded CV file immediately after processing
// We only need the extracted data and report, not the original file
await DeleteCVFileAsync(cvCheck.BlobUrl, cvCheckId);
cvCheck.BlobUrl = string.Empty; // Clear the URL as file no longer exists
await _dbContext.SaveChangesAsync(cancellationToken);
}
catch (Exception ex)
{
@@ -287,6 +293,29 @@ public sealed class ProcessCVCheckJob
}
}
/// <summary>
/// GDPR: Safely delete the uploaded CV file after processing.
/// </summary>
private async Task DeleteCVFileAsync(string blobUrl, Guid cvCheckId)
{
if (string.IsNullOrWhiteSpace(blobUrl))
{
_logger.LogDebug("No file to delete for CV check {CheckId}", cvCheckId);
return;
}
try
{
await _fileStorageService.DeleteAsync(blobUrl);
_logger.LogInformation("GDPR: Deleted CV file for check {CheckId}", cvCheckId);
}
catch (Exception ex)
{
// Log but don't fail the job - file deletion is important but shouldn't break processing
_logger.LogWarning(ex, "Failed to delete CV file for check {CheckId}: {BlobUrl}", cvCheckId, blobUrl);
}
}
private static (int Score, List<FlagResult> Flags) CalculateVeracityScore(
List<CompanyVerificationResult> verifications,
List<EducationVerificationResult> educationResults,

View File

@@ -185,17 +185,78 @@ public sealed class CVCheckService : ICVCheckService
var fileName = cvCheck.OriginalFileName;
// GDPR: Delete the uploaded file if it still exists
if (!string.IsNullOrWhiteSpace(cvCheck.BlobUrl))
{
try
{
await _fileStorageService.DeleteAsync(cvCheck.BlobUrl);
_logger.LogDebug("Deleted file for CV check {CheckId}", checkId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete file for CV check {CheckId}", checkId);
// Continue with deletion even if file deletion fails
}
}
_dbContext.CVFlags.RemoveRange(cvCheck.Flags);
_dbContext.CVChecks.Remove(cvCheck);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Deleted CV check {CheckId} for user {UserId}", checkId, userId);
_logger.LogInformation("GDPR: Deleted CV check {CheckId} and associated data for user {UserId}", checkId, userId);
await _auditService.LogAsync(userId, AuditActions.CVDeleted, "CVCheck", checkId, $"File: {fileName}");
return true;
}
public async Task<int> DeleteAllUserDataAsync(Guid userId)
{
_logger.LogInformation("GDPR: Deleting all CV data for user {UserId}", userId);
var userChecks = await _dbContext.CVChecks
.Include(c => c.Flags)
.Where(c => c.UserId == userId)
.ToListAsync();
if (userChecks.Count == 0)
{
_logger.LogDebug("No CV checks found for user {UserId}", userId);
return 0;
}
var deletedCount = 0;
foreach (var check in userChecks)
{
// Delete the file if it exists
if (!string.IsNullOrWhiteSpace(check.BlobUrl))
{
try
{
await _fileStorageService.DeleteAsync(check.BlobUrl);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to delete file for CV check {CheckId}", check.Id);
}
}
_dbContext.CVFlags.RemoveRange(check.Flags);
_dbContext.CVChecks.Remove(check);
deletedCount++;
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation("GDPR: Deleted {Count} CV checks for user {UserId}", deletedCount, userId);
await _auditService.LogAsync(userId, AuditActions.CVDeleted, null, null, $"Deleted all data: {deletedCount} checks");
return deletedCount;
}
private static CVCheckDto MapToDto(CVCheck cvCheck)
{
return new CVCheckDto

View File

@@ -78,9 +78,17 @@
</main>
<footer class="text-light py-4 mt-auto" style="background-color: var(--realcv-footer-bg);">
<div class="container text-center">
<div class="container">
<div class="row align-items-center">
<div class="col-md-6 text-center text-md-start mb-2 mb-md-0">
<p class="mb-0">&copy; @DateTime.Now.Year RealCV. All rights reserved.</p>
</div>
<div class="col-md-6 text-center text-md-end">
<a href="/privacy" class="text-light text-decoration-none me-3">Privacy Policy</a>
<span class="text-muted small">GDPR Compliant</span>
</div>
</div>
</div>
</footer>
</div>

View File

@@ -123,7 +123,7 @@
<div class="auth-testimonial">
<blockquote>
"RealCV has transformed our hiring process. We catch discrepancies we would have missed before."
"RealCV has transformed our recruitment process. We catch discrepancies we would have missed before."
</blockquote>
<cite>- HR Director, Tech Company</cite>
</div>

View File

@@ -97,9 +97,9 @@
<p class="text-center text-muted small mb-4">
By creating an account, you agree to our
<a href="#" class="text-decoration-none">Terms of Service</a>
<a href="/privacy" class="text-decoration-none">Terms of Service</a>
and
<a href="#" class="text-decoration-none">Privacy Policy</a>
<a href="/privacy" class="text-decoration-none">Privacy Policy</a>
</p>
<div class="auth-divider">
@@ -123,9 +123,9 @@
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z"/>
</svg>
</div>
<h2 class="auth-brand-title">Start Your Free Trial</h2>
<h2 class="auth-brand-title">Create Your Free Account</h2>
<p class="auth-brand-text">
Get 3 free CV verifications to experience the power of AI-driven credential analysis.
Get 3 free CV verifications per month. No credit card required.
</p>
<div class="auth-features">
@@ -157,7 +157,7 @@
<div class="auth-testimonial">
<blockquote>
"We reduced bad hires by 40% in the first quarter using RealCV."
"We reduced unsuitable appointments by 40% in the first quarter using RealCV."
</blockquote>
<cite>- Recruitment Manager, Financial Services</cite>
</div>

View File

@@ -182,6 +182,126 @@
</div>
</section>
<!-- Why RealCV Section -->
<section class="py-5" style="background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%);">
<div class="container">
<div class="text-center mb-5">
<h2 class="fw-bold mb-3 text-white" style="font-size: 2.25rem;">Why Choose RealCV?</h2>
<p style="font-size: 1.125rem; color: rgba(255,255,255,0.9);">Make better recruitment decisions with verified candidate information</p>
</div>
<div class="row g-4">
<div class="col-lg-4 col-md-6">
<div class="h-100 p-4 rounded-3" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle p-2 me-3" style="background: rgba(34, 197, 94, 0.2);">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#22C55E" viewBox="0 0 16 16">
<path d="M2.5 8a5.5 5.5 0 0 1 8.25-4.764.5.5 0 0 0 .5-.866A6.5 6.5 0 1 0 14.5 8a.5.5 0 0 0-1 0 5.5 5.5 0 1 1-11 0z"/>
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
</svg>
</div>
<h5 class="fw-bold text-white mb-0">Reduce Poor Appointments</h5>
</div>
<p class="mb-0" style="color: rgba(255,255,255,0.85);">
Studies show 30-40% of CVs contain inaccuracies. Catch embellishments and false claims before they become costly recruitment mistakes.
</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="h-100 p-4 rounded-3" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle p-2 me-3" style="background: rgba(59, 130, 246, 0.2);">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#3B82F6" viewBox="0 0 16 16">
<path d="M4 .5a.5.5 0 0 0-1 0V1H2a2 2 0 0 0-2 2v1h16V3a2 2 0 0 0-2-2h-1V.5a.5.5 0 0 0-1 0V1H4V.5zM16 14V5H0v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2zM9.5 7h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm3 0h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zM2 10.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1zm3.5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/>
</svg>
</div>
<h5 class="fw-bold text-white mb-0">Save Time</h5>
</div>
<p class="mb-0" style="color: rgba(255,255,255,0.85);">
Get comprehensive verification reports in minutes, not days. No more manual reference checking or waiting for background check results.
</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="h-100 p-4 rounded-3" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle p-2 me-3" style="background: rgba(168, 85, 247, 0.2);">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#A855F7" viewBox="0 0 16 16">
<path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/>
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z"/>
</svg>
</div>
<h5 class="fw-bold text-white mb-0">Official Data Sources</h5>
</div>
<p class="mb-0" style="color: rgba(255,255,255,0.85);">
Verify against Companies House records, cross-reference incorporation dates, check company status, and validate director claims.
</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="h-100 p-4 rounded-3" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle p-2 me-3" style="background: rgba(249, 115, 22, 0.2);">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#F97316" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
</div>
<h5 class="fw-bold text-white mb-0">Detailed Reports</h5>
</div>
<p class="mb-0" style="color: rgba(255,255,255,0.85);">
Get actionable insights with employment verification scores, timeline analysis, education checks, and specific flags for areas of concern.
</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="h-100 p-4 rounded-3" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle p-2 me-3" style="background: rgba(236, 72, 153, 0.2);">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#EC4899" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
</svg>
</div>
<h5 class="fw-bold text-white mb-0">GDPR Compliant</h5>
</div>
<p class="mb-0" style="color: rgba(255,255,255,0.85);">
CVs are deleted immediately after processing. Data is automatically purged after 30 days. Your candidates' privacy is protected.
</p>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="h-100 p-4 rounded-3" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle p-2 me-3" style="background: rgba(20, 184, 166, 0.2);">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#14B8A6" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2zm13 2.383-4.708 2.825L15 11.105V5.383zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741zM1 11.105l4.708-2.897L1 5.383v5.722z"/>
</svg>
</div>
<h5 class="fw-bold text-white mb-0">UK Specialist</h5>
</div>
<p class="mb-0" style="color: rgba(255,255,255,0.85);">
Purpose-built for UK recruitment with support for NHS, councils, public sector employers, charities, and Companies House registered businesses.
</p>
</div>
</div>
</div>
<div class="text-center mt-5">
<a href="/pricing" class="btn btn-lg px-5 py-3" style="background: white; color: #1e3a5f; font-weight: 600;">
View Pricing Plans
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="ms-2" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/>
</svg>
</a>
</div>
</div>
</section>
<!-- Trust indicators -->
<section class="py-4" style="background-color: var(--realcv-bg-muted); border-top: 1px solid var(--realcv-gray-200);">
<div class="container">

View File

@@ -11,7 +11,7 @@
<div class="text-center mb-5">
<h1 class="fw-bold mb-3">Simple, Transparent Pricing</h1>
<p class="text-muted lead mb-0" style="max-width: 600px; margin: 0 auto;">
Choose the plan that fits your hiring needs. All plans include our core CV verification technology.
Choose the plan that fits your recruitment needs. All plans include our core CV verification technology.
</p>
</div>
@@ -69,9 +69,7 @@
@if (_currentPlan == "Free")
{
<button class="btn btn-outline-secondary w-100 py-2" disabled>
Current Plan
</button>
<span class="text-muted small d-block text-center">Your current plan</span>
}
else
{
@@ -85,7 +83,7 @@
<!-- Professional Plan -->
<div class="col-lg-4 col-md-6">
<div class="card border-0 shadow h-100 position-relative @(_currentPlan == "Professional" ? "border-primary border-2" : "")">
<div class="card shadow-lg h-100 position-relative @(_currentPlan == "Professional" ? "border-primary border-2" : "border-primary")" style="@(_currentPlan != "Professional" ? "border-width: 2px !important;" : "")">
@if (_currentPlan == "Professional")
{
<div class="card-header bg-primary text-white text-center py-2 border-0">
@@ -209,15 +207,13 @@
@if (_currentPlan == "Enterprise")
{
<button class="btn btn-outline-secondary w-100 py-2" disabled>
Current Plan
</button>
<span class="text-muted small d-block text-center">Your current plan</span>
}
else
{
<form action="/api/billing/create-checkout" method="post">
<input type="hidden" name="plan" value="Enterprise" />
<button type="submit" class="btn btn-dark w-100 py-2 fw-semibold" disabled="@(!_isAuthenticated)">
<button type="submit" class="btn btn-outline-primary w-100 py-2 fw-semibold" disabled="@(!_isAuthenticated)">
@if (_isAuthenticated)
{
<span>Upgrade to Enterprise</span>

View File

@@ -0,0 +1,124 @@
@page "/privacy"
<PageTitle>Privacy Policy - RealCV</PageTitle>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="fw-bold mb-4">Privacy Policy</h1>
<p class="text-muted mb-5">Last updated: @DateTime.Now.ToString("MMMM yyyy")</p>
<div class="card mb-4">
<div class="card-body p-4">
<h2 class="h4 fw-bold mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="text-primary me-2" viewBox="0 0 16 16">
<path d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/>
</svg>
GDPR Compliance
</h2>
<p>
RealCV is committed to protecting your privacy and complying with the General Data Protection Regulation (GDPR).
This policy explains how we collect, use, and protect personal data.
</p>
</div>
</div>
<h3 class="h5 fw-bold mt-4 mb-3">Data We Collect</h3>
<p>When you use RealCV, we collect:</p>
<ul class="mb-4">
<li><strong>Account Information:</strong> Email address and password (hashed) when you register</li>
<li><strong>CV Data:</strong> When you upload a CV for verification, we temporarily process the document to extract employment and education information</li>
<li><strong>Verification Results:</strong> The analysis results and veracity scores generated from CV checks</li>
</ul>
<h3 class="h5 fw-bold mt-4 mb-3">How We Use Your Data</h3>
<ul class="mb-4">
<li>To provide CV verification services</li>
<li>To generate veracity reports</li>
<li>To maintain your account and subscription</li>
<li>To improve our services</li>
</ul>
<div class="alert alert-info mb-4">
<h4 class="h6 fw-bold mb-2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
</svg>
Important: CV File Handling
</h4>
<p class="mb-0">
<strong>Uploaded CV files are automatically deleted immediately after processing.</strong>
We do not retain the original CV documents. Only the extracted verification data and reports are stored temporarily.
</p>
</div>
<h3 class="h5 fw-bold mt-4 mb-3">Data Retention</h3>
<p>
We retain CV check data for a maximum of <strong>30 days</strong> after completion.
After this period, all associated data is automatically and permanently deleted.
</p>
<p>You can also manually delete your CV check data at any time from your dashboard.</p>
<h3 class="h5 fw-bold mt-4 mb-3">Your Rights Under GDPR</h3>
<p>You have the following rights regarding your personal data:</p>
<div class="row g-3 mb-4">
<div class="col-md-6">
<div class="border rounded p-3 h-100">
<strong>Right to Access</strong>
<p class="mb-0 small text-muted">View all data we hold about you</p>
</div>
</div>
<div class="col-md-6">
<div class="border rounded p-3 h-100">
<strong>Right to Erasure</strong>
<p class="mb-0 small text-muted">Request deletion of your data</p>
</div>
</div>
<div class="col-md-6">
<div class="border rounded p-3 h-100">
<strong>Right to Rectification</strong>
<p class="mb-0 small text-muted">Correct inaccurate personal data</p>
</div>
</div>
<div class="col-md-6">
<div class="border rounded p-3 h-100">
<strong>Right to Portability</strong>
<p class="mb-0 small text-muted">Export your data in a standard format</p>
</div>
</div>
</div>
<h3 class="h5 fw-bold mt-4 mb-3">Data Security</h3>
<ul class="mb-4">
<li>All data is encrypted in transit using TLS/HTTPS</li>
<li>Passwords are hashed using industry-standard algorithms</li>
<li>CV files are deleted immediately after processing</li>
<li>Access to data is restricted to authorised personnel only</li>
</ul>
<h3 class="h5 fw-bold mt-4 mb-3">Third-Party Services</h3>
<p>We use the following third-party services:</p>
<ul class="mb-4">
<li><strong>Companies House API:</strong> To verify UK company information (public data)</li>
<li><strong>Anthropic Claude:</strong> For AI-powered CV parsing (data is processed in accordance with Anthropic's privacy policy)</li>
<li><strong>Stripe:</strong> For payment processing (we do not store payment card details)</li>
</ul>
<h3 class="h5 fw-bold mt-4 mb-3">Contact Us</h3>
<p>
If you have any questions about this privacy policy or wish to exercise your data rights,
please contact us through your account dashboard or by email.
</p>
<div class="mt-5 pt-4 border-top">
<a href="/" class="btn btn-outline-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/>
</svg>
Back to Home
</a>
</div>
</div>
</div>
</div>

View File

@@ -169,12 +169,20 @@ try
});
}
// Schedule recurring job to reset monthly usage
// Schedule recurring jobs
var recurringJobManager = app.Services.GetRequiredService<IRecurringJobManager>();
// Reset monthly usage at 00:05 UTC daily
recurringJobManager.AddOrUpdate<RealCV.Infrastructure.Jobs.ResetMonthlyUsageJob>(
"reset-monthly-usage",
job => job.ExecuteAsync(CancellationToken.None),
Cron.Daily(0, 5)); // Run at 00:05 UTC daily
Cron.Daily(0, 5));
// GDPR: Run data retention cleanup at 02:00 UTC daily
recurringJobManager.AddOrUpdate<RealCV.Infrastructure.Jobs.DataRetentionJob>(
"gdpr-data-retention",
job => job.ExecuteAsync(CancellationToken.None),
Cron.Daily(2, 0));
// Login endpoint
app.MapPost("/account/perform-login", async (

View File

@@ -3,6 +3,9 @@
"DefaultConnection": "Server=.;Database=RealCV;Trusted_Connection=True;TrustServerCertificate=True;",
"HangfireConnection": "Server=.;Database=RealCV_Hangfire;Trusted_Connection=True;TrustServerCertificate=True;"
},
"DataRetention": {
"CVCheckRetentionDays": 30
},
"CompaniesHouse": {
"BaseUrl": "https://api.company-information.service.gov.uk",
"ApiKey": ""

View File

@@ -30,7 +30,7 @@
--realcv-info: #0EA5E9; /* Sky Blue */
--realcv-info-light: #E0F2FE;
--realcv-accent: var(--realcv-accent); /* Light Blue - accent for dark backgrounds */
--realcv-accent: #60A5FA; /* Light Blue - accent for dark backgrounds */
--realcv-neutral: #64748B; /* Slate */
--realcv-neutral-light: #F1F5F9;
@@ -1116,7 +1116,7 @@ h1:focus {
.auth-stat-label {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.8);
color: rgba(255, 255, 255, 0.9);
margin-top: 0.25rem;
}
@@ -1137,7 +1137,7 @@ h1:focus {
.auth-testimonial cite {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.8);
color: rgba(255, 255, 255, 0.85);
font-style: normal;
}