Skip to content

Commit 7177b74

Browse files
feat: Create Referrer Id's to track link shares for users (#722)
Fixes #296 This pull request introduces a new feature for tracking user referrals. ### Referral Tracking Feature: * **User Model Updates:** - Added `ReferrerId` and `ReferralCount` properties to the `EssentialCSharpWebUser` class to store referral information. * **New Referral Management Pages:** - Created `Referrals.cshtml` and `Referrals.cshtml.cs` to display and manage user referrals. [[1]](diffhunk://#diff-752d7304fd36a80c1dc6abf60983315eed978a71c5d12df02486a2f682ebbb6fR1-R19) [[2]](diffhunk://#diff-434b8ee03d79db9f0dc6186422a25cc5263ac31423c97e9f3b9d42c0a2efed1bR1-R34) - Updated `Index.cshtml.cs` to include `ReferralCount` in the user profile management page. [[1]](diffhunk://#diff-8be953d1cc6b4774e56b1ba065e06840632b9cf4ecf8511951677863b785eb26R18-R19) [[2]](diffhunk://#diff-8be953d1cc6b4774e56b1ba065e06840632b9cf4ecf8511951677863b785eb26R51) * **Navigation Adjustments:** - Added referral navigation links and classes in `ManageNavPages.cs` and `_ManageNav.cshtml`. [[1]](diffhunk://#diff-aee3bd19ba7a9fbacaba3c6dfb1c8c0fa727b1ecb2cc30758807ebd1b84bd2f0R27-R28) [[2]](diffhunk://#diff-aee3bd19ba7a9fbacaba3c6dfb1c8c0fa727b1ecb2cc30758807ebd1b84bd2f0R45-R46) [[3]](diffhunk://#diff-f06d8d62adb1a086d652f1d1fbfd39cf0c5b3fc8c61b38aadc6eef5d384fa85eL6-R15) ### Middleware for Referral Tracking: * **ReferralTrackingMiddleware:** - Implemented `ReferralTrackingMiddleware` to handle referral tracking logic, including processing referral IDs from query strings and updating user referral counts.
1 parent ebdeb4d commit 7177b74

22 files changed

+810
-258
lines changed

.editorconfig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,9 @@ csharp_preserve_single_line_blocks = true
125125
# IntelliTect Conventions #
126126
###############################
127127
# var preferences
128-
csharp_style_var_for_built_in_types = false:warning
128+
csharp_style_var_for_built_in_types = false:none
129129
csharp_style_var_when_type_is_apparent = true:suggestion
130-
csharp_style_var_elsewhere = false:warning
130+
csharp_style_var_elsewhere = false:none
131131

132132
## Naming
133133
# Style Definitions

Directory.Packages.props

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,10 @@
1616
<PackageVersion Include="ContentFeedNuget" Version="$(ToolingPackagesVersion)" />
1717
</ItemGroup>
1818
<ItemGroup>
19-
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.1" />
20-
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
21-
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
22-
<PackageVersion Include="EssentialCSharp.Shared.Models" Version="$(ToolingPackagesVersion)" />
23-
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
2419
<PackageVersion Include="AspNet.Security.OAuth.GitHub" Version="8.3.0" />
20+
<PackageVersion Include="Azure.Identity" Version="1.12.1" />
2521
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
22+
<PackageVersion Include="EssentialCSharp.Shared.Models" Version="$(ToolingPackagesVersion)" />
2623
<PackageVersion Include="HtmlAgilityPack" Version="1.11.72" />
2724
<PackageVersion Include="IntelliTect.Multitool" Version="1.5.3" />
2825
<PackageVersion Include="Mailjet.Api" Version="3.0.0" />
@@ -31,6 +28,9 @@
3128
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.12" />
3229
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.12" />
3330
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.12" />
31+
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
32+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
33+
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
3434
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
3535
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10" />
3636
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />

EssentialCSharp.Web/Areas/Identity/Data/EssentialCSharpWebContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace EssentialCSharp.Web.Data;
66

7-
public class EssentialCSharpWebContext(DbContextOptions<EssentialCSharpWebContext> options) : IdentityDbContext<EssentialCSharpWebUser>(options)
7+
public class EssentialCSharpWebContext(DbContextOptions options) : IdentityDbContext<EssentialCSharpWebUser>(options)
88
{
99
protected override void OnModelCreating(ModelBuilder builder)
1010
{

EssentialCSharp.Web/Areas/Identity/Data/EssentialCSharpWebUser.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ public class EssentialCSharpWebUser : IdentityUser
99
public virtual string? FirstName { get; set; }
1010
[ProtectedPersonalData]
1111
public virtual string? LastName { get; set; }
12+
public int ReferralCount { get; set; }
1213
}
1314

EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
using System.ComponentModel.DataAnnotations;
22
using EssentialCSharp.Web.Areas.Identity.Data;
3+
using EssentialCSharp.Web.Services.Referrals;
34
using Microsoft.AspNetCore.Authentication;
45
using Microsoft.AspNetCore.Identity;
56
using Microsoft.AspNetCore.Mvc;
67
using Microsoft.AspNetCore.Mvc.RazorPages;
78

89
namespace EssentialCSharp.Web.Areas.Identity.Pages.Account;
910

10-
public class LoginModel(SignInManager<EssentialCSharpWebUser> signInManager, UserManager<EssentialCSharpWebUser> userManager, ILogger<LoginModel> logger) : PageModel
11+
public class LoginModel(SignInManager<EssentialCSharpWebUser> signInManager, UserManager<EssentialCSharpWebUser> userManager, ILogger<LoginModel> logger, IReferralService referralService) : PageModel
1112
{
1213
private InputModel? _Input;
1314
[BindProperty]
@@ -77,6 +78,8 @@ public async Task<IActionResult> OnPostAsync(string? returnUrl = null)
7778
if (foundUser is not null)
7879
{
7980
result = await signInManager.PasswordSignInAsync(foundUser, Input.Password, Input.RememberMe, lockoutOnFailure: true);
81+
// Call the referral service to get the referral ID and set it onto the user claim
82+
_ = await referralService.EnsureReferralIdAsync(foundUser);
8083
}
8184
else
8285
{

EssentialCSharp.Web/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public static class ManageNavPages
2424

2525
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
2626

27+
public static string Referrals => "Referrals";
28+
2729
public static string? IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
2830

2931
public static string? EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email);
@@ -40,6 +42,8 @@ public static class ManageNavPages
4042

4143
public static string? TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
4244

45+
public static string? ReferralsNavClass(ViewContext viewContext) => PageNavClass(viewContext, Referrals);
46+
4347
public static string? PageNavClass(ViewContext viewContext, string page)
4448
{
4549
string? activePage = viewContext.ViewData["ActivePage"] as string
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@page
2+
@model ReferralsDataModel
3+
@{
4+
ViewData["Title"] = "Referrals";
5+
ViewData["ActivePage"] = ManageNavPages.Referrals;
6+
}
7+
8+
<h3>@ViewData["Title"]</h3>
9+
10+
<div>
11+
<p>
12+
You have <strong>@Model.ReferralCount</strong> referrals.
13+
</p>
14+
</div>
15+
16+
@section Scripts {
17+
<partial name="_ValidationScriptsPartial" />
18+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using EssentialCSharp.Web.Areas.Identity.Data;
2+
using Microsoft.AspNetCore.Identity;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.AspNetCore.Mvc.RazorPages;
5+
6+
namespace EssentialCSharp.Web.Areas.Identity.Pages.Account.Manage;
7+
8+
public class ReferralsDataModel : PageModel
9+
{
10+
private readonly UserManager<EssentialCSharpWebUser> _UserManager;
11+
private readonly ILogger<ReferralsDataModel> _Logger;
12+
13+
public int ReferralCount { get; private set; }
14+
15+
public ReferralsDataModel(
16+
UserManager<EssentialCSharpWebUser> userManager,
17+
ILogger<ReferralsDataModel> logger)
18+
{
19+
_UserManager = userManager;
20+
_Logger = logger;
21+
}
22+
23+
public async Task<IActionResult> OnGetAsync()
24+
{
25+
EssentialCSharpWebUser? user = await _UserManager.GetUserAsync(User);
26+
if (user is null)
27+
{
28+
return NotFound($"Unable to load user with ID '{_UserManager.GetUserId(User)}'.");
29+
}
30+
ReferralCount = user.ReferralCount;
31+
32+
return Page();
33+
}
34+
}

EssentialCSharp.Web/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
44
}
55
<ul class="nav nav-pills flex-column">
6-
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
7-
<li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>
8-
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
6+
<li class="nav-item"><a class="account-nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
7+
<li class="nav-item"><a class="account-nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>
8+
<li class="nav-item"><a class="account-nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
99
@if (hasExternalLogins)
1010
{
11-
<li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>
11+
<li id="external-logins" class="nav-item"><a id="external-login" class="account-nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>
1212
}
13-
<li class="nav-item"><a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li>
14-
<li class="nav-item"><a class="nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data" asp-page="./PersonalData">Personal data</a></li>
13+
<li class="nav-item"><a class="account-nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li>
14+
<li class="nav-item"><a class="account-nav-link @ManageNavPages.ReferralsNavClass(ViewContext)" id="referrals" asp-page="./Referrals">Referrals</a></li>
15+
<li class="nav-item"><a class="account-nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data" asp-page="./PersonalData">Personal data</a></li>
1516
</ul>

EssentialCSharp.Web/Controllers/HomeController.cs

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,20 @@
77

88
namespace EssentialCSharp.Web.Controllers;
99

10-
public class HomeController : Controller
10+
public class HomeController(ILogger<HomeController> logger, IWebHostEnvironment hostingEnvironment, ISiteMappingService siteMappingService, IHttpContextAccessor httpContextAccessor) : Controller
1111
{
12-
private readonly IConfiguration _Configuration;
13-
private readonly IWebHostEnvironment _HostingEnvironment;
14-
private readonly ISiteMappingService _SiteMappingService;
15-
private readonly ILogger<HomeController> _Logger;
16-
17-
public HomeController(ILogger<HomeController> logger, IWebHostEnvironment hostingEnvironment, ISiteMappingService siteMappingService, IConfiguration configuration)
18-
{
19-
_Logger = logger;
20-
_HostingEnvironment = hostingEnvironment;
21-
_SiteMappingService = siteMappingService;
22-
_Configuration = configuration;
23-
}
24-
2512
public IActionResult Index(string key)
2613
{
2714
// if no key (default case), then load up home page
28-
SiteMapping? siteMapping = _SiteMappingService.SiteMappings.Find(key);
15+
SiteMapping? siteMapping = siteMappingService.SiteMappings.Find(key);
2916

3017
if (string.IsNullOrEmpty(key))
3118
{
3219
return RedirectToAction(nameof(Home));
3320
}
3421
else if (siteMapping is not null)
3522
{
36-
string filePath = Path.Combine(_HostingEnvironment.ContentRootPath, Path.Combine(siteMapping.PagePath));
23+
string filePath = Path.Combine(hostingEnvironment.ContentRootPath, Path.Combine(siteMapping.PagePath));
3724
HtmlDocument doc = new();
3825
doc.Load(filePath);
3926
string headHtml = doc.DocumentNode.Element("html").Element("head").InnerHtml;
@@ -44,6 +31,8 @@ public IActionResult Index(string key)
4431
ViewBag.PreviousPage = FlipPage(siteMapping.ChapterNumber, siteMapping.PageNumber, false);
4532
ViewBag.HeadContents = headHtml;
4633
ViewBag.Contents = html;
34+
// Set the referral Id for use in the front end if available
35+
ViewBag.ReferralId = httpContextAccessor.HttpContext?.User?.Claims?.FirstOrDefault(f => f.Type == ClaimsExtensions.ReferrerIdClaimType)?.Value;
4736
return View();
4837
}
4938
else
@@ -84,19 +73,19 @@ public IActionResult Home()
8473
public IActionResult Guidelines()
8574
{
8675
ViewBag.PageTitle = "Coding Guidelines";
87-
FileInfo fileInfo = new(Path.Combine(_HostingEnvironment.ContentRootPath, "Guidelines", "guidelines.json"));
76+
FileInfo fileInfo = new(Path.Combine(hostingEnvironment.ContentRootPath, "Guidelines", "guidelines.json"));
8877
if (!fileInfo.Exists)
8978
{
9079
return RedirectToAction(nameof(Error), new { errorMessage = "Guidelines could not be found", statusCode = 404 });
9180
}
92-
ViewBag.Guidelines = fileInfo.ReadGuidelineJsonFromInputDirectory(_Logger);
81+
ViewBag.Guidelines = fileInfo.ReadGuidelineJsonFromInputDirectory(logger);
9382
ViewBag.GuidelinesUrl = Request.Path.Value;
9483
return View();
9584
}
9685

9786
private string FlipPage(int currentChapter, int currentPage, bool next)
9887
{
99-
if (_SiteMappingService.SiteMappings.Count == 0)
88+
if (siteMappingService.SiteMappings.Count == 0)
10089
{
10190
return "";
10291
}
@@ -107,18 +96,18 @@ private string FlipPage(int currentChapter, int currentPage, bool next)
10796
page = 1;
10897
}
10998

110-
SiteMapping? siteMap = _SiteMappingService.SiteMappings.FirstOrDefault(f => f.ChapterNumber == currentChapter && f.PageNumber == currentPage + page);
99+
SiteMapping? siteMap = siteMappingService.SiteMappings.FirstOrDefault(f => f.ChapterNumber == currentChapter && f.PageNumber == currentPage + page);
111100

112101
if (siteMap is null)
113102
{
114103
if (next)
115104
{
116-
siteMap = _SiteMappingService.SiteMappings.FirstOrDefault(f => f.ChapterNumber == currentChapter + 1 && f.PageNumber == 1);
105+
siteMap = siteMappingService.SiteMappings.FirstOrDefault(f => f.ChapterNumber == currentChapter + 1 && f.PageNumber == 1);
117106
}
118107
else
119108
{
120-
int? previousPage = _SiteMappingService.SiteMappings.LastOrDefault(f => f.ChapterNumber == currentChapter - 1)?.PageNumber;
121-
siteMap = _SiteMappingService.SiteMappings.FirstOrDefault(f => f.ChapterNumber == currentChapter - 1 && f.PageNumber == previousPage);
109+
int? previousPage = siteMappingService.SiteMappings.LastOrDefault(f => f.ChapterNumber == currentChapter - 1)?.PageNumber;
110+
siteMap = siteMappingService.SiteMappings.FirstOrDefault(f => f.ChapterNumber == currentChapter - 1 && f.PageNumber == previousPage);
122111
}
123112
if (siteMap is null)
124113
{

0 commit comments

Comments
 (0)