Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
94f5a6b
feat: add placeholder for existing students invitation
Salvatore112 May 2, 2025
afdfa21
feat: implement invites for existent students
Salvatore112 May 4, 2025
e58e80a
style: use more consistent ui
Salvatore112 May 4, 2025
775761f
fix: fix crash when student is not found
Salvatore112 May 4, 2025
05b72de
feat: add not found students handling
Salvatore112 May 4, 2025
9456b08
Merge branch 'master' into invitedStudents
Salvatore112 May 4, 2025
eba6bf2
feat: add single method for sign up and accepting student
Salvatore112 May 6, 2025
a1b4146
chore: add newline
Salvatore112 May 6, 2025
b6576e2
refactor: get rid of extra whitespaces
Salvatore112 May 6, 2025
68ec18e
feat: add a separate component for email
Salvatore112 May 6, 2025
3bd85a0
feat: use the same method for inviting students
Salvatore112 May 6, 2025
1868e07
feat: add new students invitation
Salvatore112 May 10, 2025
2b36f7c
feat: remove extra search from fields
Salvatore112 May 10, 2025
f793bbe
chore: revert swagger changes
Salvatore112 May 13, 2025
6586d98
chore: revert changes in ts files
Salvatore112 May 13, 2025
2fc97c9
chore: replace ts files
Salvatore112 May 13, 2025
b36e912
feat: add autocomplete for students
Salvatore112 May 13, 2025
6cfa0b3
feat: make register button visible
Salvatore112 May 13, 2025
4cb7dc2
style: update ui for students invitation
Salvatore112 May 13, 2025
0b4d4af
fix: fix students autocomplete
Salvatore112 May 14, 2025
7e766de
feat: remove middle name from autocomplete
Salvatore112 May 14, 2025
5ab53e0
feat: add token creating and logging in methods
Salvatore112 May 14, 2025
aefb4f7
format: add missing whitespaces
Salvatore112 May 14, 2025
6947011
feat: adapt authlayout for more roles
Salvatore112 May 14, 2025
a629b16
feat: add token-based logging in for invited students
Salvatore112 May 16, 2025
61c7607
style: remove extra comments
Salvatore112 May 16, 2025
db0f7dc
feat: add expiration date to student token
Salvatore112 May 16, 2025
a0badd4
refactor: remove extra spaces in api.tsx
Salvatore112 May 16, 2025
acc5d60
refactor: revert style changes
Salvatore112 May 16, 2025
2e878a1
feat: make email carry over in between windows
Salvatore112 May 16, 2025
8cbc20c
refactor: merge branches in InviteStudent
Salvatore112 May 28, 2025
541709c
chore: translate error's text
Salvatore112 May 28, 2025
f1bb8e4
style: replace expertEmail to email
Salvatore112 May 28, 2025
7f68677
refactor: refactor email name in token method
Salvatore112 May 29, 2025
ea54cb2
feat: remove extra role check
Salvatore112 May 29, 2025
3f2d9f8
fix: bring back removed code
Salvatore112 May 29, 2025
cdcfbe9
refactor: remove empty line
Salvatore112 May 29, 2025
ad63659
feat: split course.tsx's functionality into multiple files
Salvatore112 May 29, 2025
459d954
refactor: use ID instead of email
Salvatore112 May 29, 2025
a4ed92e
feat: remove extra request to AuthServiceClient
Salvatore112 Jun 6, 2025
4110a53
feat: make sending notification optional
Salvatore112 Jun 6, 2025
fd527d2
feat: move student related code to account controller in api gateway
Salvatore112 Jun 6, 2025
892ae52
feat: move code for creating student tokens to other account controller
Salvatore112 Jun 6, 2025
681086e
feat: add token validation on backend
Salvatore112 Jun 8, 2025
73f8c83
feat: add new AuthLayout.tsx with token validation
Salvatore112 Jun 8, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,30 @@ public async Task<IActionResult> AuthorizeGithub(

return Ok(result);
}

[HttpGet("getStudentToken")]
[Authorize(Roles = Roles.LecturerRole)]
[ProducesResponseType(typeof(Result<TokenCredentials>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetStudentToken(string email)
{
var tokenMeta = await AuthServiceClient.GetStudentToken(email);
return Ok(tokenMeta);
}

[HttpPost("loginWithToken")]
[ProducesResponseType(typeof(Result), (int)HttpStatusCode.OK)]
public async Task<IActionResult> LoginWithToken([FromBody] TokenCredentials credentials)
{
var result = await AuthServiceClient.LoginWithToken(credentials);
return Ok(result);
}

[HttpPost("validateToken")]
[ProducesResponseType(typeof(Result<TokenValidationResult>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ValidateToken([FromBody] TokenCredentials tokenCredentials)
{
var result = await AuthServiceClient.ValidateToken(tokenCredentials);
return Ok(result);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -306,5 +306,46 @@ private async Task<CourseViewModel> ToCourseViewModel(CourseDTO course)
IsOpen = course.IsOpen,
};
}

[HttpPost("inviteExistentStudent")]
[Authorize(Roles = Roles.LecturerRole)]
public async Task<IActionResult> InviteStudent([FromBody] InviteStudentViewModel model)
{
var student = await AuthServiceClient.FindByEmailAsync(model.Email);

if (student == null)
{
if (model.Name == null)
{
return BadRequest(new { error = "Пользователь с указанным email не найден" });
}

var registerModel = new RegisterViewModel
{
Email = model.Email,
Name = model.Name,
Surname = model.Surname,
MiddleName = model.MiddleName
};

var registrationResult = await AuthServiceClient.RegisterInvitedStudent(registerModel);

if (!registrationResult.Succeeded)
{
return BadRequest(new { error = "Не удалось зарегистрировать студента", details = registrationResult.Errors });
}

student = registrationResult.Value;
}

var invitationResult = await _coursesClient.SignInAndAcceptStudent(model.CourseId, student);

if (!invitationResult.Succeeded)
{
return BadRequest(new { error = invitationResult.Errors });
}

return Ok(new { message = "Студент успешно приглашен на курс" });
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,23 @@ namespace HwProj.AuthService.API.Controllers
public class AccountController : ControllerBase
{
private readonly IAccountService _accountService;
private readonly IExpertsService _expertsService;
private readonly IAuthTokenService _tokenService;
private readonly IUserManager _userManager;
private readonly IConfiguration _configuration;
private readonly IMapper _mapper;

public AccountController(
IAccountService accountService,
IUserManager userManager,
IExpertsService expertsService,
IAuthTokenService authTokenService,
IMapper mapper)
{
_accountService = accountService;
_userManager = userManager;
_expertsService = expertsService;
_tokenService = authTokenService;
_mapper = mapper;
}

Expand Down Expand Up @@ -189,5 +195,44 @@ public async Task<GithubCredentials> GithubAuthorize(
var result = await _accountService.AuthorizeGithub(code, userId);
return result;
}

[HttpPost("registerInvitedStudent")]
[ProducesResponseType(typeof(Result<string>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> RegisterInvitedStudent([FromBody] RegisterViewModel model)
{
var newModel = _mapper.Map<RegisterDataDTO>(model);
var result = await _accountService.RegisterInvitedStudentAsync(newModel);
return Ok(result);
}

[HttpGet("getStudentToken")]
[ProducesResponseType(typeof(Result<TokenCredentials>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetStudentToken(string email)
{
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return Ok(Result<TokenCredentials>.Failed("Пользователь не найден"));
}

var token = await _tokenService.GetTokenAsync(user);
return Ok(Result<TokenCredentials>.Success(token));
}

[HttpPost("loginWithToken")]
[ProducesResponseType(typeof(Result), (int)HttpStatusCode.OK)]
public async Task<IActionResult> LoginWithToken([FromBody] TokenCredentials credentials)
{
var result = await _accountService.LoginWithTokenAsync(credentials);
return Ok(result);
}

[HttpPost("validateToken")]
[ProducesResponseType(typeof(Result<TokenValidationResult>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ValidateToken([FromBody] TokenCredentials tokenCredentials)
{
var result = await _accountService.ValidateTokenAsync(tokenCredentials);
return Ok(result);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace HwProj.AuthService.API.Events
{
public class RegisterInvitedStudentEvent : RegisterEvent
{
public RegisterInvitedStudentEvent(string userId, string email, string name, string surname = "", string middleName = "")
: base(userId, email, name, surname, middleName)
{
}
public string AuthToken { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public class AccountService : IAccountService
private readonly IMapper _mapper;
private readonly IConfiguration _configuration;
private readonly HttpClient _client;

public AccountService(IUserManager userManager,
SignInManager<User> signInManager,
IAuthTokenService authTokenService,
Expand Down Expand Up @@ -374,5 +373,87 @@ private async Task<Result<TokenCredentials>> GetToken(User user)
{
return Result<TokenCredentials>.Success(await _tokenService.GetTokenAsync(user).ConfigureAwait(false));
}
private async Task<Result<string>> RegisterInvitedStudentInternal(RegisterDataDTO model)
{
var user = _mapper.Map<User>(model);
user.UserName = user.Email;

var createUserTask = model.IsExternalAuth
? _userManager.CreateAsync(user)
: _userManager.CreateAsync(user, Guid.NewGuid().ToString());

var result = await createUserTask
.Then(() => _userManager.AddToRoleAsync(user, Roles.StudentRole));

if (result.Succeeded)
{
var newUser = await _userManager.FindByEmailAsync(model.Email);
var expirationDate = DateTime.UtcNow.AddMonths(1);
var token = await _tokenService.GetTokenAsync(newUser, expirationDate);
var registerEvent = new RegisterInvitedStudentEvent(newUser.Id, newUser.Email, newUser.Name,
newUser.Surname, newUser.MiddleName)
{
AuthToken = token.AccessToken
};
_eventBus.Publish(registerEvent);
return Result<string>.Success(newUser.Id);
}

return Result<string>.Failed(result.Errors.Select(errors => errors.Description).ToArray());
}

public async Task<Result<string>> RegisterInvitedStudentAsync(RegisterDataDTO model)
{
if (await _userManager.FindByEmailAsync(model.Email) != null)
return Result<string>.Failed("Пользователь уже зарегистрирован");

return await RegisterInvitedStudentInternal(model);
}

public async Task<Result> LoginWithTokenAsync(TokenCredentials tokenCredentials)
{
var tokenClaims = _tokenService.GetTokenClaims(tokenCredentials);

if (string.IsNullOrEmpty(tokenClaims.Id))
{
return Result.Failed("Невалидный токен: id не найден");
}

var user = await _userManager.FindByEmailAsync(tokenClaims.Email);

if (user == null || user.Id != tokenClaims.Id)
{
return Result.Failed("Невалидный токен: пользователь не найден");
}

await _signInManager.SignInAsync(user, isPersistent: false);
return Result.Success();
}

public async Task<Result<TokenValidationResult>> ValidateTokenAsync(TokenCredentials tokenCredentials)
{
var tokenClaims = _tokenService.GetTokenClaims(tokenCredentials);

if (string.IsNullOrEmpty(tokenClaims.Id))
{
return Result<TokenValidationResult>.Failed("Невалидный токен: id не найден");
}

var user = await _userManager.FindByIdAsync(tokenClaims.Id);
if (user == null)
{
return Result<TokenValidationResult>.Failed("Невалидный токен: пользователь не найден");
}

var roles = await _userManager.GetRolesAsync(user);
var role = roles.FirstOrDefault() ?? Roles.StudentRole;

return Result<TokenValidationResult>.Success(new TokenValidationResult
{
IsValid = true,
Role = role,
UserId = user.Id
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,18 @@ public AuthTokenService(UserManager<User> userManager, IConfiguration configurat
_tokenHandler = new JwtSecurityTokenHandler();
}

public async Task<TokenCredentials> GetTokenAsync(User user)
public async Task<TokenCredentials> GetTokenAsync(User user, DateTime? expirationDate = null)
{
var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration["SecurityKey"]));
var timeNow = DateTime.UtcNow;

var userRoles = await _userManager.GetRolesAsync(user).ConfigureAwait(false);

var expiresIn = userRoles.FirstOrDefault() == Roles.ExpertRole
? GetExpertTokenExpiresIn(timeNow)
: timeNow.AddMinutes(int.Parse(_configuration["ExpiresIn"]));

var expiresIn = expirationDate ??
(userRoles.FirstOrDefault() == Roles.ExpertRole
? GetExpertTokenExpiresIn(timeNow)
: timeNow.AddMinutes(int.Parse(_configuration["ExpiresIn"])));

var token = new JwtSecurityToken(
issuer: _configuration["ApiName"],
notBefore: timeNow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,8 @@ public interface IAccountService
Task<Result> ResetPassword(ResetPasswordViewModel model);
Task<GithubCredentials> AuthorizeGithub(string code, string userId);
Task<Result<string>[]> GetOrRegisterStudentsBatchAsync(IEnumerable<RegisterDataDTO> models);
Task<Result<string>> RegisterInvitedStudentAsync(RegisterDataDTO model);
Task<Result> LoginWithTokenAsync(TokenCredentials tokenCredentials);
Task<Result<TokenValidationResult>> ValidateTokenAsync(TokenCredentials tokenCredentials);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
using HwProj.Models.AuthService.DTO;
using HwProj.Models.AuthService.ViewModels;
using System;
using System.Threading.Tasks;
using HwProj.Models.Result;

namespace HwProj.AuthService.API.Services
{
public interface IAuthTokenService
{
Task<TokenCredentials> GetTokenAsync(User user);
Task<TokenCredentials> GetTokenAsync(User user, DateTime? expirationDate = null);
Task<Result<TokenCredentials>> GetExpertTokenAsync(User expert);
TokenClaims GetTokenClaims(TokenCredentials tokenCredentials);
}
}
}
58 changes: 58 additions & 0 deletions HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -353,5 +353,63 @@ public async Task<Result> UpdateExpertTags(string lecturerId, UpdateExpertTagsDT
var response = await _httpClient.SendAsync(httpRequest);
return await response.DeserializeAsync<Result>();
}

public async Task<Result<TokenCredentials>> GetStudentToken(string email)
{
using var httpRequest = new HttpRequestMessage(
HttpMethod.Get,
_authServiceUri + $"api/account/getStudentToken?email={email}");

var response = await _httpClient.SendAsync(httpRequest);
return await response.DeserializeAsync<Result<TokenCredentials>>();
}

public async Task<Result> LoginWithToken(TokenCredentials credentials)
{
using var httpRequest = new HttpRequestMessage(
HttpMethod.Post,
_authServiceUri + "api/account/loginWithToken")
{
Content = new StringContent(
JsonConvert.SerializeObject(credentials),
Encoding.UTF8,
"application/json")
};

var response = await _httpClient.SendAsync(httpRequest);
return await response.DeserializeAsync<Result>();
}

public async Task<Result<string>> RegisterInvitedStudent(RegisterViewModel model)
{
using var httpRequest = new HttpRequestMessage(
HttpMethod.Post,
_authServiceUri + "api/account/registerInvitedStudent")
{
Content = new StringContent(
JsonConvert.SerializeObject(model),
Encoding.UTF8,
"application/json")
};

var response = await _httpClient.SendAsync(httpRequest);
return await response.DeserializeAsync<Result<string>>();
}

public async Task<Result<TokenValidationResult>> ValidateToken(TokenCredentials credentials)
{
using var httpRequest = new HttpRequestMessage(
HttpMethod.Post,
_authServiceUri + "api/account/validateToken")
{
Content = new StringContent(
JsonConvert.SerializeObject(credentials),
Encoding.UTF8,
"application/json")
};

var response = await _httpClient.SendAsync(httpRequest);
return await response.DeserializeAsync<Result<TokenValidationResult>>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,9 @@ public interface IAuthServiceClient
Task<ExpertDataDTO[]> GetAllExperts();
Task<Result> UpdateExpertTags(string lecturerId, UpdateExpertTagsDTO updateExpertTagsDto);
Task<Result<string>[]> GetOrRegisterStudentsBatchAsync(IEnumerable<RegisterViewModel> registrationModels);
Task<Result<TokenCredentials>> GetStudentToken(string email);
Task<Result> LoginWithToken(TokenCredentials credentials);
Task<Result<string>> RegisterInvitedStudent(RegisterViewModel model);
Task<Result<TokenValidationResult>> ValidateToken(TokenCredentials credentials);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace HwProj.Models.AuthService.DTO
{
public class TokenValidationResult
{
public bool IsValid { get; set; }
public string Role { get; set; }
public string UserId { get; set; }
}
}
Loading