Skip to content

Commit 50f6e92

Browse files
WIP on Referral Service
1 parent f0aa969 commit 50f6e92

File tree

10 files changed

+358
-228
lines changed

10 files changed

+358
-228
lines changed

Directory.Packages.props

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@
1616
<PackageVersion Include="ContentFeedNuget" Version="$(ToolingPackagesVersion)" />
1717
</ItemGroup>
1818
<ItemGroup>
19-
<PackageVersion Include="Sqids" Version="3.1.0" />
20-
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
19+
<PackageVersion Include="Azure.Identity" Version="1.12.1" />
2120
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
2221
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
2322
<PackageVersion Include="EssentialCSharp.Shared.Models" Version="$(ToolingPackagesVersion)" />
24-
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
2523
<PackageVersion Include="AspNet.Security.OAuth.GitHub" Version="8.3.0" />
2624
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
2725
<PackageVersion Include="HtmlAgilityPack" Version="1.11.72" />
@@ -32,13 +30,16 @@
3230
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.12" />
3331
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.12" />
3432
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.12" />
33+
<PackageVersion Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.8" />
34+
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
3535
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
3636
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10" />
3737
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
3838
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
3939
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0" />
4040
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
4141
<PackageVersion Include="Octokit" Version="14.0.0" />
42+
<PackageVersion Include="Sqids" Version="3.1.0" />
4243
<PackageVersion Include="xunit" Version="2.9.3" />
4344
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.1" />
4445
</ItemGroup>

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ public class EssentialCSharpWebUser : IdentityUser
99
public virtual string? FirstName { get; set; }
1010
[ProtectedPersonalData]
1111
public virtual string? LastName { get; set; }
12+
public string? ReferrerId { get; set; }
13+
public int ReferralCount { get; set; }
1214
}
1315

EssentialCSharp.Web/EssentialCSharp.Web.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,24 @@
1414
</ItemGroup>
1515

1616
<ItemGroup>
17+
<PackageReference Include="Azure.Identity" />
1718
<PackageReference Include="AspNet.Security.OAuth.GitHub" />
1819
<PackageReference Include="EssentialCSharp.Shared.Models" />
1920
<PackageReference Include="HtmlAgilityPack" />
2021
<PackageReference Include="IntelliTect.Multitool" />
2122
<PackageReference Include="Mailjet.Api" />
23+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
2224
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" />
2325
<PackageReference Include="Microsoft.AspNetCore.Components" />
2426
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
2527
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" />
26-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
28+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
29+
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" />
2730
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
2831
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
2932
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3033
</PackageReference>
3134
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" />
32-
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
3335
<PackageReference Include="Newtonsoft.Json" />
3436
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
3537
<PackageReference Include="Octokit" />
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System.Security.Claims;
2+
using System.Web;
3+
using EssentialCSharp.Web.Areas.Identity.Data;
4+
using EssentialCSharp.Web.Services;
5+
using Microsoft.AspNetCore.Http.Extensions;
6+
using Microsoft.AspNetCore.Identity;
7+
8+
namespace EssentialCSharp.Web.Middleware;
9+
10+
public class ReferralMiddleware
11+
{
12+
private readonly RequestDelegate _Next;
13+
14+
public ReferralMiddleware(RequestDelegate next)
15+
{
16+
_Next = next;
17+
}
18+
19+
public async Task InvokeAsync(HttpContext context, IReferralService referralService, UserManager<EssentialCSharpWebUser> userManager)
20+
{
21+
// Retrieve current referral Id for processing
22+
System.Collections.Specialized.NameValueCollection query = HttpUtility.ParseQueryString(context.Request.QueryString.Value!);
23+
string? referralId = query["rid"];
24+
string? userReferralId;
25+
26+
if (context.User is { } claimsUser && claimsUser.Identity is not null && claimsUser.Identity.IsAuthenticated)
27+
{
28+
if (!string.IsNullOrWhiteSpace(referralId))
29+
{
30+
await TrackReferralAsync(referralService, referralId, claimsUser);
31+
}
32+
33+
// Add the referralId to the request context if it exists on a user
34+
EssentialCSharpWebUser? user = await userManager.GetUserAsync(claimsUser);
35+
if (user is not null)
36+
{
37+
userReferralId = await referralService.GetReferralIdAsync(user.Id);
38+
39+
if (!string.IsNullOrWhiteSpace(userReferralId) && (string.IsNullOrWhiteSpace(query["rid"]) || query["rid"] != userReferralId))
40+
{
41+
query.Remove("rid");
42+
query.Add("rid", userReferralId);
43+
var builder = new UriBuilder(context.Request.GetEncodedUrl())
44+
{
45+
Query = query.ToString()
46+
};
47+
context.Response.Redirect(builder.ToString());
48+
return;
49+
}
50+
}
51+
}
52+
else
53+
{
54+
55+
if (!string.IsNullOrWhiteSpace(referralId))
56+
{
57+
await TrackReferralAsync(referralService, referralId, null);
58+
query.Remove("rid");
59+
var builder = new UriBuilder(context.Request.GetEncodedUrl())
60+
{
61+
Query = query.ToString()
62+
};
63+
context.Response.Redirect(builder.ToString());
64+
return;
65+
}
66+
}
67+
68+
await _Next(context);
69+
70+
static async Task TrackReferralAsync(IReferralService referralService, string? referralId, ClaimsPrincipal? claimsUser)
71+
{
72+
if (!string.IsNullOrWhiteSpace(referralId))
73+
{
74+
_ = await referralService.TrackReferralAsync(referralId, claimsUser);
75+
}
76+
}
77+
}
78+
}

EssentialCSharp.Web/Program.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,10 @@ private static void Main(string[] args)
110110
{
111111
// This is a shuffled version of the default alphabet so the id's are at least unique to this site.
112112
// This being open source, it will be easy to decode the ids, but these id's are not meant to be secure.
113-
Alphabet = "imx4BSz2Ys7GZLXDqT5IAkUOEnyvwbPKJtp13NWdeuH6rFfRhCcQogjaM8V09l",
114-
MinLength = 10,
115-
}));
113+
Alphabet = "imx4z2Ys7GZLXDqT5IkUOEnyvwPKJtp13NWdeuH6rRhCcQogjM8V09l",
114+
MinLength = 7,
115+
}));
116+
builder.Services.AddScoped<IReferralService, ReferralService>();
116117

117118
if (!builder.Environment.IsDevelopment())
118119
{
@@ -154,12 +155,6 @@ private static void Main(string[] args)
154155

155156
WebApplication app = builder.Build();
156157

157-
app.Use((context, next) =>
158-
{
159-
context.Request.Scheme = "https";
160-
return next(context);
161-
});
162-
163158
app.UseForwardedHeaders();
164159

165160
// Configure the HTTP request pipeline.
@@ -180,14 +175,21 @@ private static void Main(string[] args)
180175

181176
app.UseAuthentication();
182177
app.UseAuthorization();
178+
app.UseMiddleware<ReferralMiddleware>();
179+
180+
app.Use((context, next) =>
181+
{
182+
context.Request.Scheme = "https";
183+
return next(context);
184+
});
183185

184186
app.MapDefaultControllerRoute();
187+
app.MapRazorPages();
185188

186189
app.MapControllerRoute(
187190
name: "slug",
188191
pattern: "{*key}",
189192
defaults: new { controller = "Home", action = "Index" });
190-
app.MapRazorPages();
191193

192194
app.Run();
193195
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Security.Claims;
2+
3+
namespace EssentialCSharp.Web.Services;
4+
5+
public interface IReferralService
6+
{
7+
Task<bool> TrackReferralAsync(string referralId, ClaimsPrincipal? user);
8+
Task<string?> GetReferralIdAsync(string userId);
9+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System.Security.Claims;
2+
using EssentialCSharp.Web.Areas.Identity.Data;
3+
using EssentialCSharp.Web.Data;
4+
using Microsoft.AspNetCore.Identity;
5+
using Microsoft.EntityFrameworkCore;
6+
using Sqids;
7+
8+
namespace EssentialCSharp.Web.Services;
9+
10+
public class ReferralService(EssentialCSharpWebContext dbContext, SqidsEncoder<int> sqids, UserManager<EssentialCSharpWebUser> userManager) : IReferralService
11+
{
12+
public async Task<string?> GetReferralIdAsync(string userId)
13+
{
14+
EssentialCSharpWebUser? user = await userManager.FindByIdAsync(userId);
15+
if (user is null)
16+
{
17+
return null;
18+
}
19+
else
20+
{
21+
// Check if the user already has a referrer ID
22+
if (!string.IsNullOrEmpty(user.ReferrerId))
23+
{
24+
return user.ReferrerId;
25+
}
26+
else
27+
{
28+
Random random = new();
29+
string referrerId = sqids.Encode(random.Next());
30+
user.ReferrerId = referrerId;
31+
32+
await userManager.AddClaimAsync(user, new Claim("ReferrerId", referrerId));
33+
await userManager.UpdateAsync(user);
34+
return user.ReferrerId;
35+
}
36+
}
37+
}
38+
39+
/// <summary>
40+
/// Track the referral in the database.
41+
/// </summary>
42+
/// <param name="referralId">The referrer ID to track.</param>
43+
/// <returns>True if the referral was successfully tracked, otherwise false.</returns>
44+
public async Task<bool> TrackReferralAsync(string referralId, ClaimsPrincipal? user)
45+
{
46+
EssentialCSharpWebUser? claimsUser = user is null ? null : await userManager.GetUserAsync(user);
47+
if (claimsUser is null)
48+
{
49+
return await TrackReferral(dbContext, referralId);
50+
}
51+
else
52+
{
53+
// If the user is the referrer, do not track the referral
54+
if (claimsUser.ReferrerId == referralId)
55+
{
56+
return false;
57+
}
58+
else
59+
{
60+
return await TrackReferral(dbContext, referralId);
61+
}
62+
}
63+
64+
static async Task<bool> TrackReferral(EssentialCSharpWebContext dbContext, string referralId)
65+
{
66+
EssentialCSharpWebUser? dbUser = await dbContext.Users.SingleOrDefaultAsync(u => u.ReferrerId == referralId);
67+
if (dbUser is null)
68+
{
69+
return false;
70+
}
71+
else
72+
{
73+
dbUser.ReferralCount++;
74+
await dbContext.SaveChangesAsync();
75+
return true;
76+
}
77+
}
78+
}
79+
}

EssentialCSharp.Web/Services/ReferrerService.cs

Lines changed: 0 additions & 43 deletions
This file was deleted.

EssentialCSharp.Web/Views/Shared/_Layout.cshtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,8 @@
271271
}
272272
273273
PREVIOUS_PAGE = @Json.Serialize(ViewBag.PreviousPage)
274-
NEXT_PAGE = @Json.Serialize(ViewBag.NextPage)
275-
TOC_DATA = @Json.Serialize(tocData)
274+
NEXT_PAGE = @Json.Serialize(ViewBag.NextPage)
275+
TOC_DATA = @Json.Serialize(tocData)
276276
</script>
277277

278278
@* Recursive vue component template for rendering the table of contents. *@

0 commit comments

Comments
 (0)