diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/AccountController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/AccountController.cs index ddc60b9ce..0b0c9df04 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/AccountController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/AccountController.cs @@ -180,5 +180,30 @@ public async Task AuthorizeGithub( return Ok(result); } + + [HttpGet("getStudentToken")] + [Authorize(Roles = Roles.LecturerRole)] + [ProducesResponseType(typeof(Result), (int)HttpStatusCode.OK)] + public async Task GetStudentToken(string email) + { + var tokenMeta = await AuthServiceClient.GetStudentToken(email); + return Ok(tokenMeta); + } + + [HttpPost("loginWithToken")] + [ProducesResponseType(typeof(Result), (int)HttpStatusCode.OK)] + public async Task LoginWithToken([FromBody] TokenCredentials credentials) + { + var result = await AuthServiceClient.LoginWithToken(credentials); + return Ok(result); + } + + [HttpPost("validateToken")] + [ProducesResponseType(typeof(Result), (int)HttpStatusCode.OK)] + public async Task ValidateToken([FromBody] TokenCredentials tokenCredentials) + { + var result = await AuthServiceClient.ValidateToken(tokenCredentials); + return Ok(result); + } } } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs index 2bc911895..7b9fad39b 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs @@ -306,5 +306,46 @@ private async Task ToCourseViewModel(CourseDTO course) IsOpen = course.IsOpen, }; } + + [HttpPost("inviteExistentStudent")] + [Authorize(Roles = Roles.LecturerRole)] + public async Task 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 = "Студент успешно приглашен на курс" }); + } } } diff --git a/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs b/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs index c4d5aba1d..d4b59dc07 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs @@ -20,6 +20,8 @@ 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; @@ -27,10 +29,14 @@ public class AccountController : ControllerBase public AccountController( IAccountService accountService, IUserManager userManager, + IExpertsService expertsService, + IAuthTokenService authTokenService, IMapper mapper) { _accountService = accountService; _userManager = userManager; + _expertsService = expertsService; + _tokenService = authTokenService; _mapper = mapper; } @@ -189,5 +195,44 @@ public async Task GithubAuthorize( var result = await _accountService.AuthorizeGithub(code, userId); return result; } + + [HttpPost("registerInvitedStudent")] + [ProducesResponseType(typeof(Result), (int)HttpStatusCode.OK)] + public async Task RegisterInvitedStudent([FromBody] RegisterViewModel model) + { + var newModel = _mapper.Map(model); + var result = await _accountService.RegisterInvitedStudentAsync(newModel); + return Ok(result); + } + + [HttpGet("getStudentToken")] + [ProducesResponseType(typeof(Result), (int)HttpStatusCode.OK)] + public async Task GetStudentToken(string email) + { + var user = await _userManager.FindByEmailAsync(email); + if (user == null) + { + return Ok(Result.Failed("Пользователь не найден")); + } + + var token = await _tokenService.GetTokenAsync(user); + return Ok(Result.Success(token)); + } + + [HttpPost("loginWithToken")] + [ProducesResponseType(typeof(Result), (int)HttpStatusCode.OK)] + public async Task LoginWithToken([FromBody] TokenCredentials credentials) + { + var result = await _accountService.LoginWithTokenAsync(credentials); + return Ok(result); + } + + [HttpPost("validateToken")] + [ProducesResponseType(typeof(Result), (int)HttpStatusCode.OK)] + public async Task ValidateToken([FromBody] TokenCredentials tokenCredentials) + { + var result = await _accountService.ValidateTokenAsync(tokenCredentials); + return Ok(result); + } } -} +} \ No newline at end of file diff --git a/HwProj.AuthService/HwProj.AuthService.API/Events/RegisterInvitedStudentEvent.cs b/HwProj.AuthService/HwProj.AuthService.API/Events/RegisterInvitedStudentEvent.cs new file mode 100644 index 000000000..e456460ef --- /dev/null +++ b/HwProj.AuthService/HwProj.AuthService.API/Events/RegisterInvitedStudentEvent.cs @@ -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; } + } +} \ No newline at end of file diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs index 945e48e58..3db93faea 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs @@ -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 signInManager, IAuthTokenService authTokenService, @@ -374,5 +373,87 @@ private async Task> GetToken(User user) { return Result.Success(await _tokenService.GetTokenAsync(user).ConfigureAwait(false)); } + private async Task> RegisterInvitedStudentInternal(RegisterDataDTO model) + { + var user = _mapper.Map(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.Success(newUser.Id); + } + + return Result.Failed(result.Errors.Select(errors => errors.Description).ToArray()); + } + + public async Task> RegisterInvitedStudentAsync(RegisterDataDTO model) + { + if (await _userManager.FindByEmailAsync(model.Email) != null) + return Result.Failed("Пользователь уже зарегистрирован"); + + return await RegisterInvitedStudentInternal(model); + } + + public async Task 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> ValidateTokenAsync(TokenCredentials tokenCredentials) + { + var tokenClaims = _tokenService.GetTokenClaims(tokenCredentials); + + if (string.IsNullOrEmpty(tokenClaims.Id)) + { + return Result.Failed("Невалидный токен: id не найден"); + } + + var user = await _userManager.FindByIdAsync(tokenClaims.Id); + if (user == null) + { + return Result.Failed("Невалидный токен: пользователь не найден"); + } + + var roles = await _userManager.GetRolesAsync(user); + var role = roles.FirstOrDefault() ?? Roles.StudentRole; + + return Result.Success(new TokenValidationResult + { + IsValid = true, + Role = role, + UserId = user.Id + }); + } } } diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs index 3c5ea76a5..635538549 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs @@ -30,17 +30,18 @@ public AuthTokenService(UserManager userManager, IConfiguration configurat _tokenHandler = new JwtSecurityTokenHandler(); } - public async Task GetTokenAsync(User user) + public async Task 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, diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs index 463161087..2e3cc8626 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs @@ -22,5 +22,8 @@ public interface IAccountService Task ResetPassword(ResetPasswordViewModel model); Task AuthorizeGithub(string code, string userId); Task[]> GetOrRegisterStudentsBatchAsync(IEnumerable models); + Task> RegisterInvitedStudentAsync(RegisterDataDTO model); + Task LoginWithTokenAsync(TokenCredentials tokenCredentials); + Task> ValidateTokenAsync(TokenCredentials tokenCredentials); } } diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs index 18ed24100..eac045871 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs @@ -1,5 +1,6 @@ using HwProj.Models.AuthService.DTO; using HwProj.Models.AuthService.ViewModels; +using System; using System.Threading.Tasks; using HwProj.Models.Result; @@ -7,8 +8,8 @@ namespace HwProj.AuthService.API.Services { public interface IAuthTokenService { - Task GetTokenAsync(User user); + Task GetTokenAsync(User user, DateTime? expirationDate = null); Task> GetExpertTokenAsync(User expert); TokenClaims GetTokenClaims(TokenCredentials tokenCredentials); } -} +} \ No newline at end of file diff --git a/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs b/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs index 24652becb..ae68c810f 100644 --- a/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs +++ b/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs @@ -353,5 +353,63 @@ public async Task UpdateExpertTags(string lecturerId, UpdateExpertTagsDT var response = await _httpClient.SendAsync(httpRequest); return await response.DeserializeAsync(); } + + public async Task> 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>(); + } + + public async Task 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(); + } + + public async Task> 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>(); + } + + public async Task> 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>(); + } } } diff --git a/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs b/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs index 67c020ca6..7e749dc33 100644 --- a/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs +++ b/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs @@ -32,5 +32,9 @@ public interface IAuthServiceClient Task GetAllExperts(); Task UpdateExpertTags(string lecturerId, UpdateExpertTagsDTO updateExpertTagsDto); Task[]> GetOrRegisterStudentsBatchAsync(IEnumerable registrationModels); + Task> GetStudentToken(string email); + Task LoginWithToken(TokenCredentials credentials); + Task> RegisterInvitedStudent(RegisterViewModel model); + Task> ValidateToken(TokenCredentials credentials); } } diff --git a/HwProj.Common/HwProj.Models/AuthService/DTO/TokenValidationResult.cs b/HwProj.Common/HwProj.Models/AuthService/DTO/TokenValidationResult.cs new file mode 100644 index 000000000..67e090767 --- /dev/null +++ b/HwProj.Common/HwProj.Models/AuthService/DTO/TokenValidationResult.cs @@ -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; } + } +} \ No newline at end of file diff --git a/HwProj.Common/HwProj.Models/AuthService/ViewModels/InviteStudentViewModel.cs b/HwProj.Common/HwProj.Models/AuthService/ViewModels/InviteStudentViewModel.cs new file mode 100644 index 000000000..9bc1c58ec --- /dev/null +++ b/HwProj.Common/HwProj.Models/AuthService/ViewModels/InviteStudentViewModel.cs @@ -0,0 +1,14 @@ +namespace HwProj.Models.AuthService.ViewModels +{ + public class InviteStudentViewModel + { + public long CourseId { get; set; } + public string Email { get; set; } + + public string Name { get; set; } + + public string Surname { get; set; } + + public string MiddleName { get; set; } + } +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs index 18503d91e..f5aa89d7c 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs @@ -273,5 +273,16 @@ public async Task GetMentorsToAssignedStudents(long courseId) var mentorsToAssignedStudents = await _courseFilterService.GetAssignedStudentsIds(courseId, mentorIds); return Ok(mentorsToAssignedStudents); } + + [HttpPost("signInAndAcceptStudent/{courseId}")] + [ServiceFilter(typeof(CourseMentorOnlyAttribute))] + public async Task SignInAndAcceptStudent(long courseId, [FromQuery] string studentId) + { + var signInResult = await _coursesService.AddStudentAsync(courseId, studentId, false); + if (!signInResult) return NotFound(); + + var acceptResult = await _coursesService.AcceptCourseMateAsync(courseId, studentId); + return acceptResult ? (IActionResult)Ok() : NotFound(); + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs index 7fd03cdb0..79509060a 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -168,6 +168,11 @@ public async Task UpdateAsync(long courseId, Course updated) } public async Task AddStudentAsync(long courseId, string studentId) + { + return await AddStudentAsync(courseId, studentId, true); + } + + public async Task AddStudentAsync(long courseId, string studentId, bool sendNotification) { var course = await _coursesRepository.GetAsync(courseId); var cm = await _courseMatesRepository.FindAsync(cm => cm.CourseId == courseId && cm.StudentId == studentId); @@ -183,14 +188,18 @@ public async Task AddStudentAsync(long courseId, string studentId) }; await _courseMatesRepository.AddAsync(courseMate); - _eventBus.Publish(new NewCourseMateEvent + + if (sendNotification) { - CourseId = courseId, - CourseName = course.Name, - MentorIds = course.MentorIds, - StudentId = studentId, - IsAccepted = false - }); + _eventBus.Publish(new NewCourseMateEvent + { + CourseId = courseId, + CourseName = course.Name, + MentorIds = course.MentorIds, + StudentId = studentId, + IsAccepted = false + }); + } return true; } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICoursesService.cs index 57ae4c023..08e0503f9 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICoursesService.cs @@ -16,6 +16,7 @@ public interface ICoursesService Task DeleteAsync(long id); Task UpdateAsync(long courseId, Course updated); Task AddStudentAsync(long courseId, string studentId); + Task AddStudentAsync(long courseId, string studentId, bool sendNotification); Task AcceptCourseMateAsync(long courseId, string studentId); Task RejectCourseMateAsync(long courseId, string studentId); Task GetUserCoursesAsync(string userId, string role); diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs index ff98a3fd8..8326ca391 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs @@ -646,5 +646,19 @@ public async Task Ping() return false; } } + + public async Task SignInAndAcceptStudent(long courseId, string studentId) + { + using var httpRequest = new HttpRequestMessage( + HttpMethod.Post, + _coursesServiceUri + $"api/Courses/signInAndAcceptStudent/{courseId}?studentId={studentId}"); + + httpRequest.TryAddUserId(_httpContextAccessor); + var response = await _httpClient.SendAsync(httpRequest); + + return response.IsSuccessStatusCode + ? Result.Success() + : Result.Failed(response.ReasonPhrase); + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs index 284a99709..c7df3e10d 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs @@ -54,5 +54,6 @@ Task UpdateStudentCharacteristics(long courseId, string studentId, Task AddAnswerForQuestion(AddAnswerForQuestionDto answer); Task GetMentorsToAssignedStudents(long courseId); Task Ping(); + Task SignInAndAcceptStudent(long courseId, string studentId); } } diff --git a/HwProj.NotificationsService/HwProj.NotificationsService.API/EventHandlers/RegisterInvitedStudentEventHandler.cs b/HwProj.NotificationsService/HwProj.NotificationsService.API/EventHandlers/RegisterInvitedStudentEventHandler.cs new file mode 100644 index 000000000..9ae99b826 --- /dev/null +++ b/HwProj.NotificationsService/HwProj.NotificationsService.API/EventHandlers/RegisterInvitedStudentEventHandler.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; +using System.Web; +using HwProj.AuthService.API.Events; +using HwProj.EventBus.Client.Interfaces; +using HwProj.Models.NotificationsService; +using HwProj.NotificationsService.API.Repositories; +using HwProj.NotificationsService.API.Services; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; + +namespace HwProj.NotificationsService.API.EventHandlers +{ + public class RegisterInvitedStudentEventHandler : EventHandlerBase + { + private readonly INotificationsRepository _notificationRepository; + private readonly IEmailService _emailService; + private readonly IConfiguration _configuration; + private readonly bool _isDevelopmentEnv; + + public RegisterInvitedStudentEventHandler( + INotificationsRepository notificationRepository, + IEmailService emailService, + IConfiguration configuration, + IHostingEnvironment env) + { + _notificationRepository = notificationRepository; + _emailService = emailService; + _configuration = configuration; + _isDevelopmentEnv = env.IsDevelopment(); + } + + public override async Task HandleAsync(RegisterInvitedStudentEvent @event) + { + var frontendUrl = _configuration.GetSection("Notification")["Url"]; + var inviteLink = $"{frontendUrl}/join/{HttpUtility.UrlEncode(@event.AuthToken)}"; + + var notification = new Notification + { + Sender = "AuthService", + Body = $"{@event.Name} {@event.Surname}, вас пригласили в HwProj2.

" + + $"Для доступа к аккаунту перейдите по
ссылке

" + + "Если вы не ожидали этого приглашения, проигнорируйте это письмо.", + Category = CategoryState.Profile, + Date = DateTime.UtcNow, + HasSeen = false, + Owner = @event.UserId + }; + + if (_isDevelopmentEnv) Console.WriteLine(inviteLink); + var addNotificationTask = _notificationRepository.AddAsync(notification); + var sendEmailTask = _emailService.SendEmailAsync(notification, @event.Email, "HwProj - Приглашение"); + + await Task.WhenAll(addNotificationTask, sendEmailTask); + } + } +} \ No newline at end of file diff --git a/HwProj.NotificationsService/HwProj.NotificationsService.API/Startup.cs b/HwProj.NotificationsService/HwProj.NotificationsService.API/Startup.cs index 0090ac0eb..27b265863 100644 --- a/HwProj.NotificationsService/HwProj.NotificationsService.API/Startup.cs +++ b/HwProj.NotificationsService/HwProj.NotificationsService.API/Startup.cs @@ -33,6 +33,7 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddEventBus(Configuration); + services.AddTransient, RegisterEventHandler>(); services.AddTransient, RateEventHandler>(); services.AddTransient, StudentPassTaskEventHandler>(); @@ -46,8 +47,9 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient, InviteLecturerEventHandler>(); services.AddTransient, NewCourseMateHandler>(); services.AddTransient, PasswordRecoveryEventHandler>(); + services.AddTransient, RegisterInvitedStudentEventHandler>(); + services.AddSingleton(); - services.AddHttpClient(); services.AddAuthServiceClient(); @@ -72,9 +74,10 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, IEventBu eventBustSubscriber.Subscribe(); eventBustSubscriber.Subscribe(); eventBustSubscriber.Subscribe(); + eventBustSubscriber.Subscribe(); } app.ConfigureHwProj(env, "Notifications API", context); } } -} +} \ No newline at end of file diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index f8f840ec9..73388259d 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -1,4 +1,4 @@ -/// +/// // tslint:disable /** * API Gateway @@ -497,10 +497,10 @@ export interface CreateCourseViewModel { name: string; /** * - * @type {string} + * @type {Array} * @memberof CreateCourseViewModel */ - groupName?: string; + groupNames?: Array; /** * * @type {Array} @@ -1415,6 +1415,43 @@ export interface InviteLecturerViewModel { */ email: string; } +/** + * + * @export + * @interface InviteStudentViewModel + */ +export interface InviteStudentViewModel { + /** + * + * @type {number} + * @memberof InviteStudentViewModel + */ + courseId?: number; + /** + * + * @type {string} + * @memberof InviteStudentViewModel + */ + email?: string; + /** + * + * @type {string} + * @memberof InviteStudentViewModel + */ + name?: string; + /** + * + * @type {string} + * @memberof InviteStudentViewModel + */ + surname?: string; + /** + * + * @type {string} + * @memberof InviteStudentViewModel + */ + middleName?: string; +} /** * * @export @@ -2764,6 +2801,41 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @param {string} [email] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accountGetStudentToken(email?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/Account/getStudentToken`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (email !== undefined) { + localVarQueryParameter['email'] = email; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {*} [options] Override http request option. @@ -2900,6 +2972,41 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @param {TokenCredentials} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accountLoginWithToken(body?: TokenCredentials, options: any = {}): FetchArgs { + const localVarPath = `/api/Account/loginWithToken`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + localVarHeaderParameter['Content-Type'] = 'application/json-patch+json'; + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + const needsSerialization = ("TokenCredentials" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.body = needsSerialization ? JSON.stringify(body || {}) : (body || ""); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {*} [options] Override http request option. @@ -3115,6 +3222,24 @@ export const AccountApiFp = function(configuration?: Configuration) { }); }; }, + /** + * + * @param {string} [email] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accountGetStudentToken(email?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = AccountApiFetchParamCreator(configuration).accountGetStudentToken(email, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + throw response; + } + }); + }; + }, /** * * @param {*} [options] Override http request option. @@ -3186,6 +3311,24 @@ export const AccountApiFp = function(configuration?: Configuration) { }); }; }, + /** + * + * @param {TokenCredentials} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accountLoginWithToken(body?: TokenCredentials, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = AccountApiFetchParamCreator(configuration).accountLoginWithToken(body, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + throw response; + } + }); + }; + }, /** * * @param {*} [options] Override http request option. @@ -3301,6 +3444,15 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? accountGetGithubLoginUrl(body?: UrlDto, options?: any) { return AccountApiFp(configuration).accountGetGithubLoginUrl(body, options)(fetch, basePath); }, + /** + * + * @param {string} [email] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accountGetStudentToken(email?: string, options?: any) { + return AccountApiFp(configuration).accountGetStudentToken(email, options)(fetch, basePath); + }, /** * * @param {*} [options] Override http request option. @@ -3336,6 +3488,15 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? accountLogin(body?: LoginViewModel, options?: any) { return AccountApiFp(configuration).accountLogin(body, options)(fetch, basePath); }, + /** + * + * @param {TokenCredentials} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accountLoginWithToken(body?: TokenCredentials, options?: any) { + return AccountApiFp(configuration).accountLoginWithToken(body, options)(fetch, basePath); + }, /** * * @param {*} [options] Override http request option. @@ -3424,6 +3585,17 @@ export class AccountApi extends BaseAPI { return AccountApiFp(this.configuration).accountGetGithubLoginUrl(body, options)(this.fetch, this.basePath); } + /** + * + * @param {string} [email] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountApi + */ + public accountGetStudentToken(email?: string, options?: any) { + return AccountApiFp(this.configuration).accountGetStudentToken(email, options)(this.fetch, this.basePath); + } + /** * * @param {*} [options] Override http request option. @@ -3467,6 +3639,17 @@ export class AccountApi extends BaseAPI { return AccountApiFp(this.configuration).accountLogin(body, options)(this.fetch, this.basePath); } + /** + * + * @param {TokenCredentials} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountApi + */ + public accountLoginWithToken(body?: TokenCredentials, options?: any) { + return AccountApiFp(this.configuration).accountLoginWithToken(body, options)(this.fetch, this.basePath); + } + /** * * @param {*} [options] Override http request option. @@ -4799,6 +4982,41 @@ export const CoursesApiFetchParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @param {InviteStudentViewModel} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + coursesInviteStudent(body?: InviteStudentViewModel, options: any = {}): FetchArgs { + const localVarPath = `/api/Courses/inviteExistentStudent`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + localVarHeaderParameter['Content-Type'] = 'application/json-patch+json'; + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + const needsSerialization = ("InviteStudentViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.body = needsSerialization ? JSON.stringify(body || {}) : (body || ""); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {number} courseId @@ -5228,6 +5446,24 @@ export const CoursesApiFp = function(configuration?: Configuration) { }); }; }, + /** + * + * @param {InviteStudentViewModel} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + coursesInviteStudent(body?: InviteStudentViewModel, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = CoursesApiFetchParamCreator(configuration).coursesInviteStudent(body, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } else { + throw response; + } + }); + }; + }, /** * * @param {number} courseId @@ -5441,6 +5677,15 @@ export const CoursesApiFactory = function (configuration?: Configuration, fetch? coursesGetProgramNames(options?: any) { return CoursesApiFp(configuration).coursesGetProgramNames(options)(fetch, basePath); }, + /** + * + * @param {InviteStudentViewModel} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + coursesInviteStudent(body?: InviteStudentViewModel, options?: any) { + return CoursesApiFp(configuration).coursesInviteStudent(body, options)(fetch, basePath); + }, /** * * @param {number} courseId @@ -5647,6 +5892,17 @@ export class CoursesApi extends BaseAPI { return CoursesApiFp(this.configuration).coursesGetProgramNames(options)(this.fetch, this.basePath); } + /** + * + * @param {InviteStudentViewModel} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CoursesApi + */ + public coursesInviteStudent(body?: InviteStudentViewModel, options?: any) { + return CoursesApiFp(this.configuration).coursesInviteStudent(body, options)(this.fetch, this.basePath); + } + /** * * @param {number} courseId @@ -9412,3 +9668,4 @@ export class TasksApi extends BaseAPI { } } + diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index e0f0a43af..913a631b8 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -29,8 +29,11 @@ import {QRCodeSVG} from 'qrcode.react'; import ErrorsHandler from "components/Utils/ErrorsHandler"; import {useSnackbar} from 'notistack'; import QrCode2Icon from '@mui/icons-material/QrCode2'; +import MailOutlineIcon from '@mui/icons-material/MailOutline'; import {MoreVert} from "@mui/icons-material"; import {DotLottieReact} from "@lottiefiles/dotlottie-react"; +import InviteStudentDialog from "./InviteStudentDialog"; +import {makeStyles} from "@material-ui/core/styles"; type TabValue = "homeworks" | "stats" | "applications" @@ -53,11 +56,31 @@ interface IPageState { tabValue: TabValue } +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(3), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + avatar: { + margin: theme.spacing(1), + }, + form: { + marginTop: theme.spacing(3), + width: '100%' + }, + button: { + marginTop: theme.spacing(1) + }, +})); + const Course: React.FC = () => { const {courseId, tab} = useParams() const [searchParams] = useSearchParams() const navigate = useNavigate() const {enqueueSnackbar} = useSnackbar() + const classes = useStyles() const [courseState, setCourseState] = useState({ isFound: false, @@ -71,7 +94,7 @@ const Course: React.FC = () => { }) const [studentSolutions, setStudentSolutions] = useState([]) const [courseFilesInfo, setCourseFilesInfo] = useState([]) - + const [showInviteDialog, setShowInviteDialog] = useState(false) const [pageState, setPageState] = useState({ tabValue: "homeworks" }) @@ -228,6 +251,16 @@ const Course: React.FC = () => { Поделиться + {isCourseMentor && + { + setShowInviteDialog(true) + }}> + + + + Пригласить студента + + } {isCourseMentor && isLecturer && setLecturerStatsState(true)}> @@ -257,6 +290,14 @@ const Course: React.FC = () => { + + setShowInviteDialog(false)} + onStudentInvited={setCurrentState} + /> + {course.isCompleted && @@ -421,4 +462,4 @@ const Course: React.FC = () => { } -export default Course +export default Course \ No newline at end of file diff --git a/hwproj.front/src/components/Courses/InviteStudentDialog.tsx b/hwproj.front/src/components/Courses/InviteStudentDialog.tsx new file mode 100644 index 000000000..250c0df4a --- /dev/null +++ b/hwproj.front/src/components/Courses/InviteStudentDialog.tsx @@ -0,0 +1,264 @@ +import * as React from "react"; +import {FC, useEffect, useState} from "react"; +import { + Dialog, + DialogContent, + DialogTitle, + Grid, + Typography, + TextField, + Button, + Box, + Avatar, + Autocomplete +} from "@mui/material"; +import MailOutlineIcon from '@mui/icons-material/MailOutline'; +import ApiSingleton from "../../api/ApiSingleton"; +import {AccountDataDto} from "@/api"; +import ErrorsHandler from "components/Utils/ErrorsHandler"; +import {useSnackbar} from 'notistack'; +import {makeStyles} from '@material-ui/core/styles'; +import RegisterStudentDialog from "./RegisterStudentDialog"; + +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(3), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + avatar: { + margin: theme.spacing(1), + }, + form: { + marginTop: theme.spacing(3), + width: '100%' + }, + button: { + marginTop: theme.spacing(1) + }, +})) + +interface InviteStudentDialogProps { + courseId: number; + open: boolean; + onClose: () => void; + onStudentInvited: () => Promise; + email?: string; +} + +const InviteStudentDialog: FC = ({courseId, open, onClose, onStudentInvited, email: initialEmail}) => { + const classes = useStyles(); + const {enqueueSnackbar} = useSnackbar(); + const [email, setEmail] = useState(initialEmail || ""); + const [errors, setErrors] = useState([]); + const [isInviting, setIsInviting] = useState(false); + const [showRegisterDialog, setShowRegisterDialog] = useState(false); + const [students, setStudents] = useState([]); + + const getStudents = async () => { + try { + const data = await ApiSingleton.accountApi.accountGetAllStudents(); + setStudents(data); + } catch (error) { + console.error("Error fetching students:", error); + } + }; + + useEffect(() => { + getStudents(); + }, []); + + const getCleanEmail = (input: string) => { + return input.split(' / ')[0].trim(); + }; + + const inviteStudent = async () => { + setIsInviting(true); + setErrors([]); + try { + const cleanEmail = getCleanEmail(email); + await ApiSingleton.coursesApi.coursesInviteStudent({ + courseId: courseId, + email: cleanEmail, + name: "", + surname: "", + middleName: "" + }); + enqueueSnackbar("Студент успешно приглашен", {variant: "success"}); + setEmail(""); + onClose(); + await onStudentInvited(); + } catch (error) { + const responseErrors = await ErrorsHandler.getErrorMessages(error as Response); + if (responseErrors.length > 0) { + setErrors(responseErrors); + } else { + setErrors(['Студент с такой почтой не найден']); + } + } finally { + setIsInviting(false); + } + }; + + const hasMatchingStudent = () => { + const cleanEmail = getCleanEmail(email); + return students.some(student => + student.email === cleanEmail || + `${student.surname} ${student.name}`.includes(cleanEmail) + ); + }; + + return ( + <> + !isInviting && onClose()} + maxWidth="sm" + fullWidth + > + + + + + + + + + + Пригласить студента + + + + + + {errors.length > 0 && ( + + {errors[0]} + + )} +
+ + + + typeof option === 'string' + ? option + : `${option.email} / ${option.surname} ${option.name}` + } + inputValue={email} + onInputChange={(event, newInputValue) => { + setEmail(newInputValue); + }} + renderOption={(props, option) => ( +
  • + + + + {option.email} / + + + + + {option.surname} {option.name} + + + +
  • + )} + renderInput={(params) => ( + + )} + /> +
    +
    + + + + + + + + + + + +
    +
    +
    + + setShowRegisterDialog(false)} + onStudentRegistered={onStudentInvited} + initialEmail={getCleanEmail(email)} + /> + + ); +}; + +export default InviteStudentDialog; \ No newline at end of file diff --git a/hwproj.front/src/components/Courses/RegisterStudentDialog.tsx b/hwproj.front/src/components/Courses/RegisterStudentDialog.tsx new file mode 100644 index 000000000..b58de3eb6 --- /dev/null +++ b/hwproj.front/src/components/Courses/RegisterStudentDialog.tsx @@ -0,0 +1,225 @@ +import * as React from "react"; +import {FC, useEffect, useState} from "react"; +import { + Dialog, + DialogContent, + DialogTitle, + Grid, + Typography, + TextField, + Button, + Avatar +} from "@mui/material"; +import MailOutlineIcon from '@mui/icons-material/MailOutline'; +import ApiSingleton from "../../api/ApiSingleton"; +import ErrorsHandler from "components/Utils/ErrorsHandler"; +import {useSnackbar} from 'notistack'; +import {makeStyles} from '@material-ui/core/styles'; + +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(3), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + avatar: { + margin: theme.spacing(1), + }, + form: { + marginTop: theme.spacing(3), + width: '100%' + }, + button: { + marginTop: theme.spacing(1) + }, +})) + +interface RegisterStudentDialogProps { + courseId: number; + open: boolean; + onClose: () => void; + onStudentRegistered: () => Promise; + initialEmail?: string; +} + +const RegisterStudentDialog: FC = ({courseId, open, onClose, onStudentRegistered, initialEmail}) => { + const classes = useStyles(); + const {enqueueSnackbar} = useSnackbar(); + const [email, setEmail] = useState(initialEmail || ""); + const [name, setName] = useState(""); + const [surname, setSurname] = useState(""); + const [middleName, setMiddleName] = useState(""); + const [errors, setErrors] = useState([]); + const [isRegistering, setIsRegistering] = useState(false); + + useEffect(() => { + if (initialEmail) { + setEmail(initialEmail); + } + }, [initialEmail]); + + const registerStudent = async () => { + setIsRegistering(true); + setErrors([]); + try { + await ApiSingleton.coursesApi.coursesInviteStudent({ + courseId: courseId, + email: email, + name: name, + surname: surname, + middleName: middleName + }); + enqueueSnackbar("Студент успешно зарегистрирован и приглашен", {variant: "success"}); + onClose(); + await onStudentRegistered(); + } catch (error) { + const responseErrors = await ErrorsHandler.getErrorMessages(error as Response); + if (responseErrors.length > 0) { + setErrors(responseErrors); + } else { + setErrors(['Не удалось зарегистрировать студента']); + } + } finally { + setIsRegistering(false); + } + }; + + return ( + !isRegistering && onClose()} + maxWidth="sm" + fullWidth + > + + + + + + + + + + Зарегистрировать студента + + + + + + {errors.length > 0 && ( + + {errors[0]} + + )} +
    + + + setEmail(e.target.value)} + InputProps={{ + autoComplete: "new-email" + }} + /> + + + setName(e.target.value)} + InputProps={{ + autoComplete: "new-name" + }} + /> + + + setSurname(e.target.value)} + InputProps={{ + autoComplete: "new-surname" + }} + /> + + + setMiddleName(e.target.value)} + InputProps={{ + autoComplete: "new-middlename" + }} + /> + + + + + + + + + + +
    +
    +
    + ); +}; + +export default RegisterStudentDialog; \ No newline at end of file diff --git a/hwproj.front/src/components/Experts/AuthLayout.tsx b/hwproj.front/src/components/Experts/AuthLayout.tsx index ebfb92c9b..49f29dd57 100644 --- a/hwproj.front/src/components/Experts/AuthLayout.tsx +++ b/hwproj.front/src/components/Experts/AuthLayout.tsx @@ -1,4 +1,4 @@ -import {Navigate, useParams} from 'react-router-dom'; +import {Navigate, useParams} from 'react-router-dom'; import React, {FC, useEffect, useState} from "react"; import ApiSingleton from "./../../api/ApiSingleton"; import {Box, Typography} from "@material-ui/core"; @@ -17,49 +17,60 @@ const ExpertAuthLayout: FC = (props: IExpertAuthLayoutPr const [isTokenValid, setIsTokenValid] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isProfileAlreadyEdited, setIsProfileAlreadyEdited] = useState(false); + const [isExpert, setIsExpert] = useState(false); useEffect(() => { const checkToken = async () => { const isExpired = ApiSingleton.authService.isTokenExpired(token); if (!isExpired) { - const isExpertLoggedIn = await ApiSingleton.expertsApi.expertsLogin(credentials) - if (isExpertLoggedIn.succeeded) { - ApiSingleton.authService.setToken(token!); - setIsTokenValid(true); - props.onLogin(); - - const isEdited = await ApiSingleton.authService.isExpertProfileEdited(); - if (isEdited.succeeded && isEdited.value) { - setIsProfileAlreadyEdited(true); + try { + const validationResult = await ApiSingleton.accountApi.accountValidateToken(credentials); + + if (validationResult.succeeded && validationResult.value?.isValid) { + ApiSingleton.authService.setToken(token!); + setIsTokenValid(true); + setIsExpert(validationResult.value.role === "Expert"); + props.onLogin(); + + if (validationResult.value.role === "Expert") { + const isEdited = await ApiSingleton.authService.isExpertProfileEdited(); + if (isEdited.succeeded && isEdited.value) { + setIsProfileAlreadyEdited(true); + } + } + + setIsLoading(false); + return; } - - setIsLoading(false); - return + } catch (error) { + console.error("Token validation error:", error); } } + setIsTokenValid(false); setIsLoading(false); }; - + checkToken(); }, [token]); - return isLoading ? ( -
    -

    Проверка токена...

    - -
    - ) : ( - isTokenValid ? ( - isProfileAlreadyEdited ? ( - ) : () - ) : ( + if (isLoading) { + return ( +
    +

    Проверка токена...

    + +
    + ); + } + + if (!isTokenValid) { + return ( + justifyContent={'center'}> Ошибка в пригласительной ссылке @@ -69,8 +80,16 @@ const ExpertAuthLayout: FC = (props: IExpertAuthLayoutPr - ) - ); + ); + } + + if (isExpert) { + return isProfileAlreadyEdited + ? + : ; + } else { + return ; + } } -export default ExpertAuthLayout; \ No newline at end of file +export default ExpertAuthLayout;