diff --git a/screenshots/01-home.png b/screenshots/01-home.png new file mode 100644 index 0000000..4d99979 Binary files /dev/null and b/screenshots/01-home.png differ diff --git a/screenshots/02-login.png b/screenshots/02-login.png new file mode 100644 index 0000000..4255efa Binary files /dev/null and b/screenshots/02-login.png differ diff --git a/screenshots/03-register.png b/screenshots/03-register.png new file mode 100644 index 0000000..f3d70c1 Binary files /dev/null and b/screenshots/03-register.png differ diff --git a/screenshots/04-dashboard.png b/screenshots/04-dashboard.png new file mode 100644 index 0000000..c96ad62 Binary files /dev/null and b/screenshots/04-dashboard.png differ diff --git a/screenshots/05-check.png b/screenshots/05-check.png new file mode 100644 index 0000000..bbcd703 Binary files /dev/null and b/screenshots/05-check.png differ diff --git a/screenshots/06-report.png b/screenshots/06-report.png new file mode 100644 index 0000000..e22d187 Binary files /dev/null and b/screenshots/06-report.png differ diff --git a/screenshots/dashboard-compact.png b/screenshots/dashboard-compact.png new file mode 100644 index 0000000..79c7308 Binary files /dev/null and b/screenshots/dashboard-compact.png differ diff --git a/screenshots/dashboard-warm.png b/screenshots/dashboard-warm.png new file mode 100644 index 0000000..9a119ef Binary files /dev/null and b/screenshots/dashboard-warm.png differ diff --git a/screenshots/report-compact.png b/screenshots/report-compact.png new file mode 100644 index 0000000..dc8a427 Binary files /dev/null and b/screenshots/report-compact.png differ diff --git a/src/RealCV.Infrastructure/Data/Migrations/20260125074319_AddTermsAcceptedAt.Designer.cs b/src/RealCV.Infrastructure/Data/Migrations/20260125074319_AddTermsAcceptedAt.Designer.cs new file mode 100644 index 0000000..d564176 --- /dev/null +++ b/src/RealCV.Infrastructure/Data/Migrations/20260125074319_AddTermsAcceptedAt.Designer.cs @@ -0,0 +1,505 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RealCV.Infrastructure.Data; + +#nullable disable + +namespace RealCV.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260125074319_AddTermsAcceptedAt")] + partial class AddTermsAcceptedAt + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("RealCV.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Details") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Action") + .HasDatabaseName("IX_AuditLogs_Action"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_AuditLogs_CreatedAt"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_AuditLogs_UserId"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BlobUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExtractedDataJson") + .HasColumnType("nvarchar(max)"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("ProcessingStage") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ReportJson") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("VeracityScore") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Status") + .HasDatabaseName("IX_CVChecks_Status"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_CVChecks_UserId"); + + b.ToTable("CVChecks"); + }); + + modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CVCheckId") + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("ScoreImpact") + .HasColumnType("int"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("CVCheckId") + .HasDatabaseName("IX_CVFlags_CVCheckId"); + + b.ToTable("CVFlags"); + }); + + modelBuilder.Entity("RealCV.Domain.Entities.CompanyCache", b => + { + b.Property("CompanyNumber") + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("AccountsCategory") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CachedAt") + .HasColumnType("datetime2"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("CompanyType") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("DissolutionDate") + .HasColumnType("date"); + + b.Property("IncorporationDate") + .HasColumnType("date"); + + b.Property("SicCodesJson") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.HasKey("CompanyNumber"); + + b.ToTable("CompanyCache"); + }); + + modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ChecksUsedThisMonth") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("StripeCustomerId") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TermsAcceptedAt") + .HasColumnType("datetime2"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b => + { + b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null) + .WithMany("CVChecks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b => + { + b.HasOne("RealCV.Domain.Entities.CVCheck", "CVCheck") + .WithMany("Flags") + .HasForeignKey("CVCheckId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CVCheck"); + }); + + modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b => + { + b.Navigation("CVChecks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/RealCV.Infrastructure/Data/Migrations/20260125074319_AddTermsAcceptedAt.cs b/src/RealCV.Infrastructure/Data/Migrations/20260125074319_AddTermsAcceptedAt.cs new file mode 100644 index 0000000..4bd5be3 --- /dev/null +++ b/src/RealCV.Infrastructure/Data/Migrations/20260125074319_AddTermsAcceptedAt.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RealCV.Infrastructure.Data.Migrations +{ + /// + public partial class AddTermsAcceptedAt : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TermsAcceptedAt", + table: "AspNetUsers", + type: "datetime2", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TermsAcceptedAt", + table: "AspNetUsers"); + } + } +} diff --git a/src/RealCV.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/RealCV.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs index c2b4df7..da3e7c1 100644 --- a/src/RealCV.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/RealCV.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -393,6 +393,9 @@ namespace RealCV.Infrastructure.Data.Migrations .HasMaxLength(256) .HasColumnType("nvarchar(256)"); + b.Property("TermsAcceptedAt") + .HasColumnType("datetime2"); + b.Property("TwoFactorEnabled") .HasColumnType("bit"); diff --git a/src/RealCV.Infrastructure/Identity/ApplicationUser.cs b/src/RealCV.Infrastructure/Identity/ApplicationUser.cs index 96754da..1a51743 100644 --- a/src/RealCV.Infrastructure/Identity/ApplicationUser.cs +++ b/src/RealCV.Infrastructure/Identity/ApplicationUser.cs @@ -12,5 +12,7 @@ public class ApplicationUser : IdentityUser public int ChecksUsedThisMonth { get; set; } + public DateTime? TermsAcceptedAt { get; set; } + public ICollection CVChecks { get; set; } = new List(); } diff --git a/src/RealCV.Web/Components/Pages/Account/Register.razor b/src/RealCV.Web/Components/Pages/Account/Register.razor index f8f7cd5..125e7ba 100644 --- a/src/RealCV.Web/Components/Pages/Account/Register.razor +++ b/src/RealCV.Web/Components/Pages/Account/Register.razor @@ -71,6 +71,17 @@ +
+
+ + +
+ +
+
-

- By creating an account, you agree to our - Terms of Service - and - Privacy Policy -

-
Already have an account?
@@ -183,7 +187,8 @@ UserName = _model.Email, Email = _model.Email, Plan = Domain.Enums.UserPlan.Free, - ChecksUsedThisMonth = 0 + ChecksUsedThisMonth = 0, + TermsAcceptedAt = DateTime.UtcNow }; var result = await UserManager.CreateAsync(user, _model.Password); @@ -222,5 +227,8 @@ [System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Please confirm your password")] [System.ComponentModel.DataAnnotations.Compare(nameof(Password), ErrorMessage = "Passwords do not match")] public string ConfirmPassword { get; set; } = string.Empty; + + [System.ComponentModel.DataAnnotations.Range(typeof(bool), "true", "true", ErrorMessage = "You must agree to the Terms of Service")] + public bool AgreeToTerms { get; set; } } } diff --git a/src/RealCV.Web/wwwroot/app.css b/src/RealCV.Web/wwwroot/app.css index 636c335..04448b0 100644 --- a/src/RealCV.Web/wwwroot/app.css +++ b/src/RealCV.Web/wwwroot/app.css @@ -49,9 +49,9 @@ /* Surface colors */ --realcv-bg-page: #F8FAFC; - --realcv-bg-surface: #FFFFFF; + --realcv-bg-surface: #FAFAF9; --realcv-bg-muted: #F1F5F9; - --realcv-bg-elevated: #FFFFFF; + --realcv-bg-elevated: #FEFEFE; /* Footer & header */ --realcv-header-bg: #FFFFFF;