Skip to content

Commit

Permalink
Merge pull request #42 from theImmortalCoders/issue-39
Browse files Browse the repository at this point in the history
Issue 39
  • Loading branch information
marcinbator authored Nov 3, 2024
2 parents 02fa986 + c0adf4b commit 2d4d2d5
Show file tree
Hide file tree
Showing 23 changed files with 517 additions and 88 deletions.
26 changes: 13 additions & 13 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@ name: .NET

on:
push:
branches: ['dev', 'main']
branches: [ 'dev', 'main' ]
pull_request:
branches: ['main']
branches: [ 'main' ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore rag-2-backend
- name: Build
run: dotnet build --no-restore rag-2-backend
- name: Test
run: dotnet test --no-build --verbosity normal rag-2-backend
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore rag-2-backend
- name: Build
run: dotnet build --no-restore rag-2-backend
- name: Test
run: dotnet test --no-build --verbosity normal rag-2-backend
1 change: 1 addition & 0 deletions rag-2-backend/Config/ServiceRegistrationExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ private static void ConfigServices(IServiceCollection services)
services.AddScoped<JwtSecurityTokenHandler>();
services.AddScoped<AdministrationService>();
services.AddScoped<UserDao>();
services.AddScoped<RefreshTokenDao>();

services.AddSingleton<StatsService>();
}
Expand Down
1 change: 0 additions & 1 deletion rag-2-backend/Infrastructure/Common/Model/Role.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@ public enum Role
{
Student,
Teacher,
Special,
Admin
}
18 changes: 18 additions & 0 deletions rag-2-backend/Infrastructure/Dao/RefreshTokenDao.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#region

using rag_2_backend.Infrastructure.Database;
using rag_2_backend.Infrastructure.Database.Entity;

#endregion

namespace rag_2_backend.Infrastructure.Dao;

public class RefreshTokenDao(DatabaseContext context)
{
public void RemoveTokensForUser(User user)
{
var unusedTokens = context.RefreshTokens.Where(r => r.User.Id == user.Id).ToList();
context.RefreshTokens.RemoveRange(unusedTokens);
context.SaveChanges();
}
}
2 changes: 1 addition & 1 deletion rag-2-backend/Infrastructure/Database/DatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbCont
public virtual required DbSet<GameRecord> RecordedGames { get; init; }
public virtual required DbSet<User> Users { get; init; }
public virtual required DbSet<AccountConfirmationToken> AccountConfirmationTokens { get; init; }
public virtual required DbSet<BlacklistedJwt> BlacklistedJwts { get; init; }
public virtual required DbSet<RefreshToken> RefreshTokens { get; init; }
public virtual required DbSet<PasswordResetToken> PasswordResetTokens { get; init; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
Expand Down
15 changes: 0 additions & 15 deletions rag-2-backend/Infrastructure/Database/Entity/BlacklistedJwt.cs

This file was deleted.

16 changes: 16 additions & 0 deletions rag-2-backend/Infrastructure/Database/Entity/RefreshToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#region

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

#endregion

namespace rag_2_backend.Infrastructure.Database.Entity;

[Table("refresh_token_table")]
public class RefreshToken
{
[Key] [MaxLength(100)] public required string Token { get; init; }
public required DateTime Expiration { get; set; }
public required User User { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.AspNetCore.Mvc;
using rag_2_backend.Infrastructure.Common.Model;
using rag_2_backend.Infrastructure.Dao;
using rag_2_backend.Infrastructure.Module.Administration.Dto;
using rag_2_backend.Infrastructure.Module.User.Dto;

#endregion
Expand All @@ -13,7 +14,9 @@ namespace rag_2_backend.Infrastructure.Module.Administration;

[ApiController]
[Route("api/[controller]")]
public class AdministrationController(AdministrationService administrationService) : ControllerBase
public class AdministrationController(
AdministrationService administrationService,
IConfiguration config) : ControllerBase
{
/// <summary>Change ban status for any user by user ID despite admins (Admin)</summary>
/// <response code="404">User not found</response>
Expand Down Expand Up @@ -44,6 +47,19 @@ public UserResponse GetUserDetails([Required] int userId)
return administrationService.GetUserDetails(UserDao.GetPrincipalEmail(User), userId);
}

/// <summary>Get current limits for roles (Auth)</summary>
/// <response code="403">Cannot view limits</response>
[HttpGet("limits")]
[Authorize]
public LimitsResponse GetCurrentLimits()
{
return new LimitsResponse
{
StudentLimitMb = int.Parse(config["StudentDataLimitMb"] ?? "30"),
TeacherLimitMb = int.Parse(config["TeacherDataLimitMb"] ?? "30")
};
}

/// <summary>Get all users list (Admin, Teacher)</summary>
[HttpGet("users")]
[Authorize(Roles = "Admin, Teacher")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public UserResponse GetUserDetails(string principalEmail, int userId)
{
var principal = userDao.GetUserByEmailOrThrow(principalEmail);

if (principal.Role is Role.Student or Role.Special && userId != principal.Id)
if (principal.Role is Role.Student && userId != principal.Id)
throw new ForbiddenException("Cannot view details");

return UserMapper.Map(userDao.GetUserByIdOrThrow(userId));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace rag_2_backend.Infrastructure.Module.Administration.Dto;

public class LimitsResponse
{
public required int StudentLimitMb { get; set; }
public required int TeacherLimitMb { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
while (!cancellationToken.IsCancellationRequested)
{
DeleteUnusedAccountTokens();
DeleteUnusedBlacklistedJwts();
DeleteUnusedRefreshTokens();
DeleteUnusedPasswordResetTokens();

await Task.Delay(TimeSpan.FromDays(1), cancellationToken);
Expand All @@ -45,10 +45,10 @@ private void DeleteUnusedAccountTokens()
Console.WriteLine("Deleted " + unconfirmedUsers.Count + " unconfirmed accounts");
}

private void DeleteUnusedBlacklistedJwts()
private void DeleteUnusedRefreshTokens()
{
var unusedTokens = _dbContext.BlacklistedJwts.Where(b => b.Expiration < DateTime.Now).ToList();
_dbContext.BlacklistedJwts.RemoveRange(unusedTokens);
var unusedTokens = _dbContext.RefreshTokens.Where(b => b.Expiration < DateTime.Now).ToList();
_dbContext.RefreshTokens.RemoveRange(unusedTokens);
_dbContext.SaveChanges();

Console.WriteLine("Deleted " + unusedTokens.Count + " blacklisted jwts");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,16 @@ public void AddGameRecord(RecordedGameRequest request, string email)
throw new BadRequestException("Value state cannot be empty");

var user = userDao.GetUserByEmailOrThrow(email);
if (GetSizeByUser(user.Id, request.Values.Count) > configuration.GetValue<int>("UserDataLimitMb"))
throw new BadRequestException("Space limit exceeded");

switch (user.Role)
{
case Role.Student when GetSizeByUser(user.Id, request.Values.Count) >
configuration.GetValue<int>("StudentDataLimitMb"):
throw new BadRequestException("Space limit exceeded");
case Role.Teacher when GetSizeByUser(user.Id, request.Values.Count) >
configuration.GetValue<int>("TeacherDataLimitMb"):
throw new BadRequestException("Space limit exceeded");
}

var game = context.Games.SingleOrDefault(g => Equals(g.Name.ToLower(), request.GameName.ToLower()))
?? throw new NotFoundException("Game not found");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace rag_2_backend.Infrastructure.Module.User.Dto;

public class UserLoginResponse
{
public required string JwtToken { get; set; }
public required string RefreshToken { get; set; }
}
38 changes: 32 additions & 6 deletions rag-2-backend/Infrastructure/Module/User/UserController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#region

using System.ComponentModel.DataAnnotations;
using HttpExceptions.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using rag_2_backend.Infrastructure.Dao;
Expand All @@ -12,7 +13,7 @@ namespace rag_2_backend.Infrastructure.Module.User;

[ApiController]
[Route("api/[controller]/auth")]
public class UserController(UserService userService) : ControllerBase
public class UserController(UserService userService, IConfiguration config) : ControllerBase
{
/// <summary>Register new user</summary>
/// <response code="400">User already exists or wrong study cycle year</response>
Expand All @@ -27,7 +28,23 @@ public void Register([FromBody] [Required] UserRequest userRequest)
[HttpPost("login")]
public string Login([FromBody] [Required] UserLoginRequest loginRequest)
{
return userService.LoginUser(loginRequest.Email, loginRequest.Password);
var response = userService.LoginUser(
loginRequest.Email,
loginRequest.Password,
double.Parse(config["RefreshToken:ExpireDays"] ?? "30")
);

HttpContext.Response.Cookies.Append("refreshToken", response.RefreshToken,
new CookieOptions
{
Expires = DateTimeOffset.UtcNow.AddDays(
double.Parse(config["RefreshToken:ExpireDays"] ?? "30")),
HttpOnly = true
// IsEssential = true,
// Secure = true,
// SameSite = SameSiteMode.None
});
return response.JwtToken;
}

/// <summary>Resend confirmation email to specified email</summary>
Expand Down Expand Up @@ -62,15 +79,24 @@ public void ResetPassword([Required] string tokenValue, [Required] string newPas
userService.ResetPassword(tokenValue, newPassword);
}

/// <summary>Refresh token</summary>
/// <response code="401">Invalid refresh token</response>
[HttpPost("refresh-token")]
public string RefreshToken()
{
HttpContext.Request.Cookies.TryGetValue("refreshToken", out var refreshToken);
if (refreshToken == null)
throw new UnauthorizedException("Invalid refresh token");

return userService.RefreshToken(refreshToken);
}

/// <summary>Logout current user (Auth)</summary>
[HttpPost("logout")]
[Authorize]
public void Logout()
{
var header = HttpContext.Request.Headers.Authorization.FirstOrDefault() ??
throw new UnauthorizedAccessException("Unauthorized");

userService.LogoutUser(header);
userService.LogoutUser(UserDao.GetPrincipalEmail(User));
}

/// <summary>Get current user details (Auth)</summary>
Expand Down
42 changes: 31 additions & 11 deletions rag-2-backend/Infrastructure/Module/User/UserService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#region

using System.IdentityModel.Tokens.Jwt;
using HttpExceptions.Exceptions;
using Microsoft.EntityFrameworkCore;
using rag_2_backend.Infrastructure.Common.Mapper;
Expand All @@ -20,8 +19,8 @@ public class UserService(
DatabaseContext context,
JwtUtil jwtUtil,
EmailService emailService,
JwtSecurityTokenHandler jwtSecurityTokenHandler,
UserDao userDao)
UserDao userDao,
RefreshTokenDao refreshTokenDao)
{
public void RegisterUser(UserRequest userRequest)
{
Expand Down Expand Up @@ -71,7 +70,7 @@ public void ConfirmAccount(string tokenValue)
context.SaveChanges();
}

public string LoginUser(string email, string password)
public UserLoginResponse LoginUser(string email, string password, double refreshTokenExpirationTimeDays)
{
var user = userDao.GetUserByEmailOrThrow(email);

Expand All @@ -82,6 +81,31 @@ public string LoginUser(string email, string password)
if (user.Banned)
throw new UnauthorizedException("User banned");

var refreshToken = new RefreshToken
{
User = user,
Expiration = DateTime.Now.AddDays(refreshTokenExpirationTimeDays),
Token = Guid.NewGuid().ToString()
};
refreshTokenDao.RemoveTokensForUser(user);
context.RefreshTokens.Add(refreshToken);
context.SaveChanges();

return new UserLoginResponse
{
JwtToken = jwtUtil.GenerateToken(user.Email, user.Role.ToString()),
RefreshToken = refreshToken.Token
};
}

public string RefreshToken(string refreshToken)
{
var token = context.RefreshTokens
.Include(t => t.User)
.SingleOrDefault(t => t.Token == refreshToken && t.Expiration > DateTime.Now)
?? throw new UnauthorizedException("Invalid refresh token");
var user = token.User;

return jwtUtil.GenerateToken(user.Email, user.Role.ToString());
}

Expand All @@ -90,14 +114,10 @@ public UserResponse GetMe(string email)
return UserMapper.Map(userDao.GetUserByEmailOrThrow(email));
}

public void LogoutUser(string header)
public void LogoutUser(string email)
{
var tokenValue = header["Bearer ".Length..].Trim();
var jwtToken = jwtSecurityTokenHandler.ReadToken(tokenValue) as JwtSecurityToken ??
throw new UnauthorizedException("Unauthorized");

context.BlacklistedJwts.Add(new BlacklistedJwt { Token = tokenValue, Expiration = jwtToken.ValidTo });
context.SaveChanges();
var user = userDao.GetUserByEmailOrThrow(email);
refreshTokenDao.RemoveTokensForUser(user);
}

public void RequestPasswordReset(string email)
Expand Down
6 changes: 0 additions & 6 deletions rag-2-backend/Infrastructure/Util/JwtUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using rag_2_backend.Infrastructure.Database;

Expand Down Expand Up @@ -36,9 +35,4 @@ public virtual string GenerateToken(string email, string role)

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

public async Task<bool> IsTokenBlacklistedAsync(string token)
{
return await context.BlacklistedJwts.AnyAsync(bt => bt.Token == token);
}
}
Loading

0 comments on commit 2d4d2d5

Please sign in to comment.