Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Backend/Backend/Backend.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.*">
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.13">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.13" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
</ItemGroup>
Expand Down
30 changes: 22 additions & 8 deletions Backend/Backend/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,23 @@ public async Task<IActionResult> GitHubLogin([FromBody] string code)
{ "redirect_uri", _config["GitHub:RedirectUri"] ?? "" }
};

var request = new HttpRequestMessage(HttpMethod.Post, "https://github.com/login/oauth/access_token")
using var request = new HttpRequestMessage(HttpMethod.Post, "https://github.com/login/oauth/access_token")
{
Content = new FormUrlEncodedContent(requestData)
};
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

var response = await client.SendAsync(request);
var result = await response.Content.ReadFromJsonAsync<GitHubTokenResponse>();
return result?.access_token;
try
{
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode) return null;
var result = await response.Content.ReadFromJsonAsync<GitHubTokenResponse>();
return result?.access_token;
}
catch (Exception)
{
return null;
}
}

private async Task<GitHubUserResponse?> GetGitHubUser(string token)
Expand All @@ -92,10 +100,16 @@ public async Task<IActionResult> GitHubLogin([FromBody] string code)
client.DefaultRequestHeaders.UserAgent.ParseAdd("GitQuest-App");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

var response = await client.GetAsync("https://api.github.com/user");
if (!response.IsSuccessStatusCode) return null;

return await response.Content.ReadFromJsonAsync<GitHubUserResponse>();
try
{
var response = await client.GetAsync("https://api.github.com/user");
if (!response.IsSuccessStatusCode) return null;
return await response.Content.ReadFromJsonAsync<GitHubUserResponse>();
}
catch (Exception)
{
return null;
}
}

private string GenerateJwtToken(User user)
Expand Down
56 changes: 50 additions & 6 deletions Backend/Backend/Controllers/IssuesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,14 @@ public async Task<IActionResult> ClaimQuest(long id)

var userId = Guid.Parse(userIdClaim);

// Verify the issue exists in our database and is still active
var issue = await _context.Issues.FirstOrDefaultAsync(i => i.GitHubIssueId == id);
if (issue == null || !issue.IsActive) return NotFound("Issue not found or no longer active.");

var existingQuest = await _context.Quests
.FirstOrDefaultAsync(q => q.UserId == userId && q.GitHubIssueId == id && q.Status == "In Progress");

if (existingQuest != null) return BadRequest("Quest already active.");
if (existingQuest != null) return Conflict("Quest already active.");

var quest = new Quest
{
Expand All @@ -55,7 +59,15 @@ public async Task<IActionResult> ClaimQuest(long id)
};

_context.Quests.Add(quest);
await _context.SaveChangesAsync();
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException)
{
// Unique constraint violation: another concurrent request already claimed this quest
return Conflict("Quest already active.");
}

return Ok(new { message = "Quest claimed!", questId = quest.Id });
}
Expand All @@ -67,7 +79,9 @@ public async Task<IActionResult> GetMyQuests()
if (userIdClaim == null) return Unauthorized();

var userId = Guid.Parse(userIdClaim);
var quests = await _context.Quests.Where(q => q.UserId == userId).ToListAsync();
var quests = await _context.Quests
.Where(q => q.UserId == userId && q.Status == "In Progress")
.ToListAsync();
return Ok(quests);
}

Expand All @@ -83,17 +97,47 @@ public async Task<IActionResult> SubmitQuest(long id)

if (quest == null) return NotFound("Quest not found.");

// Resolve the XP reward from the stored Issue record
var issue = await _context.Issues.FirstOrDefaultAsync(i => i.GitHubIssueId == id);
if (issue == null || !issue.IsActive) return BadRequest("Issue is no longer available.");

quest.Status = "Completed";
quest.CompletedAt = DateTime.UtcNow;

var user = await _context.Users.FindAsync(userId);
if (user != null)
{
user.ExperiencePoints += 30;
user.CurrentStreak += 1;
user.ExperiencePoints += issue.XPReward;

// Update streak based on contribution dates (once per day)
var today = DateTime.UtcNow.Date;
if (user.LastContributionDate.HasValue)
{
var lastDate = user.LastContributionDate.Value.Date;
if (lastDate == today)
{
// Already contributed today – do not increment streak
}
else if (lastDate == today.AddDays(-1))
{
// Contributed yesterday – extend streak
user.CurrentStreak += 1;
}
else
{
// Gap in contributions – reset streak
user.CurrentStreak = 1;
}
}
else
{
user.CurrentStreak = 1;
}

user.LastContributionDate = DateTime.UtcNow;
}

await _context.SaveChangesAsync();
return Ok(new { message = "30 XP Awarded!", totalXp = user?.ExperiencePoints });
return Ok(new { message = $"{issue.XPReward} XP Awarded!", totalXp = user?.ExperiencePoints });
}
}
24 changes: 23 additions & 1 deletion Backend/Backend/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@ public async Task<IActionResult> GetProfile(string username)

if (user == null) return NotFound();

return Ok(user);
var profile = new
{
user.GitHubUsername,
user.AvatarUrl,
user.ExperiencePoints,
user.CurrentStreak,
user.LastContributionDate,
Contributions = user.Contributions.Select(c => new
{
c.CompletedAt,
c.PullRequestUrl,
Issue = c.Issue == null ? null : new
{
c.Issue.Title,
c.Issue.RepoFullName,
c.Issue.IssueUrl,
c.Issue.Difficulty,
c.Issue.XPReward
}
})
};

return Ok(profile);
}
}
Loading
Loading