Skip to content
Merged
12 changes: 12 additions & 0 deletions Backend/Backend/Backend.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,19 @@
</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.*">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.*" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
</ItemGroup>

<ItemGroup>
<Folder Include="NewFolder\" />
</ItemGroup>

</Project>
126 changes: 126 additions & 0 deletions Backend/Backend/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using GitQuest.Backend.Models;
using Microsoft.EntityFrameworkCore;
using System.Net.Http.Headers;

namespace GitQuest.Backend.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IConfiguration _config;
private readonly GitQuestContext _context;
private readonly IHttpClientFactory _httpClientFactory;

public AuthController(IConfiguration config, GitQuestContext context, IHttpClientFactory httpClientFactory)
{
_config = config;
_context = context;
_httpClientFactory = httpClientFactory;
}

[HttpPost("github-login")]
public async Task<IActionResult> GitHubLogin([FromBody] string code)
{
// 1. Exchange Code for GitHub Access Token
var githubToken = await GetGitHubAccessToken(code);
if (string.IsNullOrEmpty(githubToken)) return Unauthorized("Invalid GitHub Code");

// 2. Get User Info from GitHub
var githubUser = await GetGitHubUser(githubToken);
if (githubUser is null) return BadRequest("Could not fetch GitHub profile");

// 3. Sync with Database
var user = await _context.Users.FirstOrDefaultAsync(u => u.GitHubId == githubUser.id.ToString());

if (user == null)
{
user = new User
{
Id = Guid.NewGuid(),
GitHubId = githubUser.id.ToString(),
GitHubUsername = githubUser.login,
AvatarUrl = githubUser.avatar_url,
ExperiencePoints = 0
};
_context.Users.Add(user);
}
else
{
user.GitHubUsername = githubUser.login;
user.AvatarUrl = githubUser.avatar_url;
}

await _context.SaveChangesAsync();

// 4. Generate our own JWT
var token = GenerateJwtToken(user);

return Ok(new { token, user });
}

private async Task<string?> GetGitHubAccessToken(string code)
{
var client = _httpClientFactory.CreateClient();
var requestData = new Dictionary<string, string>
{
{ "client_id", _config["GitHub:ClientId"]! },
{ "client_secret", _config["GitHub:ClientSecret"]! },
{ "code", code },
{ "redirect_uri", _config["GitHub:RedirectUri"] ?? "" }
};

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;
}

private async Task<GitHubUserResponse?> GetGitHubUser(string token)
{
var client = _httpClientFactory.CreateClient();
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>();
}

private string GenerateJwtToken(User user)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JwtSettings:Key"]!));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.GitHubUsername),
new Claim("avatar", user.AvatarUrl ?? "")
};

var token = new JwtSecurityToken(
issuer: _config["JwtSettings:Issuer"],
audience: _config["JwtSettings:Audience"],
claims: claims,
expires: DateTime.Now.AddMinutes(double.Parse(_config["JwtSettings:DurationInMinutes"] ?? "1440")),
signingCredentials: credentials);

return new JwtSecurityTokenHandler().WriteToken(token);
}
}

public record GitHubTokenResponse(string access_token);

public record GitHubUserResponse(long id, string login, string avatar_url);
102 changes: 95 additions & 7 deletions Backend/Backend/Controllers/IssuesController.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,99 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using GitQuest.Backend.Services;
using GitQuest.Backend.Models;

namespace Backend.Controllers
namespace GitQuest.Backend.Controllers;

[ApiController]
[Route("api/[controller]")]
// This forces the controller to use the JWT scheme we defined in Program.cs
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class IssuesController : ControllerBase
{
[Route("api/[controller]")]
[ApiController]
public class IssuesController : ControllerBase
private readonly GitHubService _githubService;
private readonly GitQuestContext _context;

public IssuesController(GitHubService githubService, GitQuestContext context)
{
_githubService = githubService;
_context = context;
}

[HttpGet("discover")]
[AllowAnonymous]
public async Task<IActionResult> GetIssues([FromQuery] string language = "typescript")
{
var issues = await _githubService.GetSuggestedIssues(language);
if (issues == null) return BadRequest("Could not fetch issues.");
return Ok(issues);
}

[HttpPost("{id}/claim")]
public async Task<IActionResult> ClaimQuest(long id)
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdClaim)) return Unauthorized("User ID missing from token.");

var userId = Guid.Parse(userIdClaim);

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.");

var quest = new Quest
{
Id = Guid.NewGuid(),
UserId = userId,
GitHubIssueId = id,
Status = "In Progress",
CreatedAt = DateTime.UtcNow
};

_context.Quests.Add(quest);
await _context.SaveChangesAsync();

return Ok(new { message = "Quest claimed!", questId = quest.Id });
}

[HttpGet("my-active-quests")]
public async Task<IActionResult> GetMyQuests()
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userIdClaim == null) return Unauthorized();

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

[HttpPost("{id}/submit")]
public async Task<IActionResult> SubmitQuest(long id)
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userIdClaim == null) return Unauthorized();
var userId = Guid.Parse(userIdClaim);

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

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

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

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

await _context.SaveChangesAsync();
return Ok(new { message = "30 XP Awarded!", totalXp = user?.ExperiencePoints });
}
}
}
30 changes: 24 additions & 6 deletions Backend/Backend/Controllers/ProjectsController.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
using Microsoft.AspNetCore.Http;

using GitQuest.Backend.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Backend.Controllers
namespace GitQuest.Backend.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProjectsController : ControllerBase
{
[Route("api/[controller]")]
[ApiController]
public class ProjectsController : ControllerBase
private readonly GitQuestContext _context;

public ProjectsController(GitQuestContext context) => _context = context;

[HttpGet]
public async Task<IActionResult> GetProjects()
{
return Ok(await _context.Projects.ToListAsync());
}

[HttpGet("{id}")]
public async Task<IActionResult> GetProject(int id)
{
var project = await _context.Projects.FindAsync(id);
if (project == null) return NotFound();
return Ok(project);
}
}
}
43 changes: 37 additions & 6 deletions Backend/Backend/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
using Microsoft.AspNetCore.Http;
using GitQuest.Backend.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Backend.Controllers
namespace GitQuest.Backend.Controllers;

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
private readonly GitQuestContext _context;

public UsersController(GitQuestContext context) => _context = context;

// GET: api/users/leaderboard
[HttpGet("leaderboard")]
public async Task<IActionResult> GetLeaderboard()
{
var topUsers = await _context.Users
.OrderByDescending(u => u.ExperiencePoints)
.Take(10)
.Select(u => new { u.GitHubUsername, u.AvatarUrl, u.ExperiencePoints, u.CurrentStreak })
.ToListAsync();

return Ok(topUsers);
}

// GET: api/users/profile/{username}
[HttpGet("profile/{username}")]
public async Task<IActionResult> GetProfile(string username)
{
var user = await _context.Users
.Include(u => u.Contributions)
.ThenInclude(c => c.Issue)
.FirstOrDefaultAsync(u => u.GitHubUsername == username);

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

return Ok(user);
}
}
}
Loading
Loading