From c7069060a496219f0106e12c32a67174fadef803 Mon Sep 17 00:00:00 2001 From: Artem-Rzhankoff Date: Tue, 14 May 2024 15:30:57 +0300 Subject: [PATCH 1/3] feat: support sharing stats by generating a jwt and including it in the link --- .../Controllers/AccountController.cs | 9 ++ .../Controllers/StatisticsController.cs | 8 +- .../HwProj.APIGateway.API/Startup.cs | 59 +++++++++++- .../Controllers/AccountController.cs | 8 ++ .../Services/AccountService.cs | 5 + .../Services/AuthTokenService.cs | 21 ++++ .../Services/IAccountService.cs | 1 + .../Services/IAuthTokenService.cs | 1 + .../AuthServiceClient.cs | 10 ++ .../IAuthServiceClient.cs | 1 + .../HwProj.HttpUtils/RequestHeaderBuilder.cs | 7 ++ .../HwProj.Utils/Auth/AuthExtensions.cs | 3 + .../Auth/GuestModeAuthenticationHandler.cs | 46 +++++++++ .../Auth/UserIdAuthenticationHandler.cs | 4 + .../Configuration/StartupExtensions.cs | 17 +++- .../Filters/CourseDataFilterAttribute.cs | 6 +- .../CoursesServiceClient.cs | 1 + .../Controllers/SolutionsController.cs | 26 ++++- .../HwProj.SolutionsService.API/Startup.cs | 10 +- .../SolutionsServiceClient.cs | 2 + hwproj.front/src/api/api.ts | 95 +++++++++++++++++-- .../Courses/Statistics/StudentStatsChart.tsx | 48 ++++++++-- 22 files changed, 356 insertions(+), 32 deletions(-) create mode 100644 HwProj.Common/HwProj.Utils/Auth/GuestModeAuthenticationHandler.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/AccountController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/AccountController.cs index 90f476a8e..c6c5af93a 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/AccountController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/AccountController.cs @@ -121,6 +121,15 @@ public async Task RefreshToken() var tokenMeta = await AuthServiceClient.RefreshToken(UserId!); return Ok(tokenMeta); } + + [HttpGet("getGuestToken")] + [Authorize(Roles = Roles.LecturerRole)] + [ProducesResponseType(typeof(TokenCredentials), (int)HttpStatusCode.OK)] + public async Task GetGuestToken([FromQuery] string courseId) + { + var tokenMeta = await AuthServiceClient.GetGuestToken(courseId); + return Ok(tokenMeta); + } [HttpPut("edit")] [Authorize] diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs index 12668330c..9ff481b59 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Net; using System.Threading.Tasks; using HwProj.APIGateway.API.Models.Statistics; @@ -7,6 +8,7 @@ using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Roles; using HwProj.SolutionsService.Client; +using HwProj.Utils.Auth; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -70,14 +72,16 @@ public async Task GetCourseStatistics(long courseId) } [HttpGet("{courseId}/charts")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.QueryStringTokenOrDefaultAuthentication)] [ProducesResponseType(typeof(AdvancedCourseStatisticsViewModel), (int)HttpStatusCode.OK)] - public async Task GetChartStatistics(long courseId) + public async Task GetChartStatistics(long courseId, [FromQuery] string token) { var course = await _coursesClient.GetCourseById(courseId); if (course == null) return Forbid(); var statistics = await _solutionClient.GetCourseStatistics(courseId, UserId); + var studentIds = statistics.Select(t => t.StudentId).ToArray(); var studentsData = await AuthServiceClient.GetAccountsData(studentIds); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index 7f353c780..5fe7e567f 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -1,9 +1,12 @@ -using HwProj.AuthService.Client; +using System; +using System.Threading.Tasks; +using HwProj.AuthService.Client; using HwProj.CoursesService.Client; using HwProj.NotificationsService.Client; using HwProj.SolutionsService.Client; using HwProj.Utils.Auth; using HwProj.Utils.Configuration; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -27,7 +30,10 @@ public void ConfigureServices(IServiceCollection services) const string authenticationProviderKey = "GatewayKey"; - services.AddAuthentication() + services.AddAuthentication(options => + { + options.DefaultScheme = authenticationProviderKey; + }) .AddJwtBearer(authenticationProviderKey, x => { x.RequireHttpsMetadata = false; @@ -40,6 +46,55 @@ public void ConfigureServices(IServiceCollection services) IssuerSigningKey = AuthorizationKey.SecurityKey, ValidateIssuerSigningKey = true }; + }) + .AddJwtBearer(AuthSchemeConstants.QueryStringTokenAuthentication, options => + { + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = "AuthService", + ValidateLifetime = false, + ValidateAudience = false, + IssuerSigningKey = AuthorizationKey.SecurityKey + }; + + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + if (context.Request.Query.ContainsKey("token")) + { + context.Token = context.Request.Query["token"]; + } + else + { + context.Fail("Unauthorized"); + } + + return Task.CompletedTask; + }, + OnTokenValidated = async context => + { + var courseIdClaim = context.Principal.FindFirst("_courseId"); + if (courseIdClaim == null) + { + context.Fail("Unauthorized"); + return; + } + + var authServiceClient = context.HttpContext.RequestServices + .GetRequiredService(); + var statsAccessToken = await authServiceClient.GetGuestToken(courseIdClaim.Value); + var guestToken = context.Request.Query["token"]; + + if (statsAccessToken.AccessToken != guestToken) + { + context.Fail("Unauthorized"); + } + } + }; + }); services.AddHttpClient(); diff --git a/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs b/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs index da8518450..ae39bf46f 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs @@ -82,6 +82,14 @@ public async Task RefreshToken(string userId) var tokenMeta = await _accountService.RefreshToken(userId); return Ok(tokenMeta); } + + [HttpGet("getGuestToken")] + [ProducesResponseType(typeof(TokenCredentials), (int)HttpStatusCode.OK)] + public async Task GetGuestToken([FromQuery] string courseId) + { + var tokenMeta = await _accountService.GetGuestToken(courseId); + return tokenMeta; + } [HttpPut("edit/{userId}")] [ProducesResponseType(typeof(Result), (int)HttpStatusCode.OK)] diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs index 6dc1904ce..8689f42a0 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs @@ -134,6 +134,11 @@ public async Task> RefreshToken(string userId) ? Result.Failed("Пользователь не найден") : await GetToken(user); } + + public async Task GetGuestToken(string courseId) + { + return await _tokenService.GetTokenAsync(courseId); + } public async Task> RegisterUserAsync(RegisterDataDTO model) { diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs index cd7d9e778..0523373ab 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs @@ -51,5 +51,26 @@ public async Task GetTokenAsync(User user) return tokenCredentials; } + + public async Task GetTokenAsync(string courseId) + { + var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration["SecurityKey"])); + + var token = new JwtSecurityToken( + issuer: _configuration["ApiName"], + claims: new[] + { + new Claim("_courseId", courseId), + new Claim("_isGuest", "true") + }, + signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256)); + + var tokenCredentials = new TokenCredentials + { + AccessToken = new JwtSecurityTokenHandler().WriteToken(token) + }; + + return tokenCredentials; + } } } diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs index f956743f7..c2b13d0c9 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs @@ -16,6 +16,7 @@ public interface IAccountService Task EditAccountAsync(string accountId, EditDataDTO model); Task> LoginUserAsync(LoginViewModel model); Task> RefreshToken(string userId); + Task GetGuestToken(string courseId); Task InviteNewLecturer(string emailOfInvitedUser); Task> GetUsersInRole(string role); Task RequestPasswordRecovery(RequestPasswordRecoveryViewModel model); diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs index ee0caef99..e3056d11d 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs @@ -7,5 +7,6 @@ namespace HwProj.AuthService.API.Services public interface IAuthTokenService { Task GetTokenAsync(User user); + Task GetTokenAsync(string courseId); } } diff --git a/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs b/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs index d8ee4da06..534ed5f7c 100644 --- a/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs +++ b/HwProj.AuthService/HwProj.AuthService.Client/AuthServiceClient.cs @@ -99,6 +99,16 @@ public async Task> RefreshToken(string userId) var response = await _httpClient.SendAsync(httpRequest); return await response.DeserializeAsync>(); } + + public async Task GetGuestToken(string courseId) + { + using var httpRequest = new HttpRequestMessage( + HttpMethod.Get, + _authServiceUri + $"api/account/getGuestToken/?courseId={courseId}"); + + var response = await _httpClient.SendAsync(httpRequest); + return await response.DeserializeAsync(); + } public async Task Edit(EditAccountViewModel model, string userId) { diff --git a/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs b/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs index 45afa8fcc..516dee15e 100644 --- a/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs +++ b/HwProj.AuthService/HwProj.AuthService.Client/IAuthServiceClient.cs @@ -13,6 +13,7 @@ public interface IAuthServiceClient Task> Register(RegisterViewModel model); Task> Login(LoginViewModel model); Task> RefreshToken(string userId); + Task GetGuestToken(string courseId); Task Edit(EditAccountViewModel model, string userId); Task InviteNewLecturer(InviteLecturerViewModel model); Task FindByEmailAsync(string email); diff --git a/HwProj.Common/HwProj.HttpUtils/RequestHeaderBuilder.cs b/HwProj.Common/HwProj.HttpUtils/RequestHeaderBuilder.cs index 5ac4c68e2..db1ebfc46 100644 --- a/HwProj.Common/HwProj.HttpUtils/RequestHeaderBuilder.cs +++ b/HwProj.Common/HwProj.HttpUtils/RequestHeaderBuilder.cs @@ -10,5 +10,12 @@ public static void TryAddUserId(this HttpRequestMessage request, IHttpContextAcc var userId = httpContextAccessor.HttpContext.User.FindFirst("_id"); if (userId != null) request.Headers.Add("UserId", userId.Value); } + + public static void TryAddGuestMode(this HttpRequestMessage request, IHttpContextAccessor httpContextAccessor) + { + var guestMode = httpContextAccessor.HttpContext.User.FindFirst("_isGuest"); + if (guestMode != null) + request.Headers.Add("GuestMode", guestMode.Value); + } } } diff --git a/HwProj.Common/HwProj.Utils/Auth/AuthExtensions.cs b/HwProj.Common/HwProj.Utils/Auth/AuthExtensions.cs index 34a255fb0..52daae37d 100644 --- a/HwProj.Common/HwProj.Utils/Auth/AuthExtensions.cs +++ b/HwProj.Common/HwProj.Utils/Auth/AuthExtensions.cs @@ -11,6 +11,9 @@ public static class AuthExtensions { public static string? GetUserIdFromHeader(this HttpRequest request) => request.Headers.TryGetValue("UserId", out var id) ? id.FirstOrDefault() : null; + + public static string? GetGuestModeFromHeader(this HttpRequest request) => + request.Headers.TryGetValue("GuestMode", out var isGuest) ? isGuest.FirstOrDefault() : null; public static AccountDataDto ToAccountDataDto(this User user, string role) { diff --git a/HwProj.Common/HwProj.Utils/Auth/GuestModeAuthenticationHandler.cs b/HwProj.Common/HwProj.Utils/Auth/GuestModeAuthenticationHandler.cs new file mode 100644 index 000000000..727fc04cc --- /dev/null +++ b/HwProj.Common/HwProj.Utils/Auth/GuestModeAuthenticationHandler.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using HwProj.Utils.Authorization; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace HwProj.Utils.Auth +{ + public class GuestModeAuthenticationOptions : AuthenticationSchemeOptions + { + } + + public class GuestModeAuthenticationHandler : AuthenticationHandler + { + public GuestModeAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() + { + var isGuest = Request.GetGuestModeFromHeader(); + if (isGuest != "true") + return Task.FromResult(AuthenticateResult.Fail("Unauthorized")); + + var claims = new List + { + new Claim("_isGuest", isGuest) + }; + + var identity = new ClaimsIdentity(claims, Scheme.Name); + var principal = new System.Security.Principal.GenericPrincipal(identity, null); + var ticket = new AuthenticationTicket(principal, Scheme.Name); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } + } +} \ No newline at end of file diff --git a/HwProj.Common/HwProj.Utils/Auth/UserIdAuthenticationHandler.cs b/HwProj.Common/HwProj.Utils/Auth/UserIdAuthenticationHandler.cs index b76f96560..8ebda425f 100644 --- a/HwProj.Common/HwProj.Utils/Auth/UserIdAuthenticationHandler.cs +++ b/HwProj.Common/HwProj.Utils/Auth/UserIdAuthenticationHandler.cs @@ -12,6 +12,10 @@ namespace HwProj.Utils.Auth public static class AuthSchemeConstants { public const string UserIdAuthentication = "UserIdAuth"; + public const string GuestModeAuthentication = "GuestModeAuth"; + public const string QueryStringTokenAuthentication = "QueryStringTokenAuth"; + public const string QueryStringTokenOrDefaultAuthentication = QueryStringTokenAuthentication + "," + "GatewayKey"; + public const string GuestModeOrUseridAuthentication = GuestModeAuthentication + "," + UserIdAuthentication; } public class UserIdAuthenticationOptions : AuthenticationSchemeOptions diff --git a/HwProj.Common/HwProj.Utils/Configuration/StartupExtensions.cs b/HwProj.Common/HwProj.Utils/Configuration/StartupExtensions.cs index 353354af0..a5dc0e665 100644 --- a/HwProj.Common/HwProj.Utils/Configuration/StartupExtensions.cs +++ b/HwProj.Common/HwProj.Utils/Configuration/StartupExtensions.cs @@ -9,6 +9,7 @@ using HwProj.EventBus.Client.Interfaces; using HwProj.Utils.Auth; using HwProj.Utils.Configuration.Middleware; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -179,14 +180,22 @@ public static IApplicationBuilder ConfigureHwProj(this IApplicationBuilder app, return app; } - public static IServiceCollection AddUserIdAuthentication(this IServiceCollection services) + public static AuthenticationBuilder AddUserIdAuthentication(this AuthenticationBuilder builder) { - services - .AddAuthentication(AuthSchemeConstants.UserIdAuthentication) + builder .AddScheme( AuthSchemeConstants.UserIdAuthentication, null); - return services; + return builder; + } + + public static AuthenticationBuilder AddGuestModeAuthentication(this AuthenticationBuilder builder) + { + builder + .AddScheme( + AuthSchemeConstants.GuestModeAuthentication, null); + + return builder; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Filters/CourseDataFilterAttribute.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Filters/CourseDataFilterAttribute.cs index 9577d691f..41c5bbdbe 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Filters/CourseDataFilterAttribute.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Filters/CourseDataFilterAttribute.cs @@ -16,13 +16,13 @@ public class CourseDataFilterAttribute : ResultFilterAttribute { public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { + var isGuest = context.HttpContext.Request.GetGuestModeFromHeader(); var userId = context.HttpContext.Request.GetUserIdFromHeader(); - - if (userId == null) + if (userId == null && isGuest != "true") { context.Result = new ForbidResult(); } - else + else if (isGuest == null) { var result = context.Result as ObjectResult; if (result?.Value is CourseDTO courseDto && !courseDto.MentorIds.Contains(userId)) diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs index a38565b46..5c49089f6 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs @@ -58,6 +58,7 @@ public async Task GetAllCourses() _coursesServiceUri + $"api/Courses/{courseId}"); httpRequest.TryAddUserId(_httpContextAccessor); + httpRequest.TryAddGuestMode(_httpContextAccessor); var response = await _httpClient.SendAsync(httpRequest); return response.IsSuccessStatusCode ? await response.DeserializeAsync() : null; } diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs index ed30b9de6..a0baf3c8b 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs @@ -20,7 +20,6 @@ namespace HwProj.SolutionsService.API.Controllers { [Route("api/[controller]")] - [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] [ApiController] public class SolutionsController : Controller { @@ -42,12 +41,14 @@ public SolutionsController( } [HttpGet] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task GetAllSolutions() { return await _solutionsService.GetAllSolutionsAsync(); } [HttpGet("{solutionId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task GetSolution(long solutionId) { var solution = await _solutionsService.GetSolutionAsync(solutionId); @@ -57,18 +58,21 @@ public async Task GetSolution(long solutionId) } [HttpGet("taskSolutions/{taskId}/{studentId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task GetTaskSolutionsFromStudent(long taskId, string studentId) { return await _solutionsService.GetTaskSolutionsFromStudentAsync(taskId, studentId); } [HttpPost("taskSolutions/{studentId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task GetLastTaskSolutions([FromBody] long[] taskIds, string studentId) { return await _solutionsService.GetLastTaskSolutions(taskIds, studentId); } [HttpPost("{taskId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] public async Task PostSolution(long taskId, [FromBody] PostSolutionModel solutionModel) { @@ -82,6 +86,7 @@ public async Task PostSolution(long taskId, [FromBody] PostSoluti } [HttpPost("rateSolution/{solutionId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task RateSolution(long solutionId, [FromBody] RateSolutionModel rateSolutionModel) { @@ -101,6 +106,7 @@ public async Task RateSolution(long solutionId, } [HttpPost("rateEmptySolution/{taskId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task PostEmptySolutionWithRate(long taskId, [FromBody] SolutionViewModel solutionViewModel) { @@ -113,18 +119,21 @@ public async Task PostEmptySolutionWithRate(long taskId, } [HttpPost("markSolutionFinal/{solutionId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task MarkSolutionFinal(long solutionId) { await _solutionsService.MarkSolutionFinal(solutionId); } [HttpDelete("delete/{solutionId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task DeleteSolution(long solutionId) { await _solutionsService.DeleteSolutionAsync(solutionId); } [HttpPost("{groupId}/{taskId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task PostSolution(long groupId, long taskId, [FromBody] SolutionViewModel solutionViewModel) { var solution = _mapper.Map(solutionViewModel); @@ -134,12 +143,14 @@ public async Task PostSolution(long groupId, long taskId, [FromBody] Solut } [HttpGet("{groupId}/taskSolutions/{taskId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task GetTaskSolutionsFromGroup(long groupId, long taskId) { return await _solutionsService.GetTaskSolutionsFromGroupAsync(taskId, groupId); } [HttpGet("getLecturersStat/{courseId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] [ProducesResponseType(typeof(StatisticsLecturerDTO[]), (int)HttpStatusCode.OK)] public async Task GetLecturersStat(long courseId) { @@ -181,6 +192,7 @@ public async Task GetLecturersStat(long courseId) } [HttpGet("getCourseStat/{courseId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.GuestModeOrUseridAuthentication)] [ProducesResponseType(typeof(StatisticsCourseMatesDto[]), (int)HttpStatusCode.OK)] public async Task GetCourseStat(long courseId) { @@ -192,7 +204,10 @@ public async Task GetCourseStat(long courseId) .Select(t => t.Id) .ToArray(); - var userId = Request.GetUserIdFromHeader(); + var userId = Request.GetGuestModeFromHeader() == "true" + ? course.MentorIds.FirstOrDefault() + : Request.GetUserIdFromHeader(); + var solutions = await _solutionsRepository.FindAll(t => taskIds.Contains(t.TaskId)).ToListAsync(); var courseMates = course.MentorIds.Contains(userId) ? course.AcceptedStudents @@ -212,11 +227,13 @@ public async Task GetCourseStat(long courseId) } [HttpGet("getBenchmarkStat/{courseId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.GuestModeOrUseridAuthentication)] [ProducesResponseType(typeof(StatisticsCourseStudentsBenchmarkDTO), (int)HttpStatusCode.OK)] public async Task GetBenchmarkStats(long courseId) { var course = await _coursesClient.GetCourseById(courseId); - if (course == null) return NotFound(); + if (course == null) + return NotFound(); var taskIds = course.Homeworks .SelectMany(t => t.Tasks) @@ -263,6 +280,7 @@ public async Task GetBenchmarkStats(long courseId) } [HttpGet("getTaskStats/{courseId}/{taskId}")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] [ProducesResponseType(typeof(StudentSolutions[]), (int)HttpStatusCode.OK)] public async Task GetTaskStats(long courseId, long taskId) { @@ -277,12 +295,14 @@ public async Task GetTaskStats(long courseId, long taskId) } [HttpPost("allUnrated")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task GetAllUnratedSolutionsForTasks([FromBody] long[] taskIds) { return await _solutionsService.GetAllUnratedSolutions(taskIds); } [HttpGet("taskSolutionsStats")] + [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] public async Task GetTaskSolutionsStats([FromBody] long[] taskIds) { return await _solutionsService.GetTaskSolutionsStats(taskIds); diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Startup.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Startup.cs index 1e98679bb..601d4e7eb 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Startup.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Startup.cs @@ -4,6 +4,7 @@ using HwProj.SolutionsService.API.Models; using HwProj.SolutionsService.API.Repositories; using HwProj.SolutionsService.API.Services; +using HwProj.Utils.Auth; using HwProj.Utils.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -29,7 +30,14 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); - services.AddUserIdAuthentication(); + services + .AddAuthentication(options => + { + options.DefaultScheme = AuthSchemeConstants.UserIdAuthentication; + }) + .AddUserIdAuthentication() + .AddGuestModeAuthentication(); + services.AddHttpClient(); services.AddHttpContextAccessor(); services.AddAuthServiceClient(); diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs b/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs index 41b4536a6..433ebf71a 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs @@ -198,6 +198,7 @@ public async Task GetCourseStatistics(long courseId, _solutionServiceUri + $"api/Solutions/getCourseStat/{courseId}?userId={userId}"); httpRequest.TryAddUserId(_httpContextAccessor); + httpRequest.TryAddGuestMode(_httpContextAccessor); var response = await _httpClient.SendAsync(httpRequest); return await response.DeserializeAsync(); } @@ -209,6 +210,7 @@ public async Task GetBenchmarkStatistics(l _solutionServiceUri + $"api/Solutions/getBenchmarkStat/{courseId}"); httpRequest.TryAddUserId(_httpContextAccessor); + httpRequest.TryAddGuestMode(_httpContextAccessor); var response = await _httpClient.SendAsync(httpRequest); return await response.DeserializeAsync(); } diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 830b16d55..7fb9a8506 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -2093,6 +2093,41 @@ export const AccountApiFetchParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @param {string} [courseId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiAccountGetGuestTokenGet(courseId?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/Account/getGuestToken`; + 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 (courseId !== undefined) { + localVarQueryParameter['courseId'] = courseId; + } + + 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 {string} userId @@ -2496,6 +2531,24 @@ export const AccountApiFp = function(configuration?: Configuration) { }); }; }, + /** + * + * @param {string} [courseId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiAccountGetGuestTokenGet(courseId?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = AccountApiFetchParamCreator(configuration).apiAccountGetGuestTokenGet(courseId, options); + return (fetch: FetchAPI = portableFetch, 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 {string} userId @@ -2709,6 +2762,15 @@ export const AccountApiFactory = function (configuration?: Configuration, fetch? apiAccountGetAllStudentsGet(options?: any) { return AccountApiFp(configuration).apiAccountGetAllStudentsGet(options)(fetch, basePath); }, + /** + * + * @param {string} [courseId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiAccountGetGuestTokenGet(courseId?: string, options?: any) { + return AccountApiFp(configuration).apiAccountGetGuestTokenGet(courseId, options)(fetch, basePath); + }, /** * * @param {string} userId @@ -2839,6 +2901,17 @@ export class AccountApi extends BaseAPI { return AccountApiFp(this.configuration).apiAccountGetAllStudentsGet(options)(this.fetch, this.basePath); } + /** + * + * @param {string} [courseId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountApi + */ + public apiAccountGetGuestTokenGet(courseId?: string, options?: any) { + return AccountApiFp(this.configuration).apiAccountGetGuestTokenGet(courseId, options)(this.fetch, this.basePath); + } + /** * * @param {string} userId @@ -6356,10 +6429,11 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur /** * * @param {number} courseId + * @param {string} [token] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsByCourseIdChartsGet(courseId: number, options: any = {}): FetchArgs { + apiStatisticsByCourseIdChartsGet(courseId: number, token?: string, options: any = {}): FetchArgs { // verify required parameter 'courseId' is not null or undefined if (courseId === null || courseId === undefined) { throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling apiStatisticsByCourseIdChartsGet.'); @@ -6379,6 +6453,10 @@ export const StatisticsApiFetchParamCreator = function (configuration?: Configur localVarHeaderParameter["Authorization"] = localVarApiKeyValue; } + if (token !== undefined) { + localVarQueryParameter['token'] = token; + } + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 localVarUrlObj.search = null; @@ -6473,11 +6551,12 @@ export const StatisticsApiFp = function(configuration?: Configuration) { /** * * @param {number} courseId + * @param {string} [token] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsByCourseIdChartsGet(courseId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsByCourseIdChartsGet(courseId, options); + apiStatisticsByCourseIdChartsGet(courseId: number, token?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = StatisticsApiFetchParamCreator(configuration).apiStatisticsByCourseIdChartsGet(courseId, token, options); return (fetch: FetchAPI = portableFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { if (response.status >= 200 && response.status < 300) { @@ -6536,11 +6615,12 @@ export const StatisticsApiFactory = function (configuration?: Configuration, fet /** * * @param {number} courseId + * @param {string} [token] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiStatisticsByCourseIdChartsGet(courseId: number, options?: any) { - return StatisticsApiFp(configuration).apiStatisticsByCourseIdChartsGet(courseId, options)(fetch, basePath); + apiStatisticsByCourseIdChartsGet(courseId: number, token?: string, options?: any) { + return StatisticsApiFp(configuration).apiStatisticsByCourseIdChartsGet(courseId, token, options)(fetch, basePath); }, /** * @@ -6573,12 +6653,13 @@ export class StatisticsApi extends BaseAPI { /** * * @param {number} courseId + * @param {string} [token] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof StatisticsApi */ - public apiStatisticsByCourseIdChartsGet(courseId: number, options?: any) { - return StatisticsApiFp(this.configuration).apiStatisticsByCourseIdChartsGet(courseId, options)(this.fetch, this.basePath); + public apiStatisticsByCourseIdChartsGet(courseId: number, token?: string, options?: any) { + return StatisticsApiFp(this.configuration).apiStatisticsByCourseIdChartsGet(courseId, token, options)(this.fetch, this.basePath); } /** diff --git a/hwproj.front/src/components/Courses/Statistics/StudentStatsChart.tsx b/hwproj.front/src/components/Courses/Statistics/StudentStatsChart.tsx index 4ee71afa9..c0514a236 100644 --- a/hwproj.front/src/components/Courses/Statistics/StudentStatsChart.tsx +++ b/hwproj.front/src/components/Courses/Statistics/StudentStatsChart.tsx @@ -1,12 +1,13 @@ import React, {useEffect, useState} from "react"; -import {Link, useParams} from 'react-router-dom'; -import {Grid, Box, Typography, Paper, CircularProgress} from "@mui/material"; +import {Link, useParams, useSearchParams } from 'react-router-dom'; +import {Grid, Box, Typography, Paper, CircularProgress, IconButton, Snackbar } from "@mui/material"; import { CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel, StatisticsCourseMeasureSolutionModel } from "../../../api/"; +import ShareIcon from "@mui/icons-material/Share"; import ApiSingleton from "../../../api/ApiSingleton"; import HelpPopoverChartInfo from './HelpPopoverChartInfo'; import StudentCheckboxList from "./StudentCheckboxList"; @@ -15,7 +16,7 @@ import StudentPunctualityChart from './StudentPunctualityChart'; interface IStudentStatsChartState { isFound: boolean; - isSelectionMode: boolean; + isLecturer: boolean; course: CourseViewModel; homeworks: HomeworkViewModel[]; solutions: StatisticsCourseMatesModel[]; @@ -25,11 +26,14 @@ interface IStudentStatsChartState { const StudentStatsChart: React.FC = () => { const {courseId} = useParams(); + const [ searchParams ] = useSearchParams(); const isLoggedIn = ApiSingleton.authService.isLoggedIn(); const [selectedStudents, setSelectedStudents] = useState([]); + const isGuestAccess = searchParams.get("token") != null; + const [ copied, setCopied ] = useState(false); const [state, setState] = useState({ isFound: false, - isSelectionMode: false, + isLecturer: false, course: {}, homeworks: [], solutions: [], @@ -54,17 +58,21 @@ const StudentStatsChart: React.FC = () => { const setCurrentState = async () => { const params = - await ApiSingleton.statisticsApi.apiStatisticsByCourseIdChartsGet(+courseId!); - + await ApiSingleton.statisticsApi.apiStatisticsByCourseIdChartsGet(+courseId!, searchParams.get("token")!); + if (params.studentStatistics && params.studentStatistics.length > 0) { const sectorSizes = params.studentStatistics[0].homeworks! .flatMap(h => h.tasks!.map(t => t.solution!.length)) setSectorSizes(sectorSizes) } + const userId = isLoggedIn ? ApiSingleton.authService.getUserId() : undefined; + const isLecturer = userId != undefined && ApiSingleton.authService.isLecturer() + && !params.studentStatistics?.map(s => s.id).includes(userId); + setState({ isFound: true, - isSelectionMode: params.studentStatistics!.length > 1, + isLecturer: isLecturer, course: params.course!, homeworks: params.homeworks!, solutions: params.studentStatistics!, @@ -78,7 +86,7 @@ const StudentStatsChart: React.FC = () => { }, []) useEffect(() => { - if (state.isFound && !state.isSelectionMode) { + if (state.isFound && !state.isLecturer && !isGuestAccess) { setSelectedStudents((_) => [state.solutions[0].id!]) } }, [state.isFound]) @@ -88,6 +96,19 @@ const StudentStatsChart: React.FC = () => { return student.name + ' ' + student.surname; } + const copyLink = async () => { + if (copied) { + setCopied(false); + return; + } + const token = (await ApiSingleton.accountApi.apiAccountGetGuestTokenGet(courseId)).accessToken; + const sharingLink = window.location.href.includes('?token=') + ? window.location.href : window.location.href + '?token=' + token; + navigator.clipboard.writeText(sharingLink); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + const renderGoBackToCoursesStatsLink = () => { return ( { zIndex: 100 }}> - + {`${state.course.name} / ${state.course.groupName}`} бета + {state.isLecturer && + <> + + + + + } {isLoggedIn && renderGoBackToCoursesStatsLink()} - {state.isSelectionMode && + {(state.isLecturer || isGuestAccess) && ({ From a8f502c31515a83e084f9e5ea1ef150f22ec071e Mon Sep 17 00:00:00 2001 From: Artem-Rzhankoff Date: Tue, 14 May 2024 15:44:00 +0300 Subject: [PATCH 2/3] refactor --- .../HwProj.APIGateway.API/Controllers/StatisticsController.cs | 3 +-- HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs | 3 +-- .../HwProj.Utils/Auth/GuestModeAuthenticationHandler.cs | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs index 9ff481b59..5bc32704c 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/StatisticsController.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Net; using System.Threading.Tasks; using HwProj.APIGateway.API.Models.Statistics; diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index 5fe7e567f..658b4cd7d 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using HwProj.AuthService.Client; using HwProj.CoursesService.Client; using HwProj.NotificationsService.Client; diff --git a/HwProj.Common/HwProj.Utils/Auth/GuestModeAuthenticationHandler.cs b/HwProj.Common/HwProj.Utils/Auth/GuestModeAuthenticationHandler.cs index 727fc04cc..f69fb16bc 100644 --- a/HwProj.Common/HwProj.Utils/Auth/GuestModeAuthenticationHandler.cs +++ b/HwProj.Common/HwProj.Utils/Auth/GuestModeAuthenticationHandler.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; From 0c07e3d7ccd1e1a41e6a59f4e9bbff3f627e02ac Mon Sep 17 00:00:00 2001 From: Artem-Rzhankoff Date: Sun, 26 May 2024 16:07:56 +0300 Subject: [PATCH 3/3] small fixes --- .../HwProj.AuthService.API/Controllers/AccountController.cs | 6 +++--- .../HwProj.AuthService.API/Services/AccountService.cs | 4 ++-- .../HwProj.AuthService.API/Services/AuthTokenService.cs | 2 +- .../HwProj.AuthService.API/Services/IAccountService.cs | 2 +- .../HwProj.AuthService.API/Services/IAuthTokenService.cs | 2 +- .../src/components/Courses/Statistics/StudentStatsChart.tsx | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs b/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs index ae39bf46f..45f588d0b 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Controllers/AccountController.cs @@ -85,10 +85,10 @@ public async Task RefreshToken(string userId) [HttpGet("getGuestToken")] [ProducesResponseType(typeof(TokenCredentials), (int)HttpStatusCode.OK)] - public async Task GetGuestToken([FromQuery] string courseId) + public Task GetGuestToken([FromQuery] string courseId) { - var tokenMeta = await _accountService.GetGuestToken(courseId); - return tokenMeta; + var tokenMeta = _accountService.GetGuestToken(courseId); + return Task.FromResult(Ok(tokenMeta)); } [HttpPut("edit/{userId}")] diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs index 8689f42a0..cbff840c3 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/AccountService.cs @@ -135,9 +135,9 @@ public async Task> RefreshToken(string userId) : await GetToken(user); } - public async Task GetGuestToken(string courseId) + public TokenCredentials GetGuestToken(string courseId) { - return await _tokenService.GetTokenAsync(courseId); + return _tokenService.GetGuestToken(courseId); } public async Task> RegisterUserAsync(RegisterDataDTO model) diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs index 0523373ab..9aacd374b 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/AuthTokenService.cs @@ -52,7 +52,7 @@ public async Task GetTokenAsync(User user) return tokenCredentials; } - public async Task GetTokenAsync(string courseId) + public TokenCredentials GetGuestToken(string courseId) { var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration["SecurityKey"])); diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs index c2b13d0c9..c8935de03 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/IAccountService.cs @@ -16,7 +16,7 @@ public interface IAccountService Task EditAccountAsync(string accountId, EditDataDTO model); Task> LoginUserAsync(LoginViewModel model); Task> RefreshToken(string userId); - Task GetGuestToken(string courseId); + TokenCredentials GetGuestToken(string courseId); Task InviteNewLecturer(string emailOfInvitedUser); Task> GetUsersInRole(string role); Task RequestPasswordRecovery(RequestPasswordRecoveryViewModel model); diff --git a/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs b/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs index e3056d11d..d8a9ae36f 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs +++ b/HwProj.AuthService/HwProj.AuthService.API/Services/IAuthTokenService.cs @@ -7,6 +7,6 @@ namespace HwProj.AuthService.API.Services public interface IAuthTokenService { Task GetTokenAsync(User user); - Task GetTokenAsync(string courseId); + TokenCredentials GetGuestToken(string courseId); } } diff --git a/hwproj.front/src/components/Courses/Statistics/StudentStatsChart.tsx b/hwproj.front/src/components/Courses/Statistics/StudentStatsChart.tsx index c0514a236..bc0f75ff7 100644 --- a/hwproj.front/src/components/Courses/Statistics/StudentStatsChart.tsx +++ b/hwproj.front/src/components/Courses/Statistics/StudentStatsChart.tsx @@ -65,7 +65,7 @@ const StudentStatsChart: React.FC = () => { .flatMap(h => h.tasks!.map(t => t.solution!.length)) setSectorSizes(sectorSizes) } - + const userId = isLoggedIn ? ApiSingleton.authService.getUserId() : undefined; const isLecturer = userId != undefined && ApiSingleton.authService.isLecturer() && !params.studentStatistics?.map(s => s.id).includes(userId); @@ -149,7 +149,7 @@ const StudentStatsChart: React.FC = () => { {state.isLecturer && <> - +