Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ adminsetting.json
# ignore local appsettings with secrets
appsettings.json
appsettings.Development.json
adminsettings.json

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
Expand Down
51 changes: 40 additions & 11 deletions Project.App/Project.Api/Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,26 @@ namespace Project.Api.Controllers
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly IUserRepository _userRepository;
private readonly IUserService _userService;

public UserController(IUserRepository userRepository)
public UserController(IUserService userService)
{
_userRepository = userRepository;
_userService = userService;
}

// GET: api/user
[HttpGet]
public async Task<ActionResult<IEnumerable<User>>> GetAllUsers()
{
var users = await _userRepository.GetAllAsync();
var users = await _userService.GetAllUsersAsync();
return Ok(users);
}

// GET: api/user/{id}
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUserById(Guid id)
{
var user = await _userRepository.GetByIdAsync(id);
var user = await _userService.GetUserByIdAsync(id);
if (user == null)
return NotFound($"User with ID {id} not found.");
return Ok(user);
Expand All @@ -42,7 +42,7 @@ public async Task<ActionResult<User>> CreateUser([FromBody] User user)
if (!ModelState.IsValid)
return BadRequest(ModelState);

await _userRepository.AddAsync(user);
await _userService.CreateUserAsync(user);
return CreatedAtAction(nameof(GetUserById), new { id = user.Id }, user);
}

Expand All @@ -53,7 +53,7 @@ public async Task<IActionResult> UpdateUser(Guid id, [FromBody] User user)
if (id != user.Id)
return BadRequest("User ID mismatch.");

var existingUser = await _userRepository.GetByIdAsync(id);
var existingUser = await _userService.GetUserByIdAsync(id);
if (existingUser == null)
return NotFound($"User with ID {id} not found.");

Expand All @@ -62,19 +62,46 @@ public async Task<IActionResult> UpdateUser(Guid id, [FromBody] User user)
existingUser.Email = user.Email;
existingUser.Balance = user.Balance;

await _userRepository.UpdateAsync(existingUser);
await _userService.UpdateUserAsync(id, existingUser);
return NoContent();
}

// PATCH: api/user/{id}
[HttpPatch("{id}")]
[Authorize]
public async Task<IActionResult> UpdateBalance(
Guid id,
[FromBody] UpdateBalanceRequest body
)
{
if (body is null)
return BadRequest("Missing body.");

// only allow the logged-in user to change their own balance
var emailClaim = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value;
if (string.IsNullOrWhiteSpace(emailClaim))
return Unauthorized();

var me = await _userService.GetUserByEmailAsync(emailClaim);
if (me is null)
return Unauthorized();

if (me.Id != id)
return Forbid(); // 403 if trying to edit someone else

await _userService.UpdateUserBalanceAsync(id, body.Balance);
return NoContent();
}

// DELETE: api/user/{id}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUser(Guid id)
{
var existingUser = await _userRepository.GetByIdAsync(id);
var existingUser = await _userService.GetUserByIdAsync(id);
if (existingUser == null)
return NotFound($"User with ID {id} not found.");

await _userRepository.DeleteAsync(id);
await _userService.DeleteUserAsync(id);
return NoContent();
}

Expand All @@ -87,7 +114,7 @@ public async Task<IActionResult> Me([FromServices] IUserService users)
if (email is null)
return Unauthorized();

var u = await users.GetByEmailAsync(email.Value);
var u = await users.GetUserByEmailAsync(email.Value);
if (u is null)
return NotFound();
//temporary user dto since we dont have one made
Expand All @@ -103,4 +130,6 @@ public async Task<IActionResult> Me([FromServices] IUserService users)
);
}
}

public record UpdateBalanceRequest(double Balance);
}
134 changes: 108 additions & 26 deletions Project.App/Project.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.EntityFrameworkCore;
using Project.Api.Data;
using Project.Api.Repositories;
Expand Down Expand Up @@ -45,9 +49,27 @@ public static void Main(string[] args)

var app = builder.Build();

// optional/safe auto-migrate in other words our startup wont crash if the our migration model doenst match the DB
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
try
{
db.Database.Migrate();
Log.Information("[Startup] Database migration check complete.");
}
catch (Exception ex)
{
Log.Warning(
ex,
"[Startup] Database migration skipped or failed. Continuing startup."
);
}
}

app.UseMiddleware<GlobalExceptionHandler>();

// map default endpoint (shows connection string)
// connection string print
app.MapGet(
"/string",
() =>
Expand All @@ -57,7 +79,7 @@ public static void Main(string[] args)
}
);

// Configure the HTTP request pipeline.
// is development envrionment
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
Expand Down Expand Up @@ -175,13 +197,14 @@ IWebHostEnvironment environment
// where the app reads identity from on each request
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// how the app prompts an unauthenticated user to log in
// you can set DefaultChallengeScheme to Cookies instead but it didnt work before
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie(cookie =>
{
// cross-site cookie SPA on :3000 to API on :7069
cookie.Cookie.SameSite = SameSiteMode.None;
// browsers require Secure when SameSite=None this is why we need https instead of http
// browsers require Secure when SameSite=None
cookie.Cookie.SecurePolicy = CookieSecurePolicy.Always;

// for APIs return status codes instead of 302 redirects
Expand All @@ -201,37 +224,96 @@ IWebHostEnvironment environment
})
.AddGoogle(options =>
{
options.ClientId = configuration["Google:ClientId"]!; // from secrets / config
options.ClientSecret = configuration["Google:ClientSecret"]!; // from secrets / config
options.CallbackPath = "/auth/google/callback"; // google redirects here after login if we change this we need to change it on google cloud as well!
//options.Events = new OAuthEvents
{ //TEMPORARILY COMMENTED OUR BELOW BECAUSE IT WAS MESSING WITH GOOGLE AUTH LOGIN FOR SOME REASON GOTTA CHECK THIS LATER
// OnCreatingTicket = async ctx => //currently we are accessing the user json that we get back from google oauth and using it as a quick validation check since email is our unique primary identified on users rn
{ /*
var email = ctx.User.GetProperty("email").GetString(); //all of these checks are quick validation can be moved elsewhere when we decide where to put it
options.ClientId = configuration["Google:ClientId"]!;
options.ClientSecret = configuration["Google:ClientSecret"]!;
options.CallbackPath = "/auth/google/callback";
options.Scope.Add("email");
options.Scope.Add("profile");

// ensure we actually fetch profile claims from Google UserInfo endpoint
options.SaveTokens = true;
options.UserInformationEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo";
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey("picture", "picture");
options.ClaimActions.MapJsonKey("email_verified", "email_verified");

// Upsert user into DB when Google signs in successfully
options.Events = new OAuthEvents
{
OnCreatingTicket = async ctx =>
{
var log = ctx.HttpContext.RequestServices.GetRequiredService<
ILogger<Program>
>();

// fetch the full user JSON from Google's userinfo endpoint
var request = new System.Net.Http.HttpRequestMessage(
System.Net.Http.HttpMethod.Get,
ctx.Options.UserInformationEndpoint
);
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer",
ctx.AccessToken
);

var response = await ctx.Backchannel.SendAsync(request);
response.EnsureSuccessStatusCode();

using var userJson = JsonDocument.Parse(
await response.Content.ReadAsStringAsync()
);
var j = userJson.RootElement;

var email = j.TryGetProperty("email", out var e) ? e.GetString() : null;
var verified =
ctx.User.TryGetProperty("email_verified", out var ev)
&& ev.GetBoolean();
var name = ctx.User.TryGetProperty("name", out var n)
? n.GetString()
: null;
var picture = ctx.User.TryGetProperty("picture", out var p)
? p.GetString()
: null;
j.TryGetProperty("email_verified", out var v) && v.GetBoolean();
var name = j.TryGetProperty("name", out var n) ? n.GetString() : null;
var picture = j.TryGetProperty("picture", out var p) ? p.GetString() : null;

log.LogInformation(
"[OAuth] CreatingTicket: email={Email}, verified={Verified}, name={Name}",
email,
verified,
name
);

if (string.IsNullOrWhiteSpace(email) || !verified)
{
log.LogWarning("[OAuth] Missing or unverified email; failing ticket.");
ctx.Fail("Google email must be present and verified.");
return;
}

var svc =
ctx.HttpContext.RequestServices.GetRequiredService<IUserService>();
await svc.UpsertGoogleUserByEmailAsync(email, name, picture);
*/
}
}
;
try
{
using var scope = ctx.HttpContext.RequestServices.CreateScope();
var users = scope.ServiceProvider.GetRequiredService<IUserService>();

var user = await users.UpsertGoogleUserByEmailAsync(
email!,
name,
picture
);

log.LogInformation(
"[OAuth] Upsert complete: UserId={UserId}, Email={UserEmail}",
user.Id,
user.Email
);
}
catch (Exception ex)
{
log.LogError(
ex,
"[OAuth] Error during UpsertGoogleUserByEmailAsync for {Email}",
email
);
ctx.Fail("Internal error during user creation.");
}
},
};
});

services.AddAuthorization();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Reflection.Metadata;
using Project.Api.Models;

namespace Project.Api.Repositories.Interface
Expand All @@ -9,6 +10,7 @@ public interface IUserRepository
Task<User?> GetByEmailAsync(string email);
Task AddAsync(User user);
Task UpdateAsync(User user);
Task UpdateBalanceAsync(User user);
Task DeleteAsync(Guid id);
Task SaveChangesAsync();
}
Expand Down
7 changes: 7 additions & 0 deletions Project.App/Project.Api/Repositories/UserRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ public async Task UpdateAsync(User user)
await _context.SaveChangesAsync();
}

// Update user balance
public async Task UpdateBalanceAsync(User user)
{
_context.Users.Update(user);
await _context.SaveChangesAsync();
}

// Delete user by ID
public async Task DeleteAsync(Guid id)
{
Expand Down
2 changes: 1 addition & 1 deletion Project.App/Project.Api/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public async Task<User> UpdateUserBalanceAsync(Guid userId, double newBalance)
}

user.Balance = newBalance;
await _repo.UpdateAsync(user);
await _repo.UpdateBalanceAsync(user);
return user;
}
catch (Exception e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default function AddCreditsModal({ isOpen, onClose, balance, creditsToAdd
type="number"
step="10"
min="0"
max="200000"
value={creditsToAdd}
onChange={(e) => setCreditsToAdd(e.target.value)}
placeholder="0"
Expand Down
2 changes: 1 addition & 1 deletion Project.App/Project.Client/src/app/components/NavBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function NavBar() {
if (pathname.startsWith('/player/')) {
links = [{ href: '/rooms', label: 'Rooms' }];
} else if (pathname.startsWith('/rooms')) {
links = [{ href: '/player/1', label: 'Profile' }];
links = [{ href: '/player/profile', label: 'Profile' }];
} else if (pathname.startsWith('/game/')) {
links = [{ href: '/rooms', label: 'Leave Room' }];
}
Expand Down
Loading
Loading