From e67db883e3458101af35d283b91ac1f516c22f3b Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Sun, 30 Nov 2025 16:01:33 +0300 Subject: [PATCH 01/26] fix: made it possible to run on macOS --- .gitignore | 1 + HwProj.AuthService/HwProj.AuthService.API/appsettings.json | 2 +- HwProj.Common/HwProj.Common.Net8/ConnectionString.cs | 3 ++- HwProj.Common/HwProj.Utils/Configuration/ConnectionString.cs | 3 ++- .../HwProj.ContentService.API/appsettings.json | 2 +- .../HwProj.NotificationsService.API/appsettings.json | 2 +- 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 7ed326a27..dae2af57f 100644 --- a/.gitignore +++ b/.gitignore @@ -362,3 +362,4 @@ StyleCop.Cache swagger-codegen hwproj.front/static_dist/ hwproj.front/dist/ +.DS_Store diff --git a/HwProj.AuthService/HwProj.AuthService.API/appsettings.json b/HwProj.AuthService/HwProj.AuthService.API/appsettings.json index bb18739ae..c2777316b 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/appsettings.json +++ b/HwProj.AuthService/HwProj.AuthService.API/appsettings.json @@ -1,7 +1,7 @@ { "ConnectionStrings": { "DefaultConnectionForWindows": "Server=(localdb)\\mssqllocaldb;Database=AuthServiceDB;Trusted_Connection=True;TrustServerCertificate=true;", - "DefaultConnectionForLinux": "Server=localhost,1433;Database=AuthServiceDB;User ID=SA;Password=password_1234;" + "DefaultConnectionForLinux": "Server=localhost,1433;Database=AuthServiceDB;User ID=SA;Password=password_1234;TrustServerCertificate=True;" }, "Logging": { "LogLevel": { diff --git a/HwProj.Common/HwProj.Common.Net8/ConnectionString.cs b/HwProj.Common/HwProj.Common.Net8/ConnectionString.cs index 32bc4db97..be9dd0c40 100644 --- a/HwProj.Common/HwProj.Common.Net8/ConnectionString.cs +++ b/HwProj.Common/HwProj.Common.Net8/ConnectionString.cs @@ -8,7 +8,8 @@ public static class ConnectionString { public static string GetConnectionString(IConfiguration configuration) { - var option = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + var option = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "DefaultConnectionForLinux" : "DefaultConnectionForWindows"; return configuration.GetConnectionString(option) ?? ""; diff --git a/HwProj.Common/HwProj.Utils/Configuration/ConnectionString.cs b/HwProj.Common/HwProj.Utils/Configuration/ConnectionString.cs index 2c0ea9135..3e130809a 100644 --- a/HwProj.Common/HwProj.Utils/Configuration/ConnectionString.cs +++ b/HwProj.Common/HwProj.Utils/Configuration/ConnectionString.cs @@ -8,7 +8,8 @@ public static class ConnectionString { public static string GetConnectionString(IConfiguration configuration) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || + RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { return configuration.GetConnectionString("DefaultConnectionForLinux"); } diff --git a/HwProj.ContentService/HwProj.ContentService.API/appsettings.json b/HwProj.ContentService/HwProj.ContentService.API/appsettings.json index de0b9bc31..25f5a1090 100644 --- a/HwProj.ContentService/HwProj.ContentService.API/appsettings.json +++ b/HwProj.ContentService/HwProj.ContentService.API/appsettings.json @@ -1,7 +1,7 @@ { "ConnectionStrings": { "DefaultConnectionForWindows": "Server=(localdb)\\mssqllocaldb;Database=ContentServiceDB;Trusted_Connection=True;TrustServerCertificate=true;", - "DefaultConnectionForLinux": "Server=localhost,1433;Database=ContentServiceDB;User ID=SA;Password=password_1234;" + "DefaultConnectionForLinux": "Server=localhost,1433;Database=ContentServiceDB;User ID=SA;Password=password_1234;TrustServerCertificate=True;" }, "Logging": { "LogLevel": { diff --git a/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json b/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json index f6fe217f3..f196fffa6 100644 --- a/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json +++ b/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json @@ -1,7 +1,7 @@ { "ConnectionStrings": { "DefaultConnectionForWindows": "Server=(localdb)\\mssqllocaldb;Database=NotificationsServiceDB;Trusted_Connection=True;TrustServerCertificate=true;MultipleActiveResultSets=True", - "DefaultConnectionForLinux": "Server=localhost,1433;Database=NotificationsServiceDB;User ID=SA;Password=password_1234;" + "DefaultConnectionForLinux": "Server=localhost,1433;Database=NotificationsServiceDB;User ID=SA;Password=password_1234;TrustServerCertificate=True;" }, "Logging": { "LogLevel": { From e96c761a6334d81103d30d24db98c55f7b9c719f Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Sun, 30 Nov 2025 20:16:37 +0300 Subject: [PATCH 02/26] feat: made a controller for OIDC --- .../HwProj.APIGateway.API.csproj | 6 ++ .../Lti/Controllers/LtiAuthController.cs | 73 ++++++++++++++++++ .../Lti/Services/ILtiTokenService.cs | 14 ++++ .../Lti/Services/LtiOptions.cs | 9 +++ .../Lti/Services/LtiTokenService.cs | 74 +++++++++++++++++++ 5 files changed, 176 insertions(+) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiOptions.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj index 8f29323ea..2a070810a 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj +++ b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj @@ -21,9 +21,15 @@ + + + + + + diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs new file mode 100644 index 000000000..291367d40 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs @@ -0,0 +1,73 @@ +using System.Net; +using System.Threading.Tasks; +using HwProj.APIGateway.API.LTI.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/lti/launch")] +[ApiController] +public class LtiAuthController(ILtiTokenService tokenService) : ControllerBase +{ + private readonly ILtiTokenService _tokenService = tokenService; + + // Tool редиректит сюда браузер (шаг "redirect browser to Platform for Auth") + [HttpPost("authorize")] + [Authorize] // пользователь должен быть залогинен в LMS + public async Task AuthorizeLti( + [FromQuery] string issOfTheTool, + [FromQuery] string clientId, + [FromQuery] string targetLinkUri, + [FromQuery] string state, + [FromQuery] string nonce, + [FromQuery(Name = "login_hint")] string loginId, + [FromQuery(Name = "lti_message_hint")] string ltiMessageHint) + { + // 1. ПРОВЕРКА ЗАПРОСА (validate request) + if (!(await this.CheckTheRequest(issOfTheTool, clientId, targetLinkUri, loginId))) + { + return BadRequest(); + } + + // 2. СОЗДАНИЕ id_token (LTI JWT) + var idToken = _tokenService.CreateLtiIdToken( + user: User, + clientId: clientId, + redirectUri: targetLinkUri, + nonce: nonce, + ltiMessageHint: ltiMessageHint); + + // 3. ВОЗВРАТ auth response (redirect auth response to tool) + var html = $""" + + + + +
+ + + +
+ + + """; + return Content(html, "text/html"); + } + + private async Task CheckTheRequest( + string issOfTheTool, + string clientId, + string redirectUri, + string loginHint) + { + // - client_id существует и соответствует зарегистрированному Tool + // - redirect_uri допустим + // - пользователь аутентифицирован (Authorize уже проверил) + // - можешь сверить login_hint с текущим пользователем и т.д. + return true; + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs new file mode 100644 index 000000000..d47e8ea0b --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs @@ -0,0 +1,14 @@ +using System.Security.Claims; +using System.Threading.Tasks; + +namespace HwProj.APIGateway.API.LTI.Services; + +public interface ILtiTokenService +{ + string CreateLtiIdToken( + ClaimsPrincipal user, + string clientId, + string redirectUri, + string nonce, + string ltiMessageHint); +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiOptions.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiOptions.cs new file mode 100644 index 000000000..f90030bb1 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiOptions.cs @@ -0,0 +1,9 @@ +namespace HwProj.APIGateway.API.LTI.Services; + +public class LtiOptions +{ + public string? Issuer { get; set; } + public string? DeploymentId { get; set; } + public string? SigningKey { get; set; } + public int TokenLifetimeMinutes { get; set; } = 5; +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs new file mode 100644 index 000000000..5cc080960 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs @@ -0,0 +1,74 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using HwProj.APIGateway.API.LTI.Services; +using LtiAdvantage.Lti; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace HwProj.APIGateway.API.Lti.Services; + +public class LtiTokenService(IOptions options) : ILtiTokenService +{ + private readonly LtiOptions _options = options.Value; + private readonly JwtSecurityTokenHandler _tokenHandler = new(); + + public string CreateLtiIdToken( + ClaimsPrincipal user, + string clientId, + string redirectUri, + string nonce, + string ltiMessageHint) + { + // время жизни токена + var now = DateTime.UtcNow; + var expires = now.AddMinutes(this._options.TokenLifetimeMinutes); + + var signingKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(this._options.SigningKey)); + var signingCredentials = new SigningCredentials( + signingKey, + SecurityAlgorithms.RsaSha256); + + ContextClaimValueType context = new(); + /*context.Id = ; + context.Label = ; + context.Title = ; + context.Type = ; */ + + ResourceLinkClaimValueType resourceLink = new(); + /*resourceLink.Id = ; + resourceLink.Description = ; + resourceLink.Title = ;*/ + + // ??? + LaunchPresentationClaimValueType launchPresentation = new(); + + var request = new LtiResourceLinkRequest + { + DeploymentId = _options.DeploymentId, + Nonce = nonce, + // обязательные LTI claims: + Roles = [Role.InstitutionStudent], + Context = context, + ResourceLink = resourceLink, + TargetLinkUri = redirectUri, + // opaque user id внутри платформы + UserId = user.FindFirstValue(ClaimTypes.NameIdentifier) ?? "unknown-user", + LaunchPresentation = launchPresentation + }; + + // создаём сам JWT + var jwt = new JwtSecurityToken( + issuer: _options.Issuer, + audience: clientId, + claims: request.IssuedClaims, + notBefore: now, + expires: expires, + signingCredentials: signingCredentials); + + var token = _tokenHandler.WriteToken(jwt); + return token; + } +} \ No newline at end of file From fac5635d4ed0d159b6270e7c277cc537430e45de Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Thu, 4 Dec 2025 17:06:11 +0300 Subject: [PATCH 03/26] feat: added a repository for storing the connection between HomeworkTaskId and ltiTaskId --- .../HwProj.Repositories/ReadOnlyRepository.cs | 2 +- .../appsettings.Development.json | 5 ++ .../Repositories/ILtiTaskRepository.cs | 21 ++++++ .../Repositories/LtiTaskRepository.cs | 68 +++++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 HwProj.ContentService/HwProj.ContentService.API/appsettings.Development.json create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ILtiTaskRepository.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Repositories/LtiTaskRepository.cs diff --git a/HwProj.Common/HwProj.Repositories/ReadOnlyRepository.cs b/HwProj.Common/HwProj.Repositories/ReadOnlyRepository.cs index 677914ba6..b8063b384 100644 --- a/HwProj.Common/HwProj.Repositories/ReadOnlyRepository.cs +++ b/HwProj.Common/HwProj.Repositories/ReadOnlyRepository.cs @@ -17,7 +17,7 @@ public ReadOnlyRepository(DbContext context) Context = context; } - public IQueryable GetAll() + public IQueryable GetAll() { return Context.Set().AsNoTracking(); } diff --git a/HwProj.ContentService/HwProj.ContentService.API/appsettings.Development.json b/HwProj.ContentService/HwProj.ContentService.API/appsettings.Development.json new file mode 100644 index 000000000..97e58f831 --- /dev/null +++ b/HwProj.ContentService/HwProj.ContentService.API/appsettings.Development.json @@ -0,0 +1,5 @@ +{ + "LocalStorageConfiguration": { + "Path": "ContentFiles" + } +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ILtiTaskRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ILtiTaskRepository.cs new file mode 100644 index 000000000..eb0bcec8d --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ILtiTaskRepository.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using HwProj.CoursesService.API.Models; +using HwProj.Repositories; + +namespace HwProj.CoursesService.API.Repositories +{ + public interface ILtiTaskRepository + { + Task> GetAllAsync(); + Task> FindAlAsync(Expression> predicate); + Task GetAsync(long id); + Task FindAsync(Expression> predicate); + Task AddAsync(long homeworkTaskId, long ltiTaskId); + Task DeleteAsync(long id); + Task UpdateAsync(long homeworkTaskId, long ltiTaskId); + } +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/LtiTaskRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/LtiTaskRepository.cs new file mode 100644 index 000000000..47d0b3cc0 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/LtiTaskRepository.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using HwProj.CoursesService.API.Models; +using HwProj.Repositories; + +namespace HwProj.CoursesService.API.Repositories +{ + public class LtiTaskRepository : ILtiTaskRepository + { + private readonly ConcurrentDictionary _db = + new ConcurrentDictionary(); + + public Task> GetAllAsync() + { + return Task.FromResult(this._db.Values.AsQueryable()); + } + + public Task> FindAlAsync(Expression>? predicate) + { + return predicate == null + ? throw new ArgumentNullException(nameof(predicate)) + : Task.FromResult(this._db.Values.AsQueryable().Where(predicate)); + } + + public Task GetAsync(long id) + { + this._db.TryGetValue(id, out var item); + return Task.FromResult(item); + } + + public Task FindAsync(Expression> predicate) + { + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + var result = this._db.Values.AsQueryable().FirstOrDefault(predicate); + return Task.FromResult(result); + } + + public Task AddAsync(long homeworkTaskId, long ltiTaskId) + { + this._db.TryAdd(homeworkTaskId, ltiTaskId); + return Task.CompletedTask; + } + + public Task DeleteAsync(long homeworkTaskId) + { + this._db.TryRemove(homeworkTaskId, out _); + return Task.CompletedTask; + } + + public Task UpdateAsync(long homeworkTaskId, long ltiTaskId) + { + if (!this._db.TryGetValue(homeworkTaskId, out var current)) + { + throw new KeyNotFoundException($"Mapping for HomeworkTaskId={homeworkTaskId} not found."); + } + + this._db[homeworkTaskId] = ltiTaskId; + return Task.CompletedTask; + } + } +} \ No newline at end of file From e0ce973c8cafca3c2e594992867cc12b80ef52e3 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Fri, 5 Dec 2025 19:54:45 +0300 Subject: [PATCH 04/26] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=D0=B0=20Lti?= =?UTF-8?q?Tool=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B8=20=D0=BA=D1=83=D1=80=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HwProj.APIGateway.API.csproj | 1 - .../Lti/Controllers/LtiToolsController.cs | 35 ++ .../Lti/Models/LtiToolConfig.cs | 8 + .../Lti/Models/LtiToolDto.cs | 8 + .../Lti/Services/ILtiToolService.cs | 11 + .../Lti/Services/LtiToolService.cs | 33 ++ .../HwProj.APIGateway.API/Startup.cs | 5 + .../HwProj.APIGateway.API/appsettings.json | 6 +- .../HwProj.Repositories/ReadOnlyRepository.cs | 2 +- hwproj.front/package-lock.json | 83 ++--- hwproj.front/src/api/ApiSingleton.ts | 11 +- hwproj.front/src/api/api.ts | 349 ++++++++++++++++++ .../src/components/Courses/AddCourseInfo.tsx | 34 ++ .../src/components/Courses/CreateCourse.tsx | 20 +- .../components/Courses/ICreateCourseState.tsx | 5 +- 15 files changed, 552 insertions(+), 59 deletions(-) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj index 2a070810a..3f1953e36 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj +++ b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj @@ -29,7 +29,6 @@ - diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs new file mode 100644 index 000000000..3f533c501 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.Services; +using HwProj.Models.AuthService.DTO; +using Microsoft.AspNetCore.Mvc; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/lti/tools")] +[ApiController] +public class LtiToolsController(ILtiToolService toolService) : ControllerBase +{ + [HttpGet] + [ProducesResponseType(typeof(IReadOnlyList), (int)HttpStatusCode.OK)] + public async Task>> GetAll() + { + var tools = await toolService.GetAllAsync(); + return Ok(tools); + } + + [HttpGet("{id:long}")] + [ProducesResponseType(typeof(LtiToolDto), (int)HttpStatusCode.OK)] + public async Task> Get(long id) + { + var tool = await toolService.GetByIdAsync(id); + if (tool == null) + { + return NotFound(); + } + + return Ok(tool); + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs new file mode 100644 index 000000000..5f37fb156 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs @@ -0,0 +1,8 @@ +namespace HwProj.APIGateway.API.Lti.Models +{ + public class LtiToolConfig() + { + public long Id { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs new file mode 100644 index 000000000..f8539b561 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs @@ -0,0 +1,8 @@ +namespace HwProj.APIGateway.API.Lti.Models +{ + public class LtiToolDto(long id, string name) + { + public long Id { get; init; } = id; + public string Name { get; init; } = name; + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs new file mode 100644 index 000000000..3c4561f02 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Models; + +namespace HwProj.APIGateway.API.Lti.Services; + +public interface ILtiToolService +{ + Task> GetAllAsync(); + Task GetByIdAsync(long id); +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs new file mode 100644 index 000000000..7d37b5d39 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Models; +using Microsoft.Extensions.Options; + +namespace HwProj.APIGateway.API.Lti.Services; + +public class LtiToolService(IOptions> options) : ILtiToolService +{ + private readonly IReadOnlyList _tools = (options.Value ?? []).AsReadOnly(); + + public Task> GetAllAsync() + { + var result = _tools + .Select(t => new LtiToolDto(t.Id, t.Name)) + .ToList() + .AsReadOnly(); + + return Task.FromResult>(result); + } + + public Task GetByIdAsync(long id) + { + var cfg = _tools.FirstOrDefault(t => t.Id == id); + if (cfg == null) + return Task.FromResult(null); + + var dto = new LtiToolDto(cfg.Id, cfg.Name); + + return Task.FromResult(dto); + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index ec49a65cd..b6625a5a8 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -2,6 +2,8 @@ using System.Text; using System.Text.Json.Serialization; using HwProj.APIGateway.API.Filters; +using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.Services; using HwProj.AuthService.Client; using HwProj.ContentService.Client; using HwProj.CoursesService.Client; @@ -79,6 +81,9 @@ public void ConfigureServices(IServiceCollection services) services.AddNotificationsServiceClient(); services.AddContentServiceClient(); + services.Configure>(Configuration.GetSection("LtiTools")); + services.AddSingleton(); + services.AddScoped(); } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json index b3e146a09..5aa565914 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json +++ b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json @@ -15,5 +15,9 @@ "LdapHost": "ad.pu.ru", "LdapPort": 389, "SearchBase": "DC=ad,DC=pu,DC=ru" - } + }, + "LtiTools": [ + { "id": 1, "name": "Miminet" }, + { "id": 2, "name": "Инструмент 2" } + ] } diff --git a/HwProj.Common/HwProj.Repositories/ReadOnlyRepository.cs b/HwProj.Common/HwProj.Repositories/ReadOnlyRepository.cs index b8063b384..677914ba6 100644 --- a/HwProj.Common/HwProj.Repositories/ReadOnlyRepository.cs +++ b/HwProj.Common/HwProj.Repositories/ReadOnlyRepository.cs @@ -17,7 +17,7 @@ public ReadOnlyRepository(DbContext context) Context = context; } - public IQueryable GetAll() + public IQueryable GetAll() { return Context.Set().AsNoTracking(); } diff --git a/hwproj.front/package-lock.json b/hwproj.front/package-lock.json index 86b29a013..77485cf23 100644 --- a/hwproj.front/package-lock.json +++ b/hwproj.front/package-lock.json @@ -4975,6 +4975,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.16.tgz", "integrity": "sha512-p3DqQi+8QRL5k7jXhXmJZLsE/GqHqyY6PcoA1oNTJr0try48uhTGUOYkgzmqtDaa/qPFO5LP+xCPzZXckGtquQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/api": "6.5.16", @@ -5002,12 +5003,14 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/api": { "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.5.16.tgz", "integrity": "sha512-HOsuT8iomqeTMQJrRx5U8nsC7lJTwRr1DhdD0SzlqL4c80S/7uuCy4IZvOt4sYQjOzW5fOo/kamcoBXyLproTA==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/channels": "6.5.16", @@ -5041,6 +5044,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/builder-webpack4": { @@ -5439,6 +5443,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.5.16.tgz", "integrity": "sha512-VylzaWQZaMozEwZPJdyJoz+0jpDa8GRyaqu9TGG6QGv+KU5POoZaGLDkRE7TzWkyyP0KQLo80K99MssZCpgSeg==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2", @@ -5498,6 +5503,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.5.16.tgz", "integrity": "sha512-pxcNaCj3ItDdicPTXTtmYJE3YC1SjxFrBmHcyrN+nffeNyiMuViJdOOZzzzucTUG0wcOOX8jaSyak+nnHg5H1Q==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2", @@ -5512,6 +5518,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.5.16.tgz", "integrity": "sha512-LzBOFJKITLtDcbW9jXl0/PaG+4xAz25PK8JxPZpIALbmOpYWOAPcO6V9C2heX6e6NgWFMUxjplkULEk9RCQMNA==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -5536,6 +5543,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/core": { @@ -5702,6 +5710,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.5.16.tgz", "integrity": "sha512-qMZQwmvzpH5F2uwNUllTPg6eZXr2OaYZQRRN8VZJiuorZzDNdAFmiVWMWdkThwmyLEJuQKXxqCL8lMj/7PPM+g==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2" @@ -5802,6 +5811,7 @@ "version": "0.0.2--canary.4566f4d.1", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.2--canary.4566f4d.1.tgz", "integrity": "sha512-9OVvMVh3t9znYZwb0Svf/YQoxX2gVOeQTGe2bses2yj+a3+OJnCrUF3/hGv6Em7KujtOdL2LL+JnG49oMVGFgQ==", + "dev": true, "license": "MIT", "dependencies": { "lodash": "^4.17.15" @@ -6274,6 +6284,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.5.16.tgz", "integrity": "sha512-ZgeP8a5YV/iuKbv31V8DjPxlV4AzorRiR8OuSt/KqaiYXNXlOoQDz/qMmiNcrshrfLpmkzoq7fSo4T8lWo2UwQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -6295,12 +6306,14 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz", "integrity": "sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==", + "dev": true, "license": "ISC", "dependencies": { "core-js": "^3.6.5", @@ -6317,6 +6330,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -6330,6 +6344,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -6342,6 +6357,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -6357,6 +6373,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -6440,6 +6457,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.5.16.tgz", "integrity": "sha512-hNLctkjaYLRdk1+xYTkC1mg4dYz2wSv6SqbLpcKMbkPHTE0ElhddGPHQqB362md/w9emYXNkt1LSMD8Xk9JzVQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -6460,6 +6478,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/ui": { @@ -7086,6 +7105,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.3.tgz", "integrity": "sha512-/CLhCW79JUeLKznI6mbVieGbl4QU5Hfn+6udw1YHZoofASjbQ5zaP5LzAUZYDpRYEjS4/P+DhEgyJ/PQmGGTWw==", + "dev": true, "license": "MIT" }, "node_modules/@types/isomorphic-fetch": { @@ -7454,6 +7474,7 @@ "version": "1.18.8", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.8.tgz", "integrity": "sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==", + "dev": true, "license": "MIT" }, "node_modules/@types/webpack-sources": { @@ -18001,6 +18022,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "dev": true, "license": "MIT" }, "node_modules/is-generator-function": { @@ -18123,6 +18145,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -18200,6 +18223,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -18671,13 +18695,6 @@ "node": ">= 10.13.0" } }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT", - "peer": true - }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -19317,6 +19334,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", + "dev": true, "license": "MIT" }, "node_modules/map-visit": { @@ -20509,6 +20527,7 @@ "version": "1.11.3", "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", + "dev": true, "license": "MIT", "dependencies": { "map-or-similar": "^1.5.0" @@ -20565,21 +20584,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -22490,6 +22494,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -22692,6 +22697,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -22844,18 +22850,6 @@ "node": ">=6" } }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/portable-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/portable-fetch/-/portable-fetch-3.0.0.tgz", @@ -27150,6 +27144,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -28026,6 +28021,7 @@ "version": "2.14.4", "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.4.tgz", "integrity": "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==", + "dev": true, "license": "MIT" }, "node_modules/stream-browserify": { @@ -28659,6 +28655,7 @@ "version": "6.0.8", "resolved": "https://registry.npmjs.org/telejson/-/telejson-6.0.8.tgz", "integrity": "sha512-nerNXi+j8NK1QEfBHtZUN/aLdDcyupA//9kAboYLrtzZlPLpUfqbVGWb9zz91f/mIjRbAYhbgtnJHY8I1b5MBg==", + "dev": true, "license": "MIT", "dependencies": { "@types/is-function": "^1.0.0", @@ -28675,6 +28672,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29196,6 +29194,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.10" @@ -30088,6 +30087,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/util.promisify": { @@ -31943,21 +31943,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/hwproj.front/src/api/ApiSingleton.ts b/hwproj.front/src/api/ApiSingleton.ts index df3527ba0..43d417587 100644 --- a/hwproj.front/src/api/ApiSingleton.ts +++ b/hwproj.front/src/api/ApiSingleton.ts @@ -8,7 +8,8 @@ import { TasksApi, StatisticsApi, SystemApi, - FilesApi + FilesApi, + LtiToolsApi, } from "."; import AuthService from "../services/AuthService"; import CustomFilesApi from "./CustomFilesApi"; @@ -27,6 +28,7 @@ class Api { readonly authService: AuthService; readonly customFilesApi: CustomFilesApi; readonly filesApi: FilesApi; + readonly ltiToolsApi: LtiToolsApi; constructor( accountApi: AccountApi, @@ -40,7 +42,8 @@ class Api { systemApi: SystemApi, authService: AuthService, customFilesApi: CustomFilesApi, - filesApi: FilesApi + filesApi: FilesApi, + ltiToolsApi: LtiToolsApi ) { this.accountApi = accountApi; this.expertsApi = expertsApi; @@ -54,6 +57,7 @@ class Api { this.authService = authService; this.customFilesApi = customFilesApi; this.filesApi = filesApi; + this.ltiToolsApi = ltiToolsApi; } } @@ -86,6 +90,7 @@ ApiSingleton = new Api( new SystemApi({basePath: basePath}), authService, new CustomFilesApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), - new FilesApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}) + new FilesApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), + new LtiToolsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}) ); export default ApiSingleton; \ No newline at end of file diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 980dcc406..4e6ce693b 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -1489,6 +1489,25 @@ export interface LoginViewModel { */ rememberMe: boolean; } +/** + * + * @export + * @interface LtiToolDto + */ +export interface LtiToolDto { + /** + * + * @type {number} + * @memberof LtiToolDto + */ + id?: number; + /** + * + * @type {string} + * @memberof LtiToolDto + */ + name?: string; +} /** * * @export @@ -7382,6 +7401,336 @@ export class HomeworksApi extends BaseAPI { return HomeworksApiFp(this.configuration).homeworksUpdateHomework(homeworkId, body, options)(this.fetch, this.basePath); } +} +/** + * LtiAuthApi - fetch parameter creator + * @export + */ +export const LtiAuthApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {string} [issOfTheTool] + * @param {string} [clientId] + * @param {string} [targetLinkUri] + * @param {string} [state] + * @param {string} [nonce] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthAuthorizeLti(issOfTheTool?: string, clientId?: string, targetLinkUri?: string, state?: string, nonce?: string, loginHint?: string, ltiMessageHint?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/lti/launch/authorize`; + 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; + } + + if (issOfTheTool !== undefined) { + localVarQueryParameter['issOfTheTool'] = issOfTheTool; + } + + if (clientId !== undefined) { + localVarQueryParameter['clientId'] = clientId; + } + + if (targetLinkUri !== undefined) { + localVarQueryParameter['targetLinkUri'] = targetLinkUri; + } + + if (state !== undefined) { + localVarQueryParameter['state'] = state; + } + + if (nonce !== undefined) { + localVarQueryParameter['nonce'] = nonce; + } + + if (loginHint !== undefined) { + localVarQueryParameter['login_hint'] = loginHint; + } + + if (ltiMessageHint !== undefined) { + localVarQueryParameter['lti_message_hint'] = ltiMessageHint; + } + + 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, + }; + }, + } +}; + +/** + * LtiAuthApi - functional programming interface + * @export + */ +export const LtiAuthApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {string} [issOfTheTool] + * @param {string} [clientId] + * @param {string} [targetLinkUri] + * @param {string} [state] + * @param {string} [nonce] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthAuthorizeLti(issOfTheTool?: string, clientId?: string, targetLinkUri?: string, state?: string, nonce?: string, loginHint?: string, ltiMessageHint?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthAuthorizeLti(issOfTheTool, clientId, targetLinkUri, state, nonce, loginHint, ltiMessageHint, 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; + } + }); + }; + }, + } +}; + +/** + * LtiAuthApi - factory interface + * @export + */ +export const LtiAuthApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {string} [issOfTheTool] + * @param {string} [clientId] + * @param {string} [targetLinkUri] + * @param {string} [state] + * @param {string} [nonce] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthAuthorizeLti(issOfTheTool?: string, clientId?: string, targetLinkUri?: string, state?: string, nonce?: string, loginHint?: string, ltiMessageHint?: string, options?: any) { + return LtiAuthApiFp(configuration).ltiAuthAuthorizeLti(issOfTheTool, clientId, targetLinkUri, state, nonce, loginHint, ltiMessageHint, options)(fetch, basePath); + }, + }; +}; + +/** + * LtiAuthApi - object-oriented interface + * @export + * @class LtiAuthApi + * @extends {BaseAPI} + */ +export class LtiAuthApi extends BaseAPI { + /** + * + * @param {string} [issOfTheTool] + * @param {string} [clientId] + * @param {string} [targetLinkUri] + * @param {string} [state] + * @param {string} [nonce] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiAuthApi + */ + public ltiAuthAuthorizeLti(issOfTheTool?: string, clientId?: string, targetLinkUri?: string, state?: string, nonce?: string, loginHint?: string, ltiMessageHint?: string, options?: any) { + return LtiAuthApiFp(this.configuration).ltiAuthAuthorizeLti(issOfTheTool, clientId, targetLinkUri, state, nonce, loginHint, ltiMessageHint, options)(this.fetch, this.basePath); + } + +} +/** + * LtiToolsApi - fetch parameter creator + * @export + */ +export const LtiToolsApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiToolsGet(id: number, options: any = {}): FetchArgs { + // verify required parameter 'id' is not null or undefined + if (id === null || id === undefined) { + throw new RequiredError('id','Required parameter id was null or undefined when calling ltiToolsGet.'); + } + const localVarPath = `/api/lti/tools/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + 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; + } + + 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. + * @throws {RequiredError} + */ + ltiToolsGetAll(options: any = {}): FetchArgs { + const localVarPath = `/api/lti/tools`; + 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; + } + + 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, + }; + }, + } +}; + +/** + * LtiToolsApi - functional programming interface + * @export + */ +export const LtiToolsApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiToolsGet(id: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiToolsApiFetchParamCreator(configuration).ltiToolsGet(id, 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. + * @throws {RequiredError} + */ + ltiToolsGetAll(options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { + const localVarFetchArgs = LtiToolsApiFetchParamCreator(configuration).ltiToolsGetAll(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; + } + }); + }; + }, + } +}; + +/** + * LtiToolsApi - factory interface + * @export + */ +export const LtiToolsApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiToolsGet(id: number, options?: any) { + return LtiToolsApiFp(configuration).ltiToolsGet(id, options)(fetch, basePath); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiToolsGetAll(options?: any) { + return LtiToolsApiFp(configuration).ltiToolsGetAll(options)(fetch, basePath); + }, + }; +}; + +/** + * LtiToolsApi - object-oriented interface + * @export + * @class LtiToolsApi + * @extends {BaseAPI} + */ +export class LtiToolsApi extends BaseAPI { + /** + * + * @param {number} id + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiToolsApi + */ + public ltiToolsGet(id: number, options?: any) { + return LtiToolsApiFp(this.configuration).ltiToolsGet(id, options)(this.fetch, this.basePath); + } + + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiToolsApi + */ + public ltiToolsGetAll(options?: any) { + return LtiToolsApiFp(this.configuration).ltiToolsGetAll(options)(this.fetch, this.basePath); + } + } /** * NotificationsApi - fetch parameter creator diff --git a/hwproj.front/src/components/Courses/AddCourseInfo.tsx b/hwproj.front/src/components/Courses/AddCourseInfo.tsx index c0b98a1ae..e19df55ed 100644 --- a/hwproj.front/src/components/Courses/AddCourseInfo.tsx +++ b/hwproj.front/src/components/Courses/AddCourseInfo.tsx @@ -3,6 +3,7 @@ import { Grid, TextField, Button, Typography, + MenuItem, } from "@material-ui/core"; import {LoadingButton} from "@mui/lab"; import {IStepComponentProps} from "./ICreateCourseState"; @@ -112,6 +113,39 @@ const AddCourseInfo: FC = ({state, setState}) => { /> + + option.name ?? ""} + + value={ + state.ltiToolId == null + ? null + : state.ltiTools.find(t => t.id === state.ltiToolId) ?? null + } + + onChange={(_, newValue) => { + setState(prev => ({ + ...prev, + ltiToolId: newValue?.id ?? null + })); + }} + + renderInput={(params) => ( + + )} + fullWidth + /> + + {state.isGroupFromList && ( { selectedGroups: [], fetchingGroups: false, courseIsLoading: false, + ltiTools: [], + ltiToolId: null, }) const {activeStep, completedSteps, baseCourses, selectedBaseCourse} = state @@ -134,6 +136,7 @@ export const CreateCourse: FC = () => { isOpen: true, baseCourseId: selectedBaseCourse?.id, fetchStudents: state.isGroupFromList ? state.fetchStudents : false, + ltiToolId: state.ltiToolId, } try { setCourseIsLoading(true) @@ -151,15 +154,26 @@ export const CreateCourse: FC = () => { useEffect(() => { const loadData = async () => { try { - const userCourses = await ApiSingleton.coursesApi.coursesGetAllUserCourses(); + const [ + userCourses, + programResponse, + ltiToolsResponse, + ] = await Promise.all([ + ApiSingleton.coursesApi.coursesGetAllUserCourses(), + ApiSingleton.coursesApi.coursesGetProgramNames(), + ApiSingleton.ltiToolsApi.ltiToolsGetAll(), + ]); if (!userCourses.length) skipCurrentStep(); setBaseCourses(userCourses); - const programResponse = await ApiSingleton.coursesApi.coursesGetProgramNames(); const programNames = programResponse .map(model => model.programName) .filter((name): name is string => name !== undefined); - setState(prev => ({...prev, programNames})); + setState(prev => ({ + ...prev, + programNames, + ltiTools: ltiToolsResponse + })); } catch (e) { console.error("Error loading data:", e); setBaseCourses([]); diff --git a/hwproj.front/src/components/Courses/ICreateCourseState.tsx b/hwproj.front/src/components/Courses/ICreateCourseState.tsx index add096860..979c29f5e 100644 --- a/hwproj.front/src/components/Courses/ICreateCourseState.tsx +++ b/hwproj.front/src/components/Courses/ICreateCourseState.tsx @@ -1,5 +1,5 @@ import {Dispatch, SetStateAction} from "react" -import {CoursePreviewView} from "api"; +import {CoursePreviewView, LtiToolDto} from "api"; export enum CreateCourseStep { SelectBaseCourseStep = 0, @@ -33,6 +33,9 @@ export interface ICreateCourseState { fetchingGroups: boolean; courseIsLoading: boolean; + + ltiTools: LtiToolDto[]; + ltiToolId: number | null; } export interface IStepComponentProps { From 6dda56835b05881a44b9ba0b61ad7be696f3a052 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Sun, 4 Jan 2026 02:37:35 +0300 Subject: [PATCH 05/26] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D1=87=D1=82=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20deeplinking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HwProj.APIGateway.API.csproj | 1 + .../Lti/Controllers/JwksController.cs | 56 ++++++ .../Lti/Controllers/LtiAuthController.cs | 189 +++++++++++++++--- .../LtiDeepLinkingReturnController.cs | 77 +++++++ .../Lti/Controllers/LtiToolsController.cs | 1 - .../Lti/Models/AuthorizePostFormDto.cs | 10 + .../Lti/Models/LtiDeepLinkingContentItem.cs | 9 + .../Lti/Models/LtiPlatformConfig.cs | 16 ++ .../Lti/Models/LtiToolConfig.cs | 4 + .../Lti/Models/LtiToolDto.cs | 12 +- .../Lti/Services/ILtiTokenService.cs | 11 +- .../Lti/Services/LtiOptions.cs | 9 - .../Lti/Services/LtiTokenService.cs | 103 +++++----- .../Lti/Services/LtiToolService.cs | 16 +- .../HwProj.APIGateway.API/Startup.cs | 3 + .../HwProj.APIGateway.API/appsettings.json | 23 --- .../Models/CourseContext.cs | 6 + .../Models/HomeworkTaskLtiUrl.cs | 15 ++ .../Repositories/ILtiTaskRepository.cs | 21 -- .../Repositories/LtiTaskRepository.cs | 68 ------- hwproj.front/src/api/ApiSingleton.ts | 9 +- hwproj.front/src/api/api.ts | 161 ++++++++++++--- .../components/Courses/CourseExperimental.tsx | 33 ++- .../Tasks/CourseTaskExperimental.tsx | 5 +- .../src/components/Tasks/LtiImportButton.tsx | 133 ++++++++++++ 25 files changed, 742 insertions(+), 249 deletions(-) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/AuthorizePostFormDto.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiDeepLinkingContentItem.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs delete mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiOptions.cs delete mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ILtiTaskRepository.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Repositories/LtiTaskRepository.cs create mode 100644 hwproj.front/src/components/Tasks/LtiImportButton.tsx diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj index 3f1953e36..fbb5ef506 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj +++ b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj @@ -30,5 +30,6 @@ + diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs new file mode 100644 index 000000000..2e61fb688 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs @@ -0,0 +1,56 @@ +using System.Security.Cryptography; +using HwProj.APIGateway.API.Lti.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/lti")] +[ApiController] +public class JwksController(IOptions options) : ControllerBase +{ + private readonly LtiPlatformConfig _config = options.Value; + + [HttpGet("jwks")] + [AllowAnonymous] + public IActionResult GetJwks() + { + var keyConfig = _config.SigningKey; + + if (string.IsNullOrEmpty(keyConfig?.PrivateKeyPem)) + { + return StatusCode(500, "Signing key is not configured."); + } + + using var rsa = RSA.Create(); + try + { + rsa.ImportFromPem(keyConfig.PrivateKeyPem); + } + catch (CryptographicException) + { + return StatusCode(500, "Invalid Private Key format in configuration."); + } + + var publicParams = rsa.ExportParameters(false); + + var securityKey = new RsaSecurityKey(publicParams) + { + KeyId = keyConfig.KeyId + }; + + var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(securityKey); + + jwk.Use = "sig"; + jwk.Alg = "RS256"; + + var jwks = new + { + keys = new[] { jwk } + }; + + return Ok(jwks); + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs index 291367d40..130cd0384 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs @@ -1,63 +1,189 @@ +using System.Collections.Generic; using System.Net; +using System.Security.Claims; +using System.Text.Json; using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.Services; using HwProj.APIGateway.API.LTI.Services; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; namespace HwProj.APIGateway.API.Lti.Controllers; -[Route("api/lti/launch")] +[Route("api/lti")] [ApiController] -public class LtiAuthController(ILtiTokenService tokenService) : ControllerBase +public class LtiAuthController( + IOptions ltiPlatformOptions, + ILtiToolService toolService, + ILtiTokenService tokenService, + IDataProtectionProvider provider + ) + : ControllerBase { - private readonly ILtiTokenService _tokenService = tokenService; + private readonly IDataProtector protector = provider.CreateProtector("LtiPlatform.MessageHint.v1"); // Tool редиректит сюда браузер (шаг "redirect browser to Platform for Auth") [HttpPost("authorize")] - [Authorize] // пользователь должен быть залогинен в LMS + [Authorize] public async Task AuthorizeLti( - [FromQuery] string issOfTheTool, - [FromQuery] string clientId, - [FromQuery] string targetLinkUri, - [FromQuery] string state, - [FromQuery] string nonce, - [FromQuery(Name = "login_hint")] string loginId, + [FromQuery(Name = "client_id")] string clientId, + [FromQuery(Name = "redirect_uri")] string redirectUri, + [FromQuery(Name = "state")] string state, + [FromQuery(Name = "nonce")] string nonce, [FromQuery(Name = "lti_message_hint")] string ltiMessageHint) { - // 1. ПРОВЕРКА ЗАПРОСА (validate request) - if (!(await this.CheckTheRequest(issOfTheTool, clientId, targetLinkUri, loginId))) + LtiHintPayload payload; + try { - return BadRequest(); + var json = this.protector.Unprotect(ltiMessageHint); + payload = JsonSerializer.Deserialize(json); + } + catch + { + return BadRequest("Invalid or expired lti_message_hint"); } - // 2. СОЗДАНИЕ id_token (LTI JWT) - var idToken = _tokenService.CreateLtiIdToken( - user: User, - clientId: clientId, - redirectUri: targetLinkUri, - nonce: nonce, - ltiMessageHint: ltiMessageHint); + var tool = await toolService.GetByIdAsync(long.Parse(payload.ToolId)); + if (tool == null) + { + return BadRequest("Tool not found"); + } - // 3. ВОЗВРАТ auth response (redirect auth response to tool) - var html = $""" + if (tool.ClientId != clientId) + { + return BadRequest($"Invalid client_id. Expected: {tool.ClientId}, Got: {clientId}"); + } + + string idToken; + if (payload.Type == "DeepLinking") + { + idToken = tokenService.CreateDeepLinkingToken( + clientId: clientId, + toolId: payload.ToolId, + courseId: payload.CourseId, + targetLinkUri: tool.DeepLink, + userId: payload.UserId, + nonce: nonce + ); + } + else + { + // (Логика для обычного запуска, пока опустим) + idToken = tokenService.CreateDeepLinkingToken( + clientId: clientId, + courseId: payload.CourseId, + toolId: payload.ToolId, + targetLinkUri: tool.DeepLink, + userId: payload.UserId, + nonce: nonce + ); + } + var html = $""" -
+ -
"""; + return Content(html, "text/html"); } + [HttpGet("start")] + [Authorize] + public async Task StartLti( + [FromQuery] string? resourceLinkId, + [FromQuery] string? courseId, + [FromQuery] string? toolId, + [FromQuery] bool isDeepLink = false) + { + var userId = User.FindFirstValue("_id"); + if (userId == null) + { + return Unauthorized("User ID not found"); + } + + LtiToolDto? tool; + string targetUrl; + LtiHintPayload payload; + + if (isDeepLink) + { + if (courseId == null || toolId == null) + { + return BadRequest("For Deep Linking, courseId and toolId are required."); + } + + tool = await toolService.GetByIdAsync(long.Parse(toolId)); + if (tool == null) + { + return NotFound("Tool not found"); + } + + targetUrl = !string.IsNullOrEmpty(tool.DeepLink) + ? tool.DeepLink + : tool.LaunchUrl; + + payload = new LtiHintPayload + { + Type = "DeepLinking", + UserId = userId, + CourseId = courseId, + ToolId = toolId + }; + } + else if (!string.IsNullOrEmpty(resourceLinkId)) + { + // Здесь логика поиска тула может быть сложнее (через LinkService) + tool = await toolService.GetByIdAsync(1); + + if (tool == null) + { + return NotFound("Tool not found"); + } + + targetUrl = tool.LaunchUrl; + + payload = new LtiHintPayload + { + Type = "ResourceLink", + UserId = userId, + ResourceLinkId = resourceLinkId + }; + } + else + { + return BadRequest("Either resourceLinkId OR (isDeepLink + courseId + toolId) must be provided."); + } + + var json = JsonSerializer.Serialize(payload); + var messageHint = this.protector.Protect(json); + + var dto = new AuthorizePostFormDto() + { + ActionUrl = tool.InitiateLoginUri, + Method = "POST", + Fields = new Dictionary + { + ["iss"] = ltiPlatformOptions.Value.Issuer, + ["login_hint"] = userId, + ["target_link_uri"] = targetUrl, + ["lti_message_hint"] = messageHint, + ["client_id"] = "MyPlatformClientId" + } + }; + + return Ok(dto); + } + private async Task CheckTheRequest( string issOfTheTool, string clientId, @@ -70,4 +196,13 @@ private async Task CheckTheRequest( // - можешь сверить login_hint с текущим пользователем и т.д. return true; } + + private class LtiHintPayload + { + public string Type { get; set; } + public string UserId { get; set; } + public string? ResourceLinkId { get; set; } + public string? CourseId { get; set; } + public string? ToolId { get; set; } + } } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs new file mode 100644 index 000000000..721155217 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs @@ -0,0 +1,77 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Text.Json; +using LtiAdvantage.DeepLinking; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/lti")] +[ApiController] +public class LtiDeepLinkingReturnController : ControllerBase +{ + [HttpPost("deepLinkReturn")] + [AllowAnonymous] + public IActionResult OnDeepLinkingReturn([FromForm] IFormCollection form) + { + if (!form.ContainsKey("JWT")) + { + return BadRequest("Missing JWT parameter"); + } + + string tokenString = form["JWT"]!; + var handler = new JwtSecurityTokenHandler(); + + if (!handler.CanReadToken(tokenString)) + { + return BadRequest("Invalid JWT format"); + } + + // (добавить валидацию подписи) + var jwtToken = handler.ReadJwtToken(tokenString); + + var ltiResponse = new LtiDeepLinkingResponse(jwtToken.Payload); + + var items = ltiResponse.ContentItems; + + if (items == null || items.Length == 0) + { + return Content("", "text/html"); + } + + var payloadList = items.Select(item => new + { + title = !string.IsNullOrEmpty(item.Title) ? item.Title : "External Resource", + url = item.Url, + text = item.Text ?? "" + }).ToList(); + + var payloadJson = JsonSerializer.Serialize(payloadList); + + var htmlResponse = $@" + + + + + + "; + + return Content(htmlResponse, "text/html"); + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs index 3f533c501..00f0c6b52 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using HwProj.APIGateway.API.Lti.Models; using HwProj.APIGateway.API.Lti.Services; -using HwProj.Models.AuthService.DTO; using Microsoft.AspNetCore.Mvc; namespace HwProj.APIGateway.API.Lti.Controllers; diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/AuthorizePostFormDto.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/AuthorizePostFormDto.cs new file mode 100644 index 000000000..40cfed0d6 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/AuthorizePostFormDto.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace HwProj.APIGateway.API.Lti.Models; + +public class AuthorizePostFormDto +{ + public string ActionUrl { get; set; } + public string Method { get; set; } = "POST"; + public Dictionary Fields { get; set; } = new(); +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiDeepLinkingContentItem.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiDeepLinkingContentItem.cs new file mode 100644 index 000000000..44dc95a79 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiDeepLinkingContentItem.cs @@ -0,0 +1,9 @@ +namespace HwProj.APIGateway.API.Lti.Models; + +public class LtiDeepLinkingContentItem +{ + public string Type { get; set; } // "ltiResourceLink" + public string Url { get; set; } // Ссылка на запуск (Launch URL) + public string Title { get; set; } // Название задачи + public string Text { get; set; } // Описание +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs new file mode 100644 index 000000000..d72828ffb --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs @@ -0,0 +1,16 @@ +namespace HwProj.APIGateway.API.Lti.Models; + +public class LtiPlatformConfig +{ + public string Issuer { get; set; } + public string OidcAuthorizationEndpoint { get; set; } + public string DeepLinkReturnUrl { get; set; } + public string JwksEndpoint { get; set; } + public LtiSigningKeyConfig SigningKey { get; set; } +} + +public class LtiSigningKeyConfig +{ + public string KeyId { get; set; } + public string PrivateKeyPem { get; set; } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs index 5f37fb156..5c7efd115 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs @@ -4,5 +4,9 @@ public class LtiToolConfig() { public long Id { get; set; } public string Name { get; set; } + public string ClientId { get; set; } + public string InitiateLoginUri { get; set; } + public string LaunchUrl { get; set; } + public string DeepLink { get; set; } } } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs index f8539b561..6f34f58c1 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs @@ -1,8 +1,18 @@ namespace HwProj.APIGateway.API.Lti.Models { - public class LtiToolDto(long id, string name) + public class LtiToolDto( + long id, + string name, + string clientId, + string initiateLoginUri, + string launchUrl, + string deepLink) { public long Id { get; init; } = id; public string Name { get; init; } = name; + public string ClientId { get; init; } = clientId; + public string InitiateLoginUri { get; init; } = initiateLoginUri; + public string LaunchUrl { get; init; } = launchUrl; + public string DeepLink { get; init; } = deepLink; } } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs index d47e8ea0b..4b4832174 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs @@ -5,10 +5,11 @@ namespace HwProj.APIGateway.API.LTI.Services; public interface ILtiTokenService { - string CreateLtiIdToken( - ClaimsPrincipal user, + string CreateDeepLinkingToken( string clientId, - string redirectUri, - string nonce, - string ltiMessageHint); + string toolId, + string courseId, + string targetLinkUri, + string userId, + string nonce); } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiOptions.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiOptions.cs deleted file mode 100644 index f90030bb1..000000000 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace HwProj.APIGateway.API.LTI.Services; - -public class LtiOptions -{ - public string? Issuer { get; set; } - public string? DeploymentId { get; set; } - public string? SigningKey { get; set; } - public int TokenLifetimeMinutes { get; set; } = 5; -} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs index 5cc080960..be71bcbb8 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs @@ -1,74 +1,77 @@ using System; using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; +using System.Security.Cryptography; +using HwProj.APIGateway.API.Lti.Models; using HwProj.APIGateway.API.LTI.Services; +using LtiAdvantage.DeepLinking; using LtiAdvantage.Lti; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace HwProj.APIGateway.API.Lti.Services; -public class LtiTokenService(IOptions options) : ILtiTokenService +public class LtiTokenService(IOptions options) : ILtiTokenService { - private readonly LtiOptions _options = options.Value; - private readonly JwtSecurityTokenHandler _tokenHandler = new(); + private readonly LtiPlatformConfig _options = options.Value; - public string CreateLtiIdToken( - ClaimsPrincipal user, + public string CreateDeepLinkingToken( string clientId, - string redirectUri, - string nonce, - string ltiMessageHint) + string toolId, + string courseId, + string targetLinkUri, + string userId, + string nonce) { - // время жизни токена - var now = DateTime.UtcNow; - var expires = now.AddMinutes(this._options.TokenLifetimeMinutes); - - var signingKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(this._options.SigningKey)); - var signingCredentials = new SigningCredentials( - signingKey, - SecurityAlgorithms.RsaSha256); - - ContextClaimValueType context = new(); - /*context.Id = ; - context.Label = ; - context.Title = ; - context.Type = ; */ - - ResourceLinkClaimValueType resourceLink = new(); - /*resourceLink.Id = ; - resourceLink.Description = ; - resourceLink.Title = ;*/ - - // ??? - LaunchPresentationClaimValueType launchPresentation = new(); - - var request = new LtiResourceLinkRequest + var request = new LtiDeepLinkingRequest { - DeploymentId = _options.DeploymentId, + DeploymentId = toolId, Nonce = nonce, - // обязательные LTI claims: - Roles = [Role.InstitutionStudent], - Context = context, - ResourceLink = resourceLink, - TargetLinkUri = redirectUri, - // opaque user id внутри платформы - UserId = user.FindFirstValue(ClaimTypes.NameIdentifier) ?? "unknown-user", - LaunchPresentation = launchPresentation + UserId = userId, + TargetLinkUri = targetLinkUri, + Roles = [Role.ContextInstructor], + + Context = new ContextClaimValueType + { + Id = courseId + }, + + DeepLinkingSettings = new DeepLinkingSettingsClaimValueType + { + AutoCreate = true, + AcceptMultiple = true, + AcceptTypes = ["ltiResourceLink"], + AcceptPresentationDocumentTargets = [DocumentTarget.Window], + + DeepLinkReturnUrl = this._options.DeepLinkReturnUrl, + } }; - // создаём сам JWT + var now = DateTime.UtcNow; var jwt = new JwtSecurityToken( - issuer: _options.Issuer, + issuer: this._options.Issuer, audience: clientId, claims: request.IssuedClaims, notBefore: now, - expires: expires, - signingCredentials: signingCredentials); + expires: now.AddMinutes(5), + signingCredentials: GetSigningCredentials() + ); + + return new JwtSecurityTokenHandler().WriteToken(jwt); + } + + private SigningCredentials GetSigningCredentials() + { + var keyConfig = _options.SigningKey; + + var rsa = RSA.Create(); + + rsa.ImportFromPem(keyConfig.PrivateKeyPem); + + var securityKey = new RsaSecurityKey(rsa) + { + KeyId = keyConfig.KeyId + }; - var token = _tokenHandler.WriteToken(jwt); - return token; + return new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256); } } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs index 7d37b5d39..d3ed5f16d 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs @@ -13,7 +13,13 @@ public class LtiToolService(IOptions> options) : ILtiToolSer public Task> GetAllAsync() { var result = _tools - .Select(t => new LtiToolDto(t.Id, t.Name)) + .Select(t => new LtiToolDto( + t.Id, + t.Name, + t.ClientId, + t.InitiateLoginUri, + t.LaunchUrl, + t.DeepLink)) .ToList() .AsReadOnly(); @@ -26,7 +32,13 @@ public Task> GetAllAsync() if (cfg == null) return Task.FromResult(null); - var dto = new LtiToolDto(cfg.Id, cfg.Name); + var dto = new LtiToolDto( + cfg.Id, + cfg.Name, + cfg.ClientId, + cfg.InitiateLoginUri, + cfg.LaunchUrl, + cfg.DeepLink); return Task.FromResult(dto); } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index b6625a5a8..a9400eab7 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -4,6 +4,7 @@ using HwProj.APIGateway.API.Filters; using HwProj.APIGateway.API.Lti.Models; using HwProj.APIGateway.API.Lti.Services; +using HwProj.APIGateway.API.LTI.Services; using HwProj.AuthService.Client; using HwProj.ContentService.Client; using HwProj.CoursesService.Client; @@ -81,8 +82,10 @@ public void ConfigureServices(IServiceCollection services) services.AddNotificationsServiceClient(); services.AddContentServiceClient(); + services.Configure(Configuration.GetSection("LtiPlatform")); services.Configure>(Configuration.GetSection("LtiTools")); services.AddSingleton(); + services.AddSingleton(); services.AddScoped(); } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json deleted file mode 100644 index 5aa565914..000000000 --- a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "Services": { - "Auth": "http://localhost:5001", - "Courses": "http://localhost:5002", - "Notifications": "http://localhost:5006", - "Solutions": "http://localhost:5007", - "Content": "http://localhost:5008" - }, - "Security": { - "SecurityKey": "U8_.wpvk93fPWG CourseFilters { get; set; } public DbSet UserToCourseFilters { get; set; } public DbSet Questions { get; set; } + public DbSet TaskLtiUrls { get; set; } public CourseContext(DbContextOptions options) : base(options) @@ -27,6 +28,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasIndex(a => a.CourseId); modelBuilder.Entity().HasKey(u => new { u.CourseId, u.UserId }); modelBuilder.Entity().HasIndex(t => t.TaskId); + modelBuilder.Entity() + .HasOne() + .WithOne() + .HasForeignKey(u => u.TaskId) + .OnDelete(DeleteBehavior.Cascade); } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs new file mode 100644 index 000000000..8ebde9143 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace HwProj.CoursesService.API.Models +{ + public class HomeworkTaskLtiUrl + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public long TaskId { get; set; } + + [Required] + public string LtiLaunchUrl { get; set; } + } +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ILtiTaskRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ILtiTaskRepository.cs deleted file mode 100644 index eb0bcec8d..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ILtiTaskRepository.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; -using HwProj.CoursesService.API.Models; -using HwProj.Repositories; - -namespace HwProj.CoursesService.API.Repositories -{ - public interface ILtiTaskRepository - { - Task> GetAllAsync(); - Task> FindAlAsync(Expression> predicate); - Task GetAsync(long id); - Task FindAsync(Expression> predicate); - Task AddAsync(long homeworkTaskId, long ltiTaskId); - Task DeleteAsync(long id); - Task UpdateAsync(long homeworkTaskId, long ltiTaskId); - } -} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/LtiTaskRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/LtiTaskRepository.cs deleted file mode 100644 index 47d0b3cc0..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/LtiTaskRepository.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; -using HwProj.CoursesService.API.Models; -using HwProj.Repositories; - -namespace HwProj.CoursesService.API.Repositories -{ - public class LtiTaskRepository : ILtiTaskRepository - { - private readonly ConcurrentDictionary _db = - new ConcurrentDictionary(); - - public Task> GetAllAsync() - { - return Task.FromResult(this._db.Values.AsQueryable()); - } - - public Task> FindAlAsync(Expression>? predicate) - { - return predicate == null - ? throw new ArgumentNullException(nameof(predicate)) - : Task.FromResult(this._db.Values.AsQueryable().Where(predicate)); - } - - public Task GetAsync(long id) - { - this._db.TryGetValue(id, out var item); - return Task.FromResult(item); - } - - public Task FindAsync(Expression> predicate) - { - if (predicate == null) - { - throw new ArgumentNullException(nameof(predicate)); - } - var result = this._db.Values.AsQueryable().FirstOrDefault(predicate); - return Task.FromResult(result); - } - - public Task AddAsync(long homeworkTaskId, long ltiTaskId) - { - this._db.TryAdd(homeworkTaskId, ltiTaskId); - return Task.CompletedTask; - } - - public Task DeleteAsync(long homeworkTaskId) - { - this._db.TryRemove(homeworkTaskId, out _); - return Task.CompletedTask; - } - - public Task UpdateAsync(long homeworkTaskId, long ltiTaskId) - { - if (!this._db.TryGetValue(homeworkTaskId, out var current)) - { - throw new KeyNotFoundException($"Mapping for HomeworkTaskId={homeworkTaskId} not found."); - } - - this._db[homeworkTaskId] = ltiTaskId; - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/hwproj.front/src/api/ApiSingleton.ts b/hwproj.front/src/api/ApiSingleton.ts index 43d417587..3d2113b13 100644 --- a/hwproj.front/src/api/ApiSingleton.ts +++ b/hwproj.front/src/api/ApiSingleton.ts @@ -10,6 +10,7 @@ import { SystemApi, FilesApi, LtiToolsApi, + LtiAuthApi, } from "."; import AuthService from "../services/AuthService"; import CustomFilesApi from "./CustomFilesApi"; @@ -29,6 +30,7 @@ class Api { readonly customFilesApi: CustomFilesApi; readonly filesApi: FilesApi; readonly ltiToolsApi: LtiToolsApi; + readonly ltiAuthApi: LtiAuthApi; constructor( accountApi: AccountApi, @@ -43,7 +45,8 @@ class Api { authService: AuthService, customFilesApi: CustomFilesApi, filesApi: FilesApi, - ltiToolsApi: LtiToolsApi + ltiToolsApi: LtiToolsApi, + ltiAuthApi: LtiAuthApi ) { this.accountApi = accountApi; this.expertsApi = expertsApi; @@ -58,6 +61,7 @@ class Api { this.customFilesApi = customFilesApi; this.filesApi = filesApi; this.ltiToolsApi = ltiToolsApi; + this.ltiAuthApi = ltiAuthApi; } } @@ -91,6 +95,7 @@ ApiSingleton = new Api( authService, new CustomFilesApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), new FilesApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), - new LtiToolsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}) + new LtiToolsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), + new LtiAuthApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}) ); export default ApiSingleton; \ No newline at end of file diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 4e6ce693b..153dc6719 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -1507,6 +1507,24 @@ export interface LtiToolDto { * @memberof LtiToolDto */ name?: string; + /** + * + * @type {string} + * @memberof LtiToolDto + */ + initiateLoginUri?: string; + /** + * + * @type {string} + * @memberof LtiToolDto + */ + launchUrl?: string; + /** + * + * @type {string} + * @memberof LtiToolDto + */ + deepLink?: string; } /** * @@ -7410,18 +7428,16 @@ export const LtiAuthApiFetchParamCreator = function (configuration?: Configurati return { /** * - * @param {string} [issOfTheTool] * @param {string} [clientId] - * @param {string} [targetLinkUri] + * @param {string} [redirectUri] * @param {string} [state] * @param {string} [nonce] - * @param {string} [loginHint] * @param {string} [ltiMessageHint] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiAuthAuthorizeLti(issOfTheTool?: string, clientId?: string, targetLinkUri?: string, state?: string, nonce?: string, loginHint?: string, ltiMessageHint?: string, options: any = {}): FetchArgs { - const localVarPath = `/api/lti/launch/authorize`; + ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/lti/authorize`; const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'POST' }, options); const localVarHeaderParameter = {} as any; @@ -7435,16 +7451,12 @@ export const LtiAuthApiFetchParamCreator = function (configuration?: Configurati localVarHeaderParameter["Authorization"] = localVarApiKeyValue; } - if (issOfTheTool !== undefined) { - localVarQueryParameter['issOfTheTool'] = issOfTheTool; - } - if (clientId !== undefined) { - localVarQueryParameter['clientId'] = clientId; + localVarQueryParameter['client_id'] = clientId; } - if (targetLinkUri !== undefined) { - localVarQueryParameter['targetLinkUri'] = targetLinkUri; + if (redirectUri !== undefined) { + localVarQueryParameter['redirect_uri'] = redirectUri; } if (state !== undefined) { @@ -7455,10 +7467,6 @@ export const LtiAuthApiFetchParamCreator = function (configuration?: Configurati localVarQueryParameter['nonce'] = nonce; } - if (loginHint !== undefined) { - localVarQueryParameter['login_hint'] = loginHint; - } - if (ltiMessageHint !== undefined) { localVarQueryParameter['lti_message_hint'] = ltiMessageHint; } @@ -7468,6 +7476,56 @@ export const LtiAuthApiFetchParamCreator = function (configuration?: Configurati localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} [resourceLinkId] + * @param {string} [courseId] + * @param {string} [toolId] + * @param {boolean} [isDeepLink] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, isDeepLink?: boolean, options: any = {}): FetchArgs { + const localVarPath = `/api/lti/start`; + 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 (resourceLinkId !== undefined) { + localVarQueryParameter['resourceLinkId'] = resourceLinkId; + } + + if (courseId !== undefined) { + localVarQueryParameter['courseId'] = courseId; + } + + if (toolId !== undefined) { + localVarQueryParameter['toolId'] = toolId; + } + + if (isDeepLink !== undefined) { + localVarQueryParameter['isDeepLink'] = isDeepLink; + } + + 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, @@ -7484,18 +7542,37 @@ export const LtiAuthApiFp = function(configuration?: Configuration) { return { /** * - * @param {string} [issOfTheTool] * @param {string} [clientId] - * @param {string} [targetLinkUri] + * @param {string} [redirectUri] * @param {string} [state] * @param {string} [nonce] - * @param {string} [loginHint] * @param {string} [ltiMessageHint] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiAuthAuthorizeLti(issOfTheTool?: string, clientId?: string, targetLinkUri?: string, state?: string, nonce?: string, loginHint?: string, ltiMessageHint?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthAuthorizeLti(issOfTheTool, clientId, targetLinkUri, state, nonce, loginHint, ltiMessageHint, options); + ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthAuthorizeLti(clientId, redirectUri, state, nonce, ltiMessageHint, 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 {string} [resourceLinkId] + * @param {string} [courseId] + * @param {string} [toolId] + * @param {boolean} [isDeepLink] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, isDeepLink?: boolean, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, isDeepLink, 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) { @@ -7517,18 +7594,28 @@ export const LtiAuthApiFactory = function (configuration?: Configuration, fetch? return { /** * - * @param {string} [issOfTheTool] * @param {string} [clientId] - * @param {string} [targetLinkUri] + * @param {string} [redirectUri] * @param {string} [state] * @param {string} [nonce] - * @param {string} [loginHint] * @param {string} [ltiMessageHint] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiAuthAuthorizeLti(issOfTheTool?: string, clientId?: string, targetLinkUri?: string, state?: string, nonce?: string, loginHint?: string, ltiMessageHint?: string, options?: any) { - return LtiAuthApiFp(configuration).ltiAuthAuthorizeLti(issOfTheTool, clientId, targetLinkUri, state, nonce, loginHint, ltiMessageHint, options)(fetch, basePath); + ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options?: any) { + return LtiAuthApiFp(configuration).ltiAuthAuthorizeLti(clientId, redirectUri, state, nonce, ltiMessageHint, options)(fetch, basePath); + }, + /** + * + * @param {string} [resourceLinkId] + * @param {string} [courseId] + * @param {string} [toolId] + * @param {boolean} [isDeepLink] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, isDeepLink?: boolean, options?: any) { + return LtiAuthApiFp(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, isDeepLink, options)(fetch, basePath); }, }; }; @@ -7542,19 +7629,31 @@ export const LtiAuthApiFactory = function (configuration?: Configuration, fetch? export class LtiAuthApi extends BaseAPI { /** * - * @param {string} [issOfTheTool] * @param {string} [clientId] - * @param {string} [targetLinkUri] + * @param {string} [redirectUri] * @param {string} [state] * @param {string} [nonce] - * @param {string} [loginHint] * @param {string} [ltiMessageHint] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof LtiAuthApi */ - public ltiAuthAuthorizeLti(issOfTheTool?: string, clientId?: string, targetLinkUri?: string, state?: string, nonce?: string, loginHint?: string, ltiMessageHint?: string, options?: any) { - return LtiAuthApiFp(this.configuration).ltiAuthAuthorizeLti(issOfTheTool, clientId, targetLinkUri, state, nonce, loginHint, ltiMessageHint, options)(this.fetch, this.basePath); + public ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options?: any) { + return LtiAuthApiFp(this.configuration).ltiAuthAuthorizeLti(clientId, redirectUri, state, nonce, ltiMessageHint, options)(this.fetch, this.basePath); + } + + /** + * + * @param {string} [resourceLinkId] + * @param {string} [courseId] + * @param {string} [toolId] + * @param {boolean} [isDeepLink] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiAuthApi + */ + public ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, isDeepLink?: boolean, options?: any) { + return LtiAuthApiFp(this.configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, isDeepLink, options)(this.fetch, this.basePath); } } diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index b5d1819fe..a2c661f57 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -35,6 +35,7 @@ import ErrorIcon from '@mui/icons-material/Error'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import SwitchAccessShortcutIcon from '@mui/icons-material/SwitchAccessShortcut'; import Lodash from "lodash"; +import { LtiImportButton } from "../Tasks/LtiImportButton"; interface ICourseExperimentalProps { homeworks: HomeworkViewModel[] @@ -615,14 +616,30 @@ export const CourseExperimental: FC = (props) => { )} - {x.id! < 0 && - } + {isMentor && ( + + {x.id! < 0 && ( + ) + } + + {x.id! < 0 && ( +
+ window.location.reload()} + /> +
+ )} +
+ )} ; })} diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index e3af6310b..faa51bbb6 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -192,6 +192,9 @@ const CourseTaskEditor: FC<{ }} />
+ + {/* LTI КНОПКА ОТСЮДА УДАЛЕНА, ТАК КАК ОНА ТЕПЕРЬ В РОДИТЕЛЕ */} + {metadata && homeworkPublicationDateIsSet && void; // Функция, которую вызовем, чтобы обновить список задач на странице +} + +export const LtiImportButton: FC = ({ homeworkId, courseId, toolId, onTasksAdded }) => { + const [isLoading, setIsLoading] = useState(false); + + // 1. Создаем и отправляем форму в новой вкладке + const submitLtiForm = (formData: any) => { + if (!formData || !formData.actionUrl) { + console.error("Invalid LTI Form Data"); + return; + } + + const form = document.createElement("form"); + form.method = formData.method; + form.action = formData.actionUrl; + form.target = "_blank"; // Открываем в новой вкладке + + if (formData.fields) { + Object.entries(formData.fields).forEach(([key, value]) => { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = key; + input.value = String(value); + form.appendChild(input); + }); + } + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); + }; + + // 2. Старт процесса (запрос к нашему API) + const handleStartLti = async () => { + setIsLoading(true); + try { + // isDeepLink = true + const response = await ApiSingleton.ltiAuthApi.ltiAuthStartLti( + undefined, + String(courseId), + String(toolId), + true + ); + + // Обработка ответа (если NSwag вернул Response вместо JSON) + let dto = response; + if (response && typeof (response as any).json === 'function') { + dto = await (response as any).json(); + } + + submitLtiForm(dto); + + setTimeout(() => setIsLoading(false), 15000); + + } catch (e) { + console.error(e); + alert("Не удалось запустить инструмент"); + setIsLoading(false); + } + }; + + // 3. Слушаем ответ от вкладки с Инструментом + useEffect(() => { + const handleLtiMessage = async (event: MessageEvent) => { + // Проверяем, что это сообщение от нашего LTI контроллера + if (event.data && event.data.type === 'LTI_DEEP_LINK_SUCCESS') { + const payload = event.data.payload; + + // Приводим к массиву (даже если вернулся один элемент) + const items = Array.isArray(payload) ? payload : [payload]; + + if (items.length === 0) return; + + setIsLoading(true); + try { + let count = 0; + + // Создаем задачи по очереди + for (const item of items) { + const newTask: CreateTaskViewModel = { + title: item.title || "External Task", + // Формируем ссылку в Markdown + description: `[Перейти к заданию](${item.url})`, + maxRating: 10, // Дефолтные баллы + hasDeadline: false, + isDeadlineStrict: false, + publicationDate: undefined // Сразу или по настройкам домашки + }; + + await ApiSingleton.tasksApi.tasksAddTask(homeworkId, newTask); + count++; + } + + // Успех! + alert(`Успешно импортировано задач: ${count}`); + onTasksAdded(); // Обновляем список задач на странице + + } catch (e) { + console.error("Ошибка импорта", e); + alert("Произошла ошибка при создании задач."); + } finally { + setIsLoading(false); + } + } + }; + + window.addEventListener("message", handleLtiMessage); + return () => window.removeEventListener("message", handleLtiMessage); + }, [homeworkId, onTasksAdded]); + + return ( + } + > + Импорт из внешнего инструмента + + ); +}; \ No newline at end of file From aed345227bda9c57384deaf87c1e0c60340732ff Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Sun, 4 Jan 2026 18:50:32 +0300 Subject: [PATCH 06/26] feat: made front-end support for deeplinking --- .../ViewModels/HomeworkTaskViewModels.cs | 5 + .../Controllers/TasksController.cs | 9 +- .../Models/HomeworkTaskLtiUrl.cs | 2 + .../Repositories/ITasksRepository.cs | 3 + .../Repositories/TasksRepository.cs | 20 ++ .../Services/ITasksService.cs | 3 +- .../Services/TasksService.cs | 15 +- hwproj.front/src/api/api.ts | 264 ++++++++++++++++++ .../src/components/Tasks/LtiImportButton.tsx | 23 +- 9 files changed, 331 insertions(+), 13 deletions(-) diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs index 8c40835aa..29efee3ff 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs @@ -35,6 +35,9 @@ public class HomeworkTaskViewModel public bool IsGroupWork { get; set; } public bool IsDeferred { get; set; } + + [JsonProperty] + public string? LtiLaunchUrl { get; set; } } public class HomeworkTaskForEditingViewModel @@ -63,5 +66,7 @@ public class CreateTaskViewModel [Required] public int MaxRating { get; set; } public ActionOptions? ActionOptions { get; set; } + + public string? LtiLaunchUrl { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs index cdeee03ad..bdabc33dc 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs @@ -46,6 +46,9 @@ public async Task GetTask(long taskId) } var task = taskFromDb.ToHomeworkTaskViewModel(); + + + task.LtiLaunchUrl = await _tasksService.GetTaskLtiUrlAsync(taskId); return Ok(task); } @@ -72,7 +75,11 @@ public async Task AddTask(long homeworkId, [FromBody] CreateTaskV var validationResult = Validator.ValidateTask(taskViewModel, homework); if (validationResult.Any()) return BadRequest(validationResult); - var task = await _tasksService.AddTaskAsync(homeworkId, taskViewModel.ToHomeworkTask()); + var task = await _tasksService.AddTaskAsync( + homeworkId, + taskViewModel.ToHomeworkTask(), + taskViewModel.LtiLaunchUrl + ); return Ok(task); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs index 8ebde9143..549878356 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs @@ -11,5 +11,7 @@ public class HomeworkTaskLtiUrl [Required] public string LtiLaunchUrl { get; set; } + + public int ToolId { get; set; } } } \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs index 3b0959178..ba375fc39 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs @@ -7,5 +7,8 @@ namespace HwProj.CoursesService.API.Repositories public interface ITasksRepository : ICrudRepository { Task GetWithHomeworkAsync(long id); + + Task AddLtiUrlAsync(long taskId, string ltiUrl); + Task GetLtiUrlAsync(long taskId); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs index baa9b6610..e5aef195d 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs @@ -19,5 +19,25 @@ public TasksRepository(CourseContext context) .AsNoTracking() .FirstOrDefaultAsync(x => x.Id == id); } + + public async Task AddLtiUrlAsync(long taskId, string ltiUrl) + { + var ltiRecord = new HomeworkTaskLtiUrl + { + TaskId = taskId, + LtiLaunchUrl = ltiUrl + }; + + await Context.Set().AddAsync(ltiRecord); + + await Context.SaveChangesAsync(); + } + + public async Task GetLtiUrlAsync(long taskId) + { + var record = await Context.Set().FindAsync(taskId); + + return record?.LtiLaunchUrl; + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs index 8ce089dca..98ec23d2e 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs @@ -8,8 +8,9 @@ public interface ITasksService { Task GetTaskAsync(long taskId); Task GetForEditingTaskAsync(long taskId); - Task AddTaskAsync(long homeworkId, HomeworkTask task); + Task AddTaskAsync(long homeworkId, HomeworkTask task, string? ltiLaunchUrl = null); Task DeleteTaskAsync(long taskId); Task UpdateTaskAsync(long taskId, HomeworkTask update, ActionOptions options); + Task GetTaskLtiUrlAsync(long taskId); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs index d6bc08613..b3919f5b4 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs @@ -40,7 +40,7 @@ public async Task GetForEditingTaskAsync(long taskId) return await _tasksRepository.GetWithHomeworkAsync(taskId); } - public async Task AddTaskAsync(long homeworkId, HomeworkTask task) + public async Task AddTaskAsync(long homeworkId, HomeworkTask task, string? ltiLaunchUrl = null) { task.HomeworkId = homeworkId; @@ -48,8 +48,14 @@ public async Task AddTaskAsync(long homeworkId, HomeworkTask task) var course = await _coursesRepository.GetWithCourseMatesAndHomeworksAsync(homework.CourseId); var taskId = await _tasksRepository.AddAsync(task); + + if (!string.IsNullOrEmpty(ltiLaunchUrl)) + { + await _tasksRepository.AddLtiUrlAsync(taskId, ltiLaunchUrl!); + } + var deadlineDate = task.DeadlineDate ?? homework.DeadlineDate; - var studentIds = course.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); + var studentIds = course!.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); if (task.PublicationDate <= DateTime.UtcNow) _eventBus.Publish(new NewHomeworkTaskEvent(task.Title, taskId, deadlineDate, course.Name, course.Id, @@ -92,5 +98,10 @@ public async Task UpdateTaskAsync(long taskId, HomeworkTask update CourseDomain.FillTask(updatedTask.Homework, updatedTask); return updatedTask; } + + public async Task GetTaskLtiUrlAsync(long taskId) + { + return await _tasksRepository.GetLtiUrlAsync(taskId); + } } } diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 153dc6719..f74ad7cc4 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -691,6 +691,12 @@ export interface CreateTaskViewModel { * @memberof CreateTaskViewModel */ actionOptions?: ActionOptions; + /** + * + * @type {string} + * @memberof CreateTaskViewModel + */ + ltiLaunchUrl?: string; } /** * @@ -1215,6 +1221,12 @@ export interface HomeworkTaskViewModel { * @memberof HomeworkTaskViewModel */ isDeferred?: boolean; + /** + * + * @type {string} + * @memberof HomeworkTaskViewModel + */ + ltiLaunchUrl?: string; } /** * @@ -1489,6 +1501,19 @@ export interface LoginViewModel { */ rememberMe: boolean; } +/** + * + * @export + * @interface LtiDeepLinkReturnBody + */ +export interface LtiDeepLinkReturnBody { + /** + * + * @type {Array} + * @memberof LtiDeepLinkReturnBody + */ + form?: Array; +} /** * * @export @@ -1507,6 +1532,12 @@ export interface LtiToolDto { * @memberof LtiToolDto */ name?: string; + /** + * + * @type {string} + * @memberof LtiToolDto + */ + clientId?: string; /** * * @type {string} @@ -2310,6 +2341,25 @@ export interface StatisticsLecturersModel { */ numberOfCheckedUniqueSolutions?: number; } +/** + * + * @export + * @interface StringStringValuesKeyValuePair + */ +export interface StringStringValuesKeyValuePair { + /** + * + * @type {string} + * @memberof StringStringValuesKeyValuePair + */ + key?: string; + /** + * + * @type {Array} + * @memberof StringStringValuesKeyValuePair + */ + value?: Array; +} /** * * @export @@ -7419,6 +7469,106 @@ export class HomeworksApi extends BaseAPI { return HomeworksApiFp(this.configuration).homeworksUpdateHomework(homeworkId, body, options)(this.fetch, this.basePath); } +} +/** + * JwksApi - fetch parameter creator + * @export + */ +export const JwksApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + jwksGetJwks(options: any = {}): FetchArgs { + const localVarPath = `/api/lti/jwks`; + 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; + } + + 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, + }; + }, + } +}; + +/** + * JwksApi - functional programming interface + * @export + */ +export const JwksApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + jwksGetJwks(options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = JwksApiFetchParamCreator(configuration).jwksGetJwks(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; + } + }); + }; + }, + } +}; + +/** + * JwksApi - factory interface + * @export + */ +export const JwksApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + jwksGetJwks(options?: any) { + return JwksApiFp(configuration).jwksGetJwks(options)(fetch, basePath); + }, + }; +}; + +/** + * JwksApi - object-oriented interface + * @export + * @class JwksApi + * @extends {BaseAPI} + */ +export class JwksApi extends BaseAPI { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof JwksApi + */ + public jwksGetJwks(options?: any) { + return JwksApiFp(this.configuration).jwksGetJwks(options)(this.fetch, this.basePath); + } + } /** * LtiAuthApi - fetch parameter creator @@ -7656,6 +7806,120 @@ export class LtiAuthApi extends BaseAPI { return LtiAuthApiFp(this.configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, isDeepLink, options)(this.fetch, this.basePath); } +} +/** + * LtiDeepLinkingReturnApi - fetch parameter creator + * @export + */ +export const LtiDeepLinkingReturnApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiDeepLinkingReturnOnDeepLinkingReturn(form?: Array, options: any = {}): FetchArgs { + const localVarPath = `/api/lti/deepLinkReturn`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + const localVarFormParams = new URLSearchParams(); + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (form) { + form.forEach((element) => { + localVarFormParams.append('form', element as any); + }) + } + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + 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); + localVarRequestOptions.body = localVarFormParams.toString(); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * LtiDeepLinkingReturnApi - functional programming interface + * @export + */ +export const LtiDeepLinkingReturnApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiDeepLinkingReturnOnDeepLinkingReturn(form?: Array, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiDeepLinkingReturnApiFetchParamCreator(configuration).ltiDeepLinkingReturnOnDeepLinkingReturn(form, 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; + } + }); + }; + }, + } +}; + +/** + * LtiDeepLinkingReturnApi - factory interface + * @export + */ +export const LtiDeepLinkingReturnApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiDeepLinkingReturnOnDeepLinkingReturn(form?: Array, options?: any) { + return LtiDeepLinkingReturnApiFp(configuration).ltiDeepLinkingReturnOnDeepLinkingReturn(form, options)(fetch, basePath); + }, + }; +}; + +/** + * LtiDeepLinkingReturnApi - object-oriented interface + * @export + * @class LtiDeepLinkingReturnApi + * @extends {BaseAPI} + */ +export class LtiDeepLinkingReturnApi extends BaseAPI { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiDeepLinkingReturnApi + */ + public ltiDeepLinkingReturnOnDeepLinkingReturn(form?: Array, options?: any) { + return LtiDeepLinkingReturnApiFp(this.configuration).ltiDeepLinkingReturnOnDeepLinkingReturn(form, options)(this.fetch, this.basePath); + } + } /** * LtiToolsApi - fetch parameter creator diff --git a/hwproj.front/src/components/Tasks/LtiImportButton.tsx b/hwproj.front/src/components/Tasks/LtiImportButton.tsx index db77ea7cd..efb536a08 100644 --- a/hwproj.front/src/components/Tasks/LtiImportButton.tsx +++ b/hwproj.front/src/components/Tasks/LtiImportButton.tsx @@ -7,14 +7,13 @@ import { CreateTaskViewModel } from "@/api"; interface LtiImportButtonProps { homeworkId: number; courseId: number; - toolId: number; // ID инструмента (например, 1 для Miminet) - onTasksAdded: () => void; // Функция, которую вызовем, чтобы обновить список задач на странице + toolId: number; + onTasksAdded: () => void; } export const LtiImportButton: FC = ({ homeworkId, courseId, toolId, onTasksAdded }) => { const [isLoading, setIsLoading] = useState(false); - // 1. Создаем и отправляем форму в новой вкладке const submitLtiForm = (formData: any) => { if (!formData || !formData.actionUrl) { console.error("Invalid LTI Form Data"); @@ -24,7 +23,7 @@ export const LtiImportButton: FC = ({ homeworkId, courseId const form = document.createElement("form"); form.method = formData.method; form.action = formData.actionUrl; - form.target = "_blank"; // Открываем в новой вкладке + form.target = "_blank"; if (formData.fields) { Object.entries(formData.fields).forEach(([key, value]) => { @@ -40,7 +39,6 @@ export const LtiImportButton: FC = ({ homeworkId, courseId document.body.removeChild(form); }; - // 2. Старт процесса (запрос к нашему API) const handleStartLti = async () => { setIsLoading(true); try { @@ -60,6 +58,7 @@ export const LtiImportButton: FC = ({ homeworkId, courseId submitLtiForm(dto); + // Снимаем лоадер через 15 секунд (если пользователь просто закрыл вкладку и ничего не выбрал) setTimeout(() => setIsLoading(false), 15000); } catch (e) { @@ -89,12 +88,18 @@ export const LtiImportButton: FC = ({ homeworkId, courseId for (const item of items) { const newTask: CreateTaskViewModel = { title: item.title || "External Task", - // Формируем ссылку в Markdown - description: `[Перейти к заданию](${item.url})`, - maxRating: 10, // Дефолтные баллы + + // ИЗМЕНЕНИЕ: Простое описание без ссылки + description: "Это интерактивное задание. Нажмите кнопку 'Перейти к выполнению', чтобы начать.", + + maxRating: 10, hasDeadline: false, isDeadlineStrict: false, - publicationDate: undefined // Сразу или по настройкам домашки + publicationDate: undefined, + + // ИЗМЕНЕНИЕ: Передаем URL в специальное поле + // (Убедитесь, что вы перегенерировали API клиент, и это поле доступно) + ltiLaunchUrl: item.url }; await ApiSingleton.tasksApi.tasksAddTask(homeworkId, newTask); From a74f3f041656fd5d5c37efcfeb04a138b6641afd Mon Sep 17 00:00:00 2001 From: KirillBorisovich Date: Sun, 4 Jan 2026 22:25:41 +0300 Subject: [PATCH 07/26] feat: migrated the database --- ...305_AddHomeworkTaskLtiUrlTable.Designer.cs | 349 ++++++++++++++++++ ...260104192305_AddHomeworkTaskLtiUrlTable.cs | 35 ++ .../Migrations/CourseContextModelSnapshot.cs | 22 ++ 3 files changed, 406 insertions(+) create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.Designer.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.cs diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.Designer.cs new file mode 100644 index 000000000..6af6fa94b --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.Designer.cs @@ -0,0 +1,349 @@ +// +using System; +using HwProj.CoursesService.API.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace HwProj.CoursesService.API.Migrations +{ + [DbContext(typeof(CourseContext))] + [Migration("20260104192305_AddHomeworkTaskLtiUrlTable")] + partial class AddHomeworkTaskLtiUrlTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("MentorId"); + + b.Property("StudentId"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Assignments"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupName"); + + b.Property("InviteCode"); + + b.Property("IsCompleted"); + + b.Property("IsOpen"); + + b.Property("MentorIds"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("FilterJson"); + + b.HasKey("Id"); + + b.ToTable("CourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("IsAccepted"); + + b.Property("StudentId"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("CourseMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupId"); + + b.Property("StudentId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasAlternateKey("GroupId", "StudentId"); + + b.ToTable("GroupMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("DeadlineDate"); + + b.Property("Description"); + + b.Property("HasDeadline"); + + b.Property("IsDeadlineStrict"); + + b.Property("PublicationDate"); + + b.Property("Tags"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("DeadlineDate"); + + b.Property("Description"); + + b.Property("HasDeadline"); + + b.Property("HomeworkId"); + + b.Property("IsDeadlineStrict"); + + b.Property("MaxRating"); + + b.Property("PublicationDate"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("HomeworkId"); + + b.ToTable("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + { + b.Property("TaskId"); + + b.Property("LtiLaunchUrl") + .IsRequired(); + + b.Property("ToolId"); + + b.HasKey("TaskId"); + + b.ToTable("TaskLtiUrls"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.Property("CourseMateId"); + + b.Property("Description"); + + b.Property("Tags"); + + b.HasKey("CourseMateId"); + + b.ToTable("StudentCharacteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupId"); + + b.Property("TaskId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("TasksModels"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Answer") + .HasMaxLength(1000); + + b.Property("IsPrivate"); + + b.Property("LecturerId"); + + b.Property("StudentId"); + + b.Property("TaskId"); + + b.Property("Text") + .HasMaxLength(1000); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Questions"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.Property("CourseId"); + + b.Property("UserId"); + + b.Property("CourseFilterId"); + + b.HasKey("CourseId", "UserId"); + + b.HasIndex("CourseFilterId"); + + b.ToTable("UserToCourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("Assignments") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("CourseMates") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group") + .WithMany("GroupMates") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("Homeworks") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") + .WithMany("Tasks") + .HasForeignKey("HomeworkId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") + .WithOne() + .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", "TaskId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseMate") + .WithOne("Characteristics") + .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group") + .WithMany("Tasks") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") + .WithMany() + .HasForeignKey("CourseFilterId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.cs new file mode 100644 index 000000000..f43816216 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace HwProj.CoursesService.API.Migrations +{ + public partial class AddHomeworkTaskLtiUrlTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TaskLtiUrls", + columns: table => new + { + TaskId = table.Column(nullable: false), + LtiLaunchUrl = table.Column(nullable: false), + ToolId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TaskLtiUrls", x => x.TaskId); + table.ForeignKey( + name: "FK_TaskLtiUrls_Tasks_TaskId", + column: x => x.TaskId, + principalTable: "Tasks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TaskLtiUrls"); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs index 736fb0058..415275f32 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -184,6 +184,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Tasks"); }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + { + b.Property("TaskId"); + + b.Property("LtiLaunchUrl") + .IsRequired(); + + b.Property("ToolId"); + + b.HasKey("TaskId"); + + b.ToTable("TaskLtiUrls"); + }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => { b.Property("CourseMateId"); @@ -296,6 +310,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") + .WithOne() + .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", "TaskId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => { b.HasOne("HwProj.CoursesService.API.Models.CourseMate") From 47455bc868ed5963bc91389d13fbd82b4edd0296 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Tue, 6 Jan 2026 16:05:19 +0300 Subject: [PATCH 08/26] feat: debugged the deeplinking implementation and added it to the correct state --- .../Lti/Controllers/MockToolController.cs | 6 + .../Controllers/TasksController.cs | 12 +- ...83735_HomeworkCommonProperties.Designer.cs | 214 ----------- ...20240104183735_HomeworkCommonProperties.cs | 51 --- .../20240108203028_Assignments.Designer.cs | 241 ------------ .../Migrations/20240108203028_Assignments.cs | 37 -- .../20240209220217_IsGroupWork.Designer.cs | 243 ------------- .../Migrations/20240209220217_IsGroupWork.cs | 16 - ...240408124740_AddTagsToHomework.Designer.cs | 245 ------------- .../20240408124740_AddTagsToHomework.cs | 22 -- ...240413143547_DeleteIsGroupWork.Designer.cs | 243 ------------- .../20240413143547_DeleteIsGroupWork.cs | 26 -- ...13242_CreateCourseFilterTables.Designer.cs | 279 -------------- ...20240911013242_CreateCourseFilterTables.cs | 57 --- .../20241110212839_TaskQuestions.Designer.cs | 306 ---------------- .../20241110212839_TaskQuestions.cs | 40 -- ...84715_'StudentCharacteristics'.Designer.cs | 327 ----------------- ...20250420184715_'StudentCharacteristics'.cs | 35 -- ...260104192305_AddHomeworkTaskLtiUrlTable.cs | 35 -- ... 20260104221447_InitialCreate.Designer.cs} | 4 +- .../20260104221447_InitialCreate.cs | 344 ++++++++++++++++++ .../Repositories/ITasksRepository.cs | 5 +- .../Repositories/TasksRepository.cs | 34 +- .../Services/CoursesService.cs | 17 + .../Services/HomeworksService.cs | 25 +- .../Services/ITasksService.cs | 2 +- .../Services/TasksService.cs | 8 +- .../HwProj.CoursesService.API/global.json | 1 + 28 files changed, 439 insertions(+), 2436 deletions(-) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.cs rename HwProj.CoursesService/HwProj.CoursesService.API/Migrations/{20260104192305_AddHomeworkTaskLtiUrlTable.Designer.cs => 20260104221447_InitialCreate.Designer.cs} (99%) create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/global.json diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs new file mode 100644 index 000000000..76e5fc0c6 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs @@ -0,0 +1,6 @@ +namespace HwProj.APIGateway.API.Lti.Controllers; + +public class MockToolController +{ + +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs index bdabc33dc..86f039f72 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs @@ -84,7 +84,7 @@ public async Task AddTask(long homeworkId, [FromBody] CreateTaskV return Ok(task); } - [HttpDelete("delete/{taskId}")] //bug with rights + [HttpDelete("delete/{taskId}")] [ServiceFilter(typeof(CourseMentorOnlyAttribute))] public async Task DeleteTask(long taskId) { @@ -101,9 +101,13 @@ public async Task UpdateTask(long taskId, [FromBody] CreateTaskVi if (validationResult.Any()) return BadRequest(validationResult); - var updatedTask = - await _tasksService.UpdateTaskAsync(taskId, taskViewModel.ToHomeworkTask(), - taskViewModel.ActionOptions ?? ActionOptions.Default); + var updatedTask = await _tasksService.UpdateTaskAsync( + taskId, + taskViewModel.ToHomeworkTask(), + taskViewModel.ActionOptions ?? ActionOptions.Default, + taskViewModel.LtiLaunchUrl + ); + return Ok(updatedTask.ToHomeworkTaskViewModel()); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.Designer.cs deleted file mode 100644 index 94831cdd7..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.Designer.cs +++ /dev/null @@ -1,214 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240104183735_HomeworkCommonProperties")] - partial class HomeworkCommonProperties - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.cs deleted file mode 100644 index eec4c39bd..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240104183735_HomeworkCommonProperties.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class HomeworkCommonProperties : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "Date", - table: "Homeworks", - newName: "PublicationDate"); - - migrationBuilder.AlterColumn( - name: "PublicationDate", - table: "Tasks", - nullable: true, - oldClrType: typeof(DateTime)); - - migrationBuilder.AlterColumn( - name: "IsDeadlineStrict", - table: "Tasks", - nullable: true, - oldClrType: typeof(bool)); - - migrationBuilder.AlterColumn( - name: "HasDeadline", - table: "Tasks", - nullable: true, - oldClrType: typeof(bool)); - - migrationBuilder.AddColumn( - name: "DeadlineDate", - table: "Homeworks", - nullable: true); - - migrationBuilder.AddColumn( - name: "HasDeadline", - table: "Homeworks", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "IsDeadlineStrict", - table: "Homeworks", - nullable: false, - defaultValue: false); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.Designer.cs deleted file mode 100644 index 9ad925339..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.Designer.cs +++ /dev/null @@ -1,241 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240108203028_Assignments")] - partial class Assignments - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.cs deleted file mode 100644 index 5ae583675..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240108203028_Assignments.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class Assignments : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Assignments", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), - CourseId = table.Column(nullable: false), - MentorId = table.Column(nullable: false), - StudentId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Assignments", x => x.Id); - table.ForeignKey( - name: "FK_Assignments_Courses_CourseId", - column: x => x.CourseId, - principalTable: "Courses", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Assignments_CourseId", - table: "Assignments", - column: "CourseId"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.Designer.cs deleted file mode 100644 index 5fd43694a..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.Designer.cs +++ /dev/null @@ -1,243 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240209220217_IsGroupWork")] - partial class IsGroupWork - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("IsGroupWork"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.cs deleted file mode 100644 index c698d63d6..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240209220217_IsGroupWork.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class IsGroupWork : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsGroupWork", - table: "Homeworks", - nullable: false, - defaultValue: false); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.Designer.cs deleted file mode 100644 index 00c1bbf88..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.Designer.cs +++ /dev/null @@ -1,245 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240408124740_AddTagsToHomework")] - partial class AddTagsToHomework - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("IsGroupWork"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.cs deleted file mode 100644 index dac7516bd..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240408124740_AddTagsToHomework.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class AddTagsToHomework : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Tags", - table: "Homeworks", - nullable: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Tags", - table: "Homeworks"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.Designer.cs deleted file mode 100644 index 1d2a123cc..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.Designer.cs +++ /dev/null @@ -1,243 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240413143547_DeleteIsGroupWork")] - partial class DeleteIsGroupWork - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.cs deleted file mode 100644 index f09bdd8e5..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240413143547_DeleteIsGroupWork.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class DeleteIsGroupWork : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql("UPDATE Homeworks SET Tags = Tags + ';Командная работа' WHERE IsGroupWork = 1 AND Tags != null"); - migrationBuilder.Sql("UPDATE Homeworks SET Tags = 'Командная работа' WHERE IsGroupWork = 1 AND Tags = null"); - - migrationBuilder.DropColumn( - name: "IsGroupWork", - table: "Homeworks"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsGroupWork", - table: "Homeworks", - nullable: false, - defaultValue: false); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.Designer.cs deleted file mode 100644 index 4fff1b41c..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.Designer.cs +++ /dev/null @@ -1,279 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20240911013242_CreateCourseFilterTables")] - partial class CreateCourseFilterTables - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.cs deleted file mode 100644 index be351c013..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20240911013242_CreateCourseFilterTables.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class CreateCourseFilterTables : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "CourseFilters", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), - FilterJson = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_CourseFilters", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "UserToCourseFilters", - columns: table => new - { - CourseId = table.Column(nullable: false), - UserId = table.Column(nullable: false), - CourseFilterId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserToCourseFilters", x => new { x.CourseId, x.UserId }); - table.ForeignKey( - name: "FK_UserToCourseFilters_CourseFilters_CourseFilterId", - column: x => x.CourseFilterId, - principalTable: "CourseFilters", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_UserToCourseFilters_CourseFilterId", - table: "UserToCourseFilters", - column: "CourseFilterId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "UserToCourseFilters"); - - migrationBuilder.DropTable( - name: "CourseFilters"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.Designer.cs deleted file mode 100644 index 25de66053..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.Designer.cs +++ /dev/null @@ -1,306 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20241110212839_TaskQuestions")] - partial class TaskQuestions - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.cs deleted file mode 100644 index 1f7c21fe1..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20241110212839_TaskQuestions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class TaskQuestions : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Questions", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), - TaskId = table.Column(nullable: false), - StudentId = table.Column(nullable: true), - Text = table.Column(maxLength: 1000, nullable: true), - IsPrivate = table.Column(nullable: false), - LecturerId = table.Column(nullable: true), - Answer = table.Column(maxLength: 1000, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Questions", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_Questions_TaskId", - table: "Questions", - column: "TaskId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Questions"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.Designer.cs deleted file mode 100644 index 66c42ced8..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.Designer.cs +++ /dev/null @@ -1,327 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20250420184715_'StudentCharacteristics'")] - partial class StudentCharacteristics - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.Property("CourseMateId"); - - b.Property("Description"); - - b.Property("Tags"); - - b.HasKey("CourseMateId"); - - b.ToTable("StudentCharacteristics"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseMate") - .WithOne("Characteristics") - .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.cs deleted file mode 100644 index 2ae866cbf..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20250420184715_'StudentCharacteristics'.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class StudentCharacteristics : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "StudentCharacteristics", - columns: table => new - { - CourseMateId = table.Column(nullable: false), - Tags = table.Column(nullable: true), - Description = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_StudentCharacteristics", x => x.CourseMateId); - table.ForeignKey( - name: "FK_StudentCharacteristics_CourseMates_CourseMateId", - column: x => x.CourseMateId, - principalTable: "CourseMates", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "StudentCharacteristics"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.cs deleted file mode 100644 index f43816216..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class AddHomeworkTaskLtiUrlTable : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "TaskLtiUrls", - columns: table => new - { - TaskId = table.Column(nullable: false), - LtiLaunchUrl = table.Column(nullable: false), - ToolId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TaskLtiUrls", x => x.TaskId); - table.ForeignKey( - name: "FK_TaskLtiUrls_Tasks_TaskId", - column: x => x.TaskId, - principalTable: "Tasks", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "TaskLtiUrls"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.Designer.cs similarity index 99% rename from HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.Designer.cs rename to HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.Designer.cs index 6af6fa94b..fe3c6fab2 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104192305_AddHomeworkTaskLtiUrlTable.Designer.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.Designer.cs @@ -10,8 +10,8 @@ namespace HwProj.CoursesService.API.Migrations { [DbContext(typeof(CourseContext))] - [Migration("20260104192305_AddHomeworkTaskLtiUrlTable")] - partial class AddHomeworkTaskLtiUrlTable + [Migration("20260104221447_InitialCreate")] + partial class InitialCreate { protected override void BuildTargetModel(ModelBuilder modelBuilder) { diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.cs new file mode 100644 index 000000000..836a5a933 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.cs @@ -0,0 +1,344 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace HwProj.CoursesService.API.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CourseFilters", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + FilterJson = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CourseFilters", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Courses", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Name = table.Column(nullable: true), + GroupName = table.Column(nullable: true), + IsOpen = table.Column(nullable: false), + InviteCode = table.Column(nullable: true), + IsCompleted = table.Column(nullable: false), + MentorIds = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Courses", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Groups", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + CourseId = table.Column(nullable: false), + Name = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Groups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Questions", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + TaskId = table.Column(nullable: false), + StudentId = table.Column(nullable: true), + Text = table.Column(maxLength: 1000, nullable: true), + IsPrivate = table.Column(nullable: false), + LecturerId = table.Column(nullable: true), + Answer = table.Column(maxLength: 1000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Questions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UserToCourseFilters", + columns: table => new + { + CourseId = table.Column(nullable: false), + UserId = table.Column(nullable: false), + CourseFilterId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserToCourseFilters", x => new { x.CourseId, x.UserId }); + table.ForeignKey( + name: "FK_UserToCourseFilters_CourseFilters_CourseFilterId", + column: x => x.CourseFilterId, + principalTable: "CourseFilters", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Assignments", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + CourseId = table.Column(nullable: false), + MentorId = table.Column(nullable: true), + StudentId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Assignments", x => x.Id); + table.ForeignKey( + name: "FK_Assignments_Courses_CourseId", + column: x => x.CourseId, + principalTable: "Courses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CourseMates", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + CourseId = table.Column(nullable: false), + StudentId = table.Column(nullable: true), + IsAccepted = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CourseMates", x => x.Id); + table.ForeignKey( + name: "FK_CourseMates_Courses_CourseId", + column: x => x.CourseId, + principalTable: "Courses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Homeworks", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Title = table.Column(nullable: true), + Description = table.Column(nullable: true), + HasDeadline = table.Column(nullable: false), + DeadlineDate = table.Column(nullable: true), + IsDeadlineStrict = table.Column(nullable: false), + PublicationDate = table.Column(nullable: false), + Tags = table.Column(nullable: true), + CourseId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Homeworks", x => x.Id); + table.ForeignKey( + name: "FK_Homeworks_Courses_CourseId", + column: x => x.CourseId, + principalTable: "Courses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "GroupMates", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + GroupId = table.Column(nullable: false), + StudentId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GroupMates", x => x.Id); + table.UniqueConstraint("AK_GroupMates_GroupId_StudentId", x => new { x.GroupId, x.StudentId }); + table.ForeignKey( + name: "FK_GroupMates_Groups_GroupId", + column: x => x.GroupId, + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TasksModels", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + TaskId = table.Column(nullable: false), + GroupId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TasksModels", x => x.Id); + table.ForeignKey( + name: "FK_TasksModels_Groups_GroupId", + column: x => x.GroupId, + principalTable: "Groups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "StudentCharacteristics", + columns: table => new + { + CourseMateId = table.Column(nullable: false), + Tags = table.Column(nullable: true), + Description = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StudentCharacteristics", x => x.CourseMateId); + table.ForeignKey( + name: "FK_StudentCharacteristics_CourseMates_CourseMateId", + column: x => x.CourseMateId, + principalTable: "CourseMates", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tasks", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Title = table.Column(nullable: true), + Description = table.Column(nullable: true), + MaxRating = table.Column(nullable: false), + HasDeadline = table.Column(nullable: true), + DeadlineDate = table.Column(nullable: true), + IsDeadlineStrict = table.Column(nullable: true), + PublicationDate = table.Column(nullable: true), + HomeworkId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tasks", x => x.Id); + table.ForeignKey( + name: "FK_Tasks_Homeworks_HomeworkId", + column: x => x.HomeworkId, + principalTable: "Homeworks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TaskLtiUrls", + columns: table => new + { + TaskId = table.Column(nullable: false), + LtiLaunchUrl = table.Column(nullable: false), + ToolId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TaskLtiUrls", x => x.TaskId); + table.ForeignKey( + name: "FK_TaskLtiUrls_Tasks_TaskId", + column: x => x.TaskId, + principalTable: "Tasks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Assignments_CourseId", + table: "Assignments", + column: "CourseId"); + + migrationBuilder.CreateIndex( + name: "IX_CourseMates_CourseId", + table: "CourseMates", + column: "CourseId"); + + migrationBuilder.CreateIndex( + name: "IX_Homeworks_CourseId", + table: "Homeworks", + column: "CourseId"); + + migrationBuilder.CreateIndex( + name: "IX_Questions_TaskId", + table: "Questions", + column: "TaskId"); + + migrationBuilder.CreateIndex( + name: "IX_Tasks_HomeworkId", + table: "Tasks", + column: "HomeworkId"); + + migrationBuilder.CreateIndex( + name: "IX_TasksModels_GroupId", + table: "TasksModels", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "IX_UserToCourseFilters_CourseFilterId", + table: "UserToCourseFilters", + column: "CourseFilterId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Assignments"); + + migrationBuilder.DropTable( + name: "GroupMates"); + + migrationBuilder.DropTable( + name: "Questions"); + + migrationBuilder.DropTable( + name: "StudentCharacteristics"); + + migrationBuilder.DropTable( + name: "TaskLtiUrls"); + + migrationBuilder.DropTable( + name: "TasksModels"); + + migrationBuilder.DropTable( + name: "UserToCourseFilters"); + + migrationBuilder.DropTable( + name: "CourseMates"); + + migrationBuilder.DropTable( + name: "Tasks"); + + migrationBuilder.DropTable( + name: "Groups"); + + migrationBuilder.DropTable( + name: "CourseFilters"); + + migrationBuilder.DropTable( + name: "Homeworks"); + + migrationBuilder.DropTable( + name: "Courses"); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs index ba375fc39..3e5dc440f 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using HwProj.CoursesService.API.Models; using HwProj.Repositories; @@ -10,5 +11,7 @@ public interface ITasksRepository : ICrudRepository Task AddLtiUrlAsync(long taskId, string ltiUrl); Task GetLtiUrlAsync(long taskId); + + Task> GetLtiUrlsForTasksAsync(IEnumerable taskIds); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs index e5aef195d..9681321ad 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using HwProj.CoursesService.API.Models; using HwProj.Repositories; using Microsoft.EntityFrameworkCore; @@ -22,14 +24,23 @@ public TasksRepository(CourseContext context) public async Task AddLtiUrlAsync(long taskId, string ltiUrl) { - var ltiRecord = new HomeworkTaskLtiUrl - { - TaskId = taskId, - LtiLaunchUrl = ltiUrl - }; + var existingRecord = await Context.Set().FindAsync(taskId); - await Context.Set().AddAsync(ltiRecord); - + if (existingRecord != null) + { + existingRecord.LtiLaunchUrl = ltiUrl; + Context.Set().Update(existingRecord); + } + else + { + var ltiRecord = new HomeworkTaskLtiUrl + { + TaskId = taskId, + LtiLaunchUrl = ltiUrl + }; + await Context.Set().AddAsync(ltiRecord); + } + await Context.SaveChangesAsync(); } @@ -39,5 +50,12 @@ public async Task AddLtiUrlAsync(long taskId, string ltiUrl) return record?.LtiLaunchUrl; } + + public async Task> GetLtiUrlsForTasksAsync(IEnumerable taskIds) + { + return await Context.Set() + .Where(t => taskIds.Contains(t.TaskId)) + .ToDictionaryAsync(t => t.TaskId, t => t.LtiLaunchUrl); + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs index 4c77eaf0f..01a085d56 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -76,6 +76,23 @@ public async Task GetAllAsync() var groups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(course.Id).ToArrayAsync(); var courseDto = course.ToCourseDto(); + var allTasks = courseDto.Homeworks.SelectMany(h => h.Tasks).ToList(); + + if (allTasks.Any()) + { + var taskIds = allTasks.Select(t => t.Id).ToArray(); + + var ltiUrls = await _tasksRepository.GetLtiUrlsForTasksAsync(taskIds); + + foreach (var taskDto in allTasks) + { + if (ltiUrls.TryGetValue(taskDto.Id, out var url)) + { + taskDto.LtiLaunchUrl = url; + } + } + } + courseDto.Groups = groups.Select(g => new GroupViewModel { diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 24b2bc843..a785c0365 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -16,13 +16,15 @@ public class HomeworksService : IHomeworksService private readonly IHomeworksRepository _homeworksRepository; private readonly IEventBus _eventBus; private readonly ICoursesRepository _coursesRepository; + private readonly ITasksRepository _tasksRepository; public HomeworksService(IHomeworksRepository homeworksRepository, IEventBus eventBus, - ICoursesRepository coursesRepository) + ICoursesRepository coursesRepository, ITasksRepository tasksRepository) { _homeworksRepository = homeworksRepository; _eventBus = eventBus; _coursesRepository = coursesRepository; + _tasksRepository = tasksRepository; } public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewModel homeworkViewModel) @@ -39,7 +41,26 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo homework.DeadlineDate)); } - await _homeworksRepository.AddAsync(homework); + var homeworkId = await _homeworksRepository.AddAsync(homework); + + if (homeworkViewModel.Tasks != null && homework.Tasks != null) + { + // Превращаем в списки для доступа по индексу + var createdTasks = homework.Tasks.ToList(); + var taskModels = homeworkViewModel.Tasks; + + // Проходимся по списку и сохраняем URL, если он был передан + for (var i = 0; i < createdTasks.Count && i < taskModels.Count; i++) + { + var url = taskModels[i].LtiLaunchUrl; + + if (!string.IsNullOrEmpty(url)) + { + await _tasksRepository.AddLtiUrlAsync(createdTasks[i].Id, url!); + } + } + } + return await GetHomeworkAsync(homework.Id); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs index 98ec23d2e..207d6d0e3 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs @@ -10,7 +10,7 @@ public interface ITasksService Task GetForEditingTaskAsync(long taskId); Task AddTaskAsync(long homeworkId, HomeworkTask task, string? ltiLaunchUrl = null); Task DeleteTaskAsync(long taskId); - Task UpdateTaskAsync(long taskId, HomeworkTask update, ActionOptions options); + Task UpdateTaskAsync(long taskId, HomeworkTask update, ActionOptions options, string? ltiLaunchUrl = null); Task GetTaskLtiUrlAsync(long taskId); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs index b3919f5b4..5f8420ef4 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs @@ -69,7 +69,7 @@ public async Task DeleteTaskAsync(long taskId) await _tasksRepository.DeleteAsync(taskId); } - public async Task UpdateTaskAsync(long taskId, HomeworkTask update, ActionOptions options) + public async Task UpdateTaskAsync(long taskId, HomeworkTask update, ActionOptions options, string? ltiLaunchUrl = null) { var task = await _tasksRepository.GetWithHomeworkAsync(taskId); if (task == null) throw new InvalidOperationException("Task not found"); @@ -93,6 +93,12 @@ public async Task UpdateTaskAsync(long taskId, HomeworkTask update PublicationDate = update.PublicationDate }); + if (ltiLaunchUrl != null) + { + await _tasksRepository.AddLtiUrlAsync(taskId, ltiLaunchUrl); + } + + var updatedTask = await _tasksRepository.GetAsync(taskId); updatedTask.Homework = task.Homework; CourseDomain.FillTask(updatedTask.Homework, updatedTask); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/global.json b/HwProj.CoursesService/HwProj.CoursesService.API/global.json new file mode 100644 index 000000000..e4d8f6d06 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/global.json @@ -0,0 +1 @@ +{ "sdk": { "version": "2.2.207" } } From f02fd066ed37205d172456e5443e2102e95ba832 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Tue, 6 Jan 2026 17:45:07 +0300 Subject: [PATCH 09/26] fix: fixed a bug with the presence of ltiLaunchUrl in the server response --- .../Lti/Controllers/LtiAuthController.cs | 4 +- .../LtiDeepLinkingReturnController.cs | 81 ++++-- .../Lti/Controllers/MockToolController.cs | 114 +++++++- .../Controllers/HomeworksController.cs | 49 +++- .../Domains/MappingExtensions.cs | 1 + .../Models/HomeworkTaskTemplate.cs | 2 + .../Services/CoursesService.cs | 60 +++-- .../Services/HomeworksService.cs | 4 +- .../Services/ITasksService.cs | 4 +- .../Services/TasksService.cs | 6 + hwproj.front/src/api/api.ts | 247 +++++++++++++++++- .../components/Courses/CourseExperimental.tsx | 107 ++++++-- .../Homeworks/CourseHomeworkExperimental.tsx | 3 +- .../Tasks/CourseTaskExperimental.tsx | 6 +- .../src/components/Tasks/LtiImportButton.tsx | 104 +++----- 15 files changed, 638 insertions(+), 154 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs index 130cd0384..5827012b3 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs @@ -26,8 +26,8 @@ IDataProtectionProvider provider private readonly IDataProtector protector = provider.CreateProtector("LtiPlatform.MessageHint.v1"); // Tool редиректит сюда браузер (шаг "redirect browser to Platform for Auth") - [HttpPost("authorize")] - [Authorize] + [HttpGet("authorize")] + [AllowAnonymous] public async Task AuthorizeLti( [FromQuery(Name = "client_id")] string clientId, [FromQuery(Name = "redirect_uri")] string redirectUri, diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs index 721155217..dcc89b1db 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs @@ -1,10 +1,9 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Linq; +using System.Collections.Generic; using System.Text.Json; -using LtiAdvantage.DeepLinking; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; // Убедитесь, что установлен пакет Microsoft.IdentityModel.Tokens namespace HwProj.APIGateway.API.Lti.Controllers; @@ -12,61 +11,95 @@ namespace HwProj.APIGateway.API.Lti.Controllers; [ApiController] public class LtiDeepLinkingReturnController : ControllerBase { + // Инструмент отправляет форму с полем JWT на этот адрес [HttpPost("deepLinkReturn")] - [AllowAnonymous] + [AllowAnonymous] // Анонимно, так как запрос идет от браузера при редиректе из тула public IActionResult OnDeepLinkingReturn([FromForm] IFormCollection form) { + // 1. Проверяем наличие параметра JWT if (!form.ContainsKey("JWT")) { return BadRequest("Missing JWT parameter"); } string tokenString = form["JWT"]!; - var handler = new JwtSecurityTokenHandler(); - - if (!handler.CanReadToken(tokenString)) + + // 2. Разбиваем токен на части (Header.Payload.Signature) + var parts = tokenString.Split('.'); + if (parts.Length != 3) { - return BadRequest("Invalid JWT format"); + return BadRequest("Invalid JWT structure"); } - // (добавить валидацию подписи) - var jwtToken = handler.ReadJwtToken(tokenString); + // В ПРОДАКШЕНЕ ЗДЕСЬ НУЖНА ВАЛИДАЦИЯ ПОДПИСИ (Signature) + // Для этого нужно достать Public Key инструмента (JWKS) и проверить подпись. + // Пока мы просто доверяем содержимому для тестов. - var ltiResponse = new LtiDeepLinkingResponse(jwtToken.Payload); + // 3. Декодируем Payload из Base64Url + string payloadJson; + try + { + payloadJson = Base64UrlEncoder.Decode(parts[1]); + } + catch + { + return BadRequest("Invalid Base64 in JWT"); + } - var items = ltiResponse.ContentItems; + // 4. Парсим JSON вручную с помощью JsonDocument + // Это самый надежный способ, который не падает из-за несовпадения типов C# классов. + using var doc = JsonDocument.Parse(payloadJson); + var root = doc.RootElement; + + // Имя поля по стандарту LTI 1.3 Deep Linking + var itemsClaimName = "https://purl.imsglobal.org/spec/lti-dl/claim/content_items"; + + var resultList = new List(); - if (items == null || items.Length == 0) + // 5. Ищем массив content_items + if (root.TryGetProperty(itemsClaimName, out var itemsElement) && itemsElement.ValueKind == JsonValueKind.Array) { - return Content("", "text/html"); + foreach (var rawItem in itemsElement.EnumerateArray()) + { + resultList.Add(rawItem.Clone().ToString()); + } } - var payloadList = items.Select(item => new + // Если список пуст (инструмент ничего не выбрал или формат неверен) + if (resultList.Count == 0) { - title = !string.IsNullOrEmpty(item.Title) ? item.Title : "External Resource", - url = item.Url, - text = item.Text ?? "" - }).ToList(); + // Просто закрываем окно + return Content("", "text/html"); + } - var payloadJson = JsonSerializer.Serialize(payloadList); + // 6. Сериализуем список обратно в JSON для передачи на фронтенд HwProj + var responsePayloadJson = JsonSerializer.Serialize(resultList); + // 7. Генерируем HTML-страницу, которая передаст данные родительскому окну и закроется var htmlResponse = $@" + Processing LTI Return... +

Задача выбрана. Возвращаемся в HwProj...

diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs index 76e5fc0c6..6c87467a3 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs @@ -1,6 +1,114 @@ -namespace HwProj.APIGateway.API.Lti.Controllers; +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text.Json; +using Newtonsoft.Json.Linq; -public class MockToolController +[Route("api/mocktool")] +[ApiController] +public class MockToolController : ControllerBase { - + [HttpPost("login")] + public IActionResult Login([FromForm] string iss, [FromForm] string login_hint, [FromForm] string lti_message_hint) + { +// Эмулируем редирект обратно на Платформу (Authorize) +// В реальном мире тут инструмент генерирует nonce и state + var callbackUrl = $"{iss}/api/lti/authorize?" + + $"client_id=mock-tool-client-id&" + + $"response_type=id_token&" + + $"redirect_uri=http://localhost:5000/api/mocktool/callback&" + // Куда вернуть токен + $"login_hint={login_hint}&" + + $"lti_message_hint={lti_message_hint}&" + + $"scope=openid&state=xyz&nonce=123"; + + return Redirect(callbackUrl); + } + + [HttpPost("callback")] + public IActionResult Callback([FromForm] string id_token) + { + var handler = new JwtSecurityTokenHandler(); + + // Читаем входящий токен (без валидации подписи, т.к. это мок) + var token = handler.ReadJwtToken(id_token); + + // Достаем URL возврата + var settingsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings"); + if (settingsClaim == null) return BadRequest("No deep linking settings found"); + + var settings = JsonDocument.Parse(settingsClaim.Value); + var returnUrl = settings.RootElement.GetProperty("deep_link_return_url").GetString(); + + // --- ГЕНЕРАЦИЯ ОТВЕТА --- + + // 1. Формируем Content Items, используя обычные C# объекты (Arrays/Anonymous Objects) + // Это гарантирует правильный JSON на выходе. + var contentItems = new List> + { + new Dictionary + { + ["type"] = "ltiResourceLink", + ["title"] = "Тестовая Задача из Мока", + ["url"] = "http://localhost:5000/mock/task/1", + ["text"] = "Описание тестовой задачи" + } + }; + + contentItems.Add(new Dictionary + { + ["type"] = "ltiResourceLink", + ["title"] = "Тестовая Задача из Мока2", + ["url"] = "http://localhost:5000/mock/task/2", + ["text"] = "Описание тестовой задачи2" + }); + + // 2. Собираем Payload + var payload = new JwtPayload + { + { "iss", "MockTool" }, + { "aud", "HwProj" }, + { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, + { "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() }, + { "nonce", "random-nonce-123" }, // LTI требует nonce + { "https://purl.imsglobal.org/spec/lti-dl/claim/message_type", "LtiDeepLinkingResponse" }, + { "https://purl.imsglobal.org/spec/lti-dl/claim/version", "1.3.0" }, + { "https://purl.imsglobal.org/spec/lti-dl/claim/content_items", contentItems } + }; + + // 3. Создаем Header с "пустой" подписью, чтобы библиотека не ругалась + // В реальном LTI здесь должен быть реальный ключ. Для мока делаем "unsigned". + var header = new JwtHeader(); + + // ВАЖНО: JwtSecurityTokenHandler по умолчанию не дает создать токен без подписи. + // Мы обойдем это, создав токен вручную, или просто подпишем "мусорным" ключом. + // Самый простой способ для мока - подписать любым ключом, т.к. принимающая сторона (пока) не проверяет подпись. + + var securityKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("secret-key-must-be-at-least-16-chars")); + var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(securityKey, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256); + + // Пересоздаем хедер с алгоритмом + header = new JwtHeader(credentials); + + // 4. Генерируем строку + var responseToken = new JwtSecurityToken(header, payload); + var responseString = handler.WriteToken(responseToken); + + // Рисуем форму авто-сабмита или кнопку + var html = $@" + + +

Интерфейс Инструмента (Mock)

+

Задача выбрана. Нажмите кнопку, чтобы вернуться в HwProj.

+
+ + +
+ + "; + + return Content(html, "text/html"); + } } \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs index dbbdb1ba6..d003aad20 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs @@ -13,10 +13,12 @@ namespace HwProj.CoursesService.API.Controllers public class HomeworksController : Controller { private readonly IHomeworksService _homeworksService; + private readonly ITasksService _tasksService; - public HomeworksController(IHomeworksService homeworksService) + public HomeworksController(IHomeworksService homeworksService, ITasksService tasksService) { _homeworksService = homeworksService; + _tasksService = tasksService; } [HttpPost("{courseId}/add")] @@ -28,15 +30,22 @@ public async Task AddHomework(long courseId, if (validationResult.Any()) return BadRequest(validationResult); var newHomework = await _homeworksService.AddHomeworkAsync(courseId, homeworkViewModel); - return Ok(newHomework.ToHomeworkViewModel()); + var responseViewModel = newHomework.ToHomeworkViewModel(); + + await FillLtiUrls(responseViewModel); + + return Ok(responseViewModel); } [HttpGet("get/{homeworkId}")] public async Task GetHomework(long homeworkId) { var homeworkFromDb = await _homeworksService.GetHomeworkAsync(homeworkId); - var homework = homeworkFromDb.ToHomeworkViewModel(); - return homework; + var homeworkViewModel = homeworkFromDb.ToHomeworkViewModel(); + + await FillLtiUrls(homeworkViewModel); + + return homeworkViewModel; } [HttpGet("getForEditing/{homeworkId}")] @@ -44,8 +53,11 @@ public async Task GetHomework(long homeworkId) public async Task GetForEditingHomework(long homeworkId) { var homeworkFromDb = await _homeworksService.GetForEditingHomeworkAsync(homeworkId); - var homework = homeworkFromDb.ToHomeworkViewModel(); - return homework; + var homeworkViewModel = homeworkFromDb.ToHomeworkViewModel(); + + await FillLtiUrls(homeworkViewModel); + + return homeworkViewModel; } [HttpDelete("delete/{homeworkId}")] @@ -65,7 +77,28 @@ public async Task UpdateHomework(long homeworkId, if (validationResult.Any()) return BadRequest(validationResult); var updatedHomework = await _homeworksService.UpdateHomeworkAsync(homeworkId, homeworkViewModel); - return Ok(updatedHomework.ToHomeworkViewModel()); + var responseViewModel = updatedHomework.ToHomeworkViewModel(); + + await FillLtiUrls(responseViewModel); + + return Ok(responseViewModel); + } + + private async Task FillLtiUrls(HomeworkViewModel viewModel) + { + if (viewModel.Tasks != null && viewModel.Tasks.Any()) + { + var taskIds = viewModel.Tasks.Select(t => t.Id).ToArray(); + var ltiUrls = await _tasksService.GetLtiUrlsForTasksAsync(taskIds); + + foreach (var task in viewModel.Tasks) + { + if (ltiUrls.TryGetValue(task.Id, out var url)) + { + task.LtiLaunchUrl = url; + } + } + } } } -} +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs index 8e5924cf8..ab5ff863e 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs @@ -166,6 +166,7 @@ public static HomeworkTaskTemplate ToHomeworkTaskTemplate(this HomeworkTaskViewM IsDeadlineStrict = task.IsDeadlineStrict, HasSpecialPublicationDate = task.PublicationDate != null, HasSpecialDeadlineDate = task.DeadlineDate != null, + LtiLaunchUrl = task.LtiLaunchUrl }; public static Course ToCourse(this CourseTemplate courseTemplate) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs index 2e1b17e58..1fcc964e6 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs @@ -15,5 +15,7 @@ public class HomeworkTaskTemplate public bool HasSpecialPublicationDate { get; set; } public bool HasSpecialDeadlineDate { get; set; } + + public string? LtiLaunchUrl { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs index 01a085d56..928e548d4 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -76,22 +76,8 @@ public async Task GetAllAsync() var groups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(course.Id).ToArrayAsync(); var courseDto = course.ToCourseDto(); - var allTasks = courseDto.Homeworks.SelectMany(h => h.Tasks).ToList(); - - if (allTasks.Any()) - { - var taskIds = allTasks.Select(t => t.Id).ToArray(); - var ltiUrls = await _tasksRepository.GetLtiUrlsForTasksAsync(taskIds); - - foreach (var taskDto in allTasks) - { - if (ltiUrls.TryGetValue(taskDto.Id, out var url)) - { - taskDto.LtiLaunchUrl = url; - } - } - } + await FillLtiUrlsForCourseDtos(new[] { courseDto }); courseDto.Groups = groups.Select(g => new GroupViewModel @@ -166,9 +152,25 @@ private async Task AddFromTemplateAsync(CourseTemplate courseTemplate, L var homeworks = courseTemplate.Homeworks.Select(hwTemplate => hwTemplate.ToHomework(courseId)); var homeworkIds = await _homeworksRepository.AddRangeAsync(homeworks); - var tasks = courseTemplate.Homeworks.SelectMany((hwTemplate, i) => - hwTemplate.Tasks.Select(taskTemplate => taskTemplate.ToHomeworkTask(homeworkIds[i]))); - await _tasksRepository.AddRangeAsync(tasks); + var taskPairs = courseTemplate.Homeworks + .SelectMany((hwTemplate, i) => + hwTemplate.Tasks.Select(taskTemplate => new + { + Template = taskTemplate, + NewEntity = taskTemplate.ToHomeworkTask(homeworkIds[i]) + })) + .ToList(); + + var tasksToSave = taskPairs.Select(x => x.NewEntity); + await _tasksRepository.AddRangeAsync(tasksToSave); + + foreach (var pair in taskPairs) + { + if (!string.IsNullOrEmpty(pair.Template.LtiLaunchUrl)) + { + await _tasksRepository.AddLtiUrlAsync(pair.NewEntity.Id, pair.Template.LtiLaunchUrl); + } + } if (studentIds.Any()) { @@ -302,6 +304,8 @@ public async Task GetUserCoursesAsync(string userId, string role) var result = await _courseFilterService.ApplyFiltersToCourses( userId, coursesWithValues.Select(c => c.ToCourseDto()).ToArray()); + await FillLtiUrlsForCourseDtos(result); + if (role == Roles.ExpertRole) { foreach (var courseDto in result) @@ -381,5 +385,25 @@ await _courseMatesRepository.FindAll(x => x.CourseId == courseId && x.StudentId await _context.SaveChangesAsync(); return true; } + + private async Task FillLtiUrlsForCourseDtos(IEnumerable courses) + { + var allTasks = courses.SelectMany(c => c.Homeworks).SelectMany(h => h.Tasks).ToList(); + + if (allTasks.Any()) + { + var taskIds = allTasks.Select(t => t.Id).ToArray(); + + var ltiUrls = await _tasksRepository.GetLtiUrlsForTasksAsync(taskIds); + + foreach (var taskDto in allTasks) + { + if (ltiUrls.TryGetValue(taskDto.Id, out var url)) + { + taskDto.LtiLaunchUrl = url; + } + } + } + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index a785c0365..eb234d33c 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -41,15 +41,13 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo homework.DeadlineDate)); } - var homeworkId = await _homeworksRepository.AddAsync(homework); + await _homeworksRepository.AddAsync(homework); if (homeworkViewModel.Tasks != null && homework.Tasks != null) { - // Превращаем в списки для доступа по индексу var createdTasks = homework.Tasks.ToList(); var taskModels = homeworkViewModel.Tasks; - // Проходимся по списку и сохраняем URL, если он был передан for (var i = 0; i < createdTasks.Count && i < taskModels.Count; i++) { var url = taskModels[i].LtiLaunchUrl; diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs index 207d6d0e3..ff1ba3cfb 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using HwProj.CoursesService.API.Models; using HwProj.Models; @@ -12,5 +13,6 @@ public interface ITasksService Task DeleteTaskAsync(long taskId); Task UpdateTaskAsync(long taskId, HomeworkTask update, ActionOptions options, string? ltiLaunchUrl = null); Task GetTaskLtiUrlAsync(long taskId); + Task> GetLtiUrlsForTasksAsync(long[] taskIds); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs index 5f8420ef4..fa22ab7c1 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using HwProj.CoursesService.API.Models; using HwProj.CoursesService.API.Repositories; @@ -109,5 +110,10 @@ public async Task UpdateTaskAsync(long taskId, HomeworkTask update { return await _tasksRepository.GetLtiUrlAsync(taskId); } + + public async Task> GetLtiUrlsForTasksAsync(long[] taskIds) + { + return await _tasksRepository.GetLtiUrlsForTasksAsync(taskIds); + } } } diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index f74ad7cc4..7543f4938 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -1576,6 +1576,44 @@ export interface MentorToAssignedStudentsDTO { */ selectedStudentsIds?: Array; } +/** + * + * @export + * @interface MocktoolCallbackBody + */ +export interface MocktoolCallbackBody { + /** + * + * @type {string} + * @memberof MocktoolCallbackBody + */ + idToken?: string; +} +/** + * + * @export + * @interface MocktoolLoginBody + */ +export interface MocktoolLoginBody { + /** + * + * @type {string} + * @memberof MocktoolLoginBody + */ + iss?: string; + /** + * + * @type {string} + * @memberof MocktoolLoginBody + */ + loginHint?: string; + /** + * + * @type {string} + * @memberof MocktoolLoginBody + */ + ltiMessageHint?: string; +} /** * * @export @@ -7589,7 +7627,7 @@ export const LtiAuthApiFetchParamCreator = function (configuration?: Configurati ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options: any = {}): FetchArgs { const localVarPath = `/api/lti/authorize`; const localVarUrlObj = url.parse(localVarPath, true); - const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -8094,6 +8132,211 @@ export class LtiToolsApi extends BaseAPI { return LtiToolsApiFp(this.configuration).ltiToolsGetAll(options)(this.fetch, this.basePath); } +} +/** + * MockToolApi - fetch parameter creator + * @export + */ +export const MockToolApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {string} [idToken] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolCallback(idToken?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/mocktool/callback`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + const localVarFormParams = new URLSearchParams(); + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (idToken !== undefined) { + localVarFormParams.set('id_token', idToken as any); + } + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + 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); + localVarRequestOptions.body = localVarFormParams.toString(); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} [iss] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolLogin(iss?: string, loginHint?: string, ltiMessageHint?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/mocktool/login`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + const localVarFormParams = new URLSearchParams(); + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (iss !== undefined) { + localVarFormParams.set('iss', iss as any); + } + + if (loginHint !== undefined) { + localVarFormParams.set('login_hint', loginHint as any); + } + + if (ltiMessageHint !== undefined) { + localVarFormParams.set('lti_message_hint', ltiMessageHint as any); + } + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + 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); + localVarRequestOptions.body = localVarFormParams.toString(); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * MockToolApi - functional programming interface + * @export + */ +export const MockToolApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {string} [idToken] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolCallback(idToken?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = MockToolApiFetchParamCreator(configuration).mockToolCallback(idToken, 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 {string} [iss] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolLogin(iss?: string, loginHint?: string, ltiMessageHint?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = MockToolApiFetchParamCreator(configuration).mockToolLogin(iss, loginHint, ltiMessageHint, 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; + } + }); + }; + }, + } +}; + +/** + * MockToolApi - factory interface + * @export + */ +export const MockToolApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {string} [idToken] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolCallback(idToken?: string, options?: any) { + return MockToolApiFp(configuration).mockToolCallback(idToken, options)(fetch, basePath); + }, + /** + * + * @param {string} [iss] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolLogin(iss?: string, loginHint?: string, ltiMessageHint?: string, options?: any) { + return MockToolApiFp(configuration).mockToolLogin(iss, loginHint, ltiMessageHint, options)(fetch, basePath); + }, + }; +}; + +/** + * MockToolApi - object-oriented interface + * @export + * @class MockToolApi + * @extends {BaseAPI} + */ +export class MockToolApi extends BaseAPI { + /** + * + * @param {string} [idToken] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MockToolApi + */ + public mockToolCallback(idToken?: string, options?: any) { + return MockToolApiFp(this.configuration).mockToolCallback(idToken, options)(this.fetch, this.basePath); + } + + /** + * + * @param {string} [iss] + * @param {string} [loginHint] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MockToolApi + */ + public mockToolLogin(iss?: string, loginHint?: string, ltiMessageHint?: string, options?: any) { + return MockToolApiFp(this.configuration).mockToolLogin(iss, loginHint, ltiMessageHint, options)(this.fetch, this.basePath); + } + } /** * NotificationsApi - fetch parameter creator @@ -9989,7 +10232,7 @@ export const TasksApiFetchParamCreator = function (configuration?: Configuration // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); - const needsSerialization = ("CreateTaskViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + const needsSerialization = ("CreateTaskVCreateTaskViewModeliewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; localVarRequestOptions.body = needsSerialization ? JSON.stringify(body || {}) : (body || ""); return { diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index a2c661f57..cd4badaff 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -35,7 +35,7 @@ import ErrorIcon from '@mui/icons-material/Error'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import SwitchAccessShortcutIcon from '@mui/icons-material/SwitchAccessShortcut'; import Lodash from "lodash"; -import { LtiImportButton } from "../Tasks/LtiImportButton"; +import { LtiImportButton, LtiItemDto } from "../Tasks/LtiImportButton"; interface ICourseExperimentalProps { homeworks: HomeworkViewModel[] @@ -375,18 +375,7 @@ export const CourseExperimental: FC = (props) => { const isTest = tags.includes(TestTag) const isBonus = tags.includes(BonusTag) - const ratingCandidate = Lodash(homeworks - .map(h => h.tasks![0]) - .filter(x => { - if (x === undefined) return false - const xIsTest = isTestWork(x) - const xIsBonus = isBonusWork(x) - return x.id! > 0 && (isTest && xIsTest || isBonus && xIsBonus || !isTest && !isBonus && !xIsTest && !xIsBonus) - })) - .map(x => x.maxRating!) - .groupBy(x => [x]) - .entries() - .sortBy(x => x[1].length).last()?.[1][0] + const ratingCandidate = calculateSuggestedRating(homework); const task = { homeworkId: homework.id, @@ -482,6 +471,67 @@ export const CourseExperimental: FC = (props) => { } + const calculateSuggestedRating = (homework: HomeworkViewModel) => { + const tags = homework.tags!; + const isTest = tags.includes(TestTag); + const isBonus = tags.includes(BonusTag); + + const ratingCandidate = Lodash(homeworks + .map(h => h.tasks![0]) + .filter(x => { + if (x === undefined) return false; + const xIsTest = isTestWork(x); + const xIsBonus = isBonusWork(x); + return x.id! > 0 && ((isTest && xIsTest) || (isBonus && xIsBonus) || (!isTest && !isBonus && !xIsTest && !xIsBonus)); + })) + .map(x => x.maxRating!) + .groupBy(x => [x]) + .entries() + .sortBy(x => x[1].length).last()?.[1][0]; + + return ratingCandidate || 10; + }; + + const handleLtiImport = (items: LtiItemDto[], homework: HomeworkViewModel) => { + let currentCounter = newTaskCounter; + + const suggestedRating = calculateSuggestedRating(homework); + + items.forEach(item => { + + if (!item.url) { + return; + } + + const taskId = currentCounter; + const description = item.text && item.text.trim().length > 0 + ? item.text + : ""; + + const newTask = { + id: taskId, + homeworkId: homework.id, + title: item.title || "External Task", + + description: description, + + maxRating: suggestedRating, + suggestedMaxRating: suggestedRating, + + tags: homework.tags, + isDeferred: homework.isDeferred, + + ltiLaunchUrl: item.url + }; + + props.onTaskUpdate({ task: newTask }); + + currentCounter--; + }); + + setNewTaskCounter(currentCounter); + }; + const renderGif = () => = (props) => { {t.title}{getTip(x)} + {(t.ltiLaunchUrl) && ( + + + + )} )} @@ -620,10 +687,10 @@ export const CourseExperimental: FC = (props) => { {x.id! < 0 && ( ) } @@ -631,10 +698,10 @@ export const CourseExperimental: FC = (props) => { {x.id! < 0 && (
window.location.reload()} + toolId={1} + // Передаем данные в нашу новую функцию + onImport={(items) => handleLtiImport(items, x)} />
)} diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 9ee3efdaa..24b15ae8f 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -237,7 +237,8 @@ const CourseHomeworkEditor: FC<{ const task: CreateTaskViewModel = { ...t, title: t.title!, - maxRating: t.maxRating! + maxRating: t.maxRating!, + ltiLaunchUrl: t.ltiLaunchUrl } return task }) : [] diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index faa51bbb6..93deb7a26 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -98,7 +98,8 @@ const CourseTaskEditor: FC<{ description: description, deadlineDateNotSet: metadata?.hasDeadline === true && !metadata.deadlineDate, maxRating: maxRating, - hasErrors: hasErrors + hasErrors: hasErrors, + ltiLaunchUrl: props.speculativeTask.ltiLaunchUrl } props.onUpdate({task: update}) }, [title, description, maxRating, metadata, hasErrors]) @@ -117,6 +118,7 @@ const CourseTaskEditor: FC<{ description: description, maxRating: maxRating, actionOptions: editOptions, + ltiLaunchUrl: props.speculativeTask.ltiLaunchUrl } const updatedTask = isNewTask @@ -193,8 +195,6 @@ const CourseTaskEditor: FC<{ /> - {/* LTI КНОПКА ОТСЮДА УДАЛЕНА, ТАК КАК ОНА ТЕПЕРЬ В РОДИТЕЛЕ */} - {metadata && homeworkPublicationDateIsSet && void; + onImport: (items: LtiItemDto[]) => void; } -export const LtiImportButton: FC = ({ homeworkId, courseId, toolId, onTasksAdded }) => { +export const LtiImportButton: FC = ({ courseId, toolId, onImport }) => { const [isLoading, setIsLoading] = useState(false); const submitLtiForm = (formData: any) => { - if (!formData || !formData.actionUrl) { - console.error("Invalid LTI Form Data"); - return; - } + // ... (код отправки формы тот же самый, без изменений) ... + const windowName = "lti_popup_" + new Date().getTime(); + const width = 800; const height = 700; + const left = (window.screen.width - width) / 2; + const top = (window.screen.height - height) / 2; + window.open('about:blank', windowName, `width=${width},height=${height},top=${top},left=${left},resizable,scrollbars,status`); const form = document.createElement("form"); form.method = formData.method; form.action = formData.actionUrl; - form.target = "_blank"; + form.target = windowName; if (formData.fields) { Object.entries(formData.fields).forEach(([key, value]) => { @@ -42,94 +48,54 @@ export const LtiImportButton: FC = ({ homeworkId, courseId const handleStartLti = async () => { setIsLoading(true); try { - // isDeepLink = true const response = await ApiSingleton.ltiAuthApi.ltiAuthStartLti( - undefined, - String(courseId), - String(toolId), - true + undefined, String(courseId), String(toolId), true ); - - // Обработка ответа (если NSwag вернул Response вместо JSON) let dto = response; if (response && typeof (response as any).json === 'function') { dto = await (response as any).json(); } - submitLtiForm(dto); - - // Снимаем лоадер через 15 секунд (если пользователь просто закрыл вкладку и ничего не выбрал) - setTimeout(() => setIsLoading(false), 15000); - + setTimeout(() => setIsLoading(false), 30000); // Тайм-аут побольше } catch (e) { console.error(e); - alert("Не удалось запустить инструмент"); setIsLoading(false); } }; - // 3. Слушаем ответ от вкладки с Инструментом useEffect(() => { - const handleLtiMessage = async (event: MessageEvent) => { - // Проверяем, что это сообщение от нашего LTI контроллера + const handleLtiMessage = (event: MessageEvent) => { if (event.data && event.data.type === 'LTI_DEEP_LINK_SUCCESS') { const payload = event.data.payload; - // Приводим к массиву (даже если вернулся один элемент) - const items = Array.isArray(payload) ? payload : [payload]; - - if (items.length === 0) return; + const rawItems = Array.isArray(payload) ? payload : [payload]; - setIsLoading(true); - try { - let count = 0; - - // Создаем задачи по очереди - for (const item of items) { - const newTask: CreateTaskViewModel = { - title: item.title || "External Task", - - // ИЗМЕНЕНИЕ: Простое описание без ссылки - description: "Это интерактивное задание. Нажмите кнопку 'Перейти к выполнению', чтобы начать.", - - maxRating: 10, - hasDeadline: false, - isDeadlineStrict: false, - publicationDate: undefined, - - // ИЗМЕНЕНИЕ: Передаем URL в специальное поле - // (Убедитесь, что вы перегенерировали API клиент, и это поле доступно) - ltiLaunchUrl: item.url - }; - - await ApiSingleton.tasksApi.tasksAddTask(homeworkId, newTask); - count++; + const items = rawItems.map((item: any) => { + if (typeof item === 'string') { + try { + return JSON.parse(item); + } catch (e) { + console.error("Ошибка парсинга JSON от LTI:", item); + return null; + } } + return item; + }).filter(item => item !== null); - // Успех! - alert(`Успешно импортировано задач: ${count}`); - onTasksAdded(); // Обновляем список задач на странице - - } catch (e) { - console.error("Ошибка импорта", e); - alert("Произошла ошибка при создании задач."); - } finally { - setIsLoading(false); + if (items.length > 0) { + onImport(items); } + setIsLoading(false); } }; - window.addEventListener("message", handleLtiMessage); return () => window.removeEventListener("message", handleLtiMessage); - }, [homeworkId, onTasksAdded]); + }, [onImport]); return ( } > Импорт из внешнего инструмента From a21edf917afa86867fc423afa9b846984879cf6b Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Tue, 6 Jan 2026 20:11:16 +0300 Subject: [PATCH 10/26] refactor: deleted unnecessary folders --- .../HwProj.APIGateway.API/HwProj.APIGateway.API.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj index fbb5ef506..077296375 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj +++ b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj @@ -29,7 +29,6 @@ - From 1a92b69417da7cfb67385d821e24b951c82a4d97 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Wed, 7 Jan 2026 00:05:06 +0300 Subject: [PATCH 11/26] feat: did deeplinking --- .../Controllers/CoursesController.cs | 3 +- .../Lti/Controllers/LtiAuthController.cs | 2 +- .../ViewModels/CourseViewModels.cs | 4 + .../Controllers/CoursesController.cs | 3 +- .../Domains/MappingExtensions.cs | 4 + ...106172258_AddLtiToolIdToCourse.Designer.cs | 351 ++++++++++++++++++ .../20260106172258_AddLtiToolIdToCourse.cs | 22 ++ .../Migrations/CourseContextModelSnapshot.cs | 2 + .../Models/Course.cs | 1 + .../Models/CourseTemplate.cs | 1 + .../Services/CoursesService.cs | 4 +- hwproj.front/src/api/api.ts | 20 +- .../src/components/Courses/AddCourseInfo.tsx | 27 +- .../src/components/Courses/Course.tsx | 2 + .../components/Courses/CourseExperimental.tsx | 10 +- .../src/components/Courses/CreateCourse.tsx | 2 +- .../components/Courses/ICreateCourseState.tsx | 2 +- .../src/components/Tasks/LtiImportButton.tsx | 1 - 18 files changed, 442 insertions(+), 19 deletions(-) create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.Designer.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs index 78377976f..13e6d95b5 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs @@ -309,7 +309,8 @@ private async Task ToCourseViewModel(CourseDTO course) NewStudents = newStudents.ToArray(), Homeworks = course.Homeworks, IsCompleted = course.IsCompleted, - IsOpen = course.IsOpen + IsOpen = course.IsOpen, + LtiToolId = course.LtiToolId, }; } } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs index 5827012b3..601d18e87 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs @@ -177,7 +177,7 @@ public async Task StartLti( ["login_hint"] = userId, ["target_link_uri"] = targetUrl, ["lti_message_hint"] = messageHint, - ["client_id"] = "MyPlatformClientId" + ["client_id"] = tool.ClientId, } }; diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs index 18e2b51b6..106e5150c 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs @@ -18,6 +18,7 @@ public class CreateCourseViewModel public bool FetchStudents { get; set; } [Required] public bool IsOpen { get; set; } public long? BaseCourseId { get; set; } + public long? LtiToolId { get; set; } } public class UpdateCourseViewModel @@ -31,6 +32,7 @@ public class UpdateCourseViewModel [Required] public bool IsOpen { get; set; } public bool IsCompleted { get; set; } + public long? LtiToolId { get; set; } } public class CourseDTO : CoursePreview @@ -42,6 +44,7 @@ public class CourseDTO : CoursePreview public GroupViewModel[] Groups { get; set; } = Array.Empty(); public IEnumerable AcceptedStudents => CourseMates.Where(t => t.IsAccepted); public IEnumerable NewStudents => CourseMates.Where(t => !t.IsAccepted); + public long? LtiToolId { get; set; } } public class CourseViewModel @@ -51,6 +54,7 @@ public class CourseViewModel public string GroupName { get; set; } public bool IsOpen { get; set; } public bool IsCompleted { get; set; } + public long? LtiToolId { get; set; } public AccountDataDto[] Mentors { get; set; } public AccountDataDto[] AcceptedStudents { get; set; } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs index 97c845eb2..d8bee3b4f 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs @@ -124,7 +124,8 @@ public async Task UpdateCourse(long courseId, [FromBody] UpdateCo Name = courseViewModel.Name, GroupName = courseViewModel.GroupName, IsCompleted = courseViewModel.IsCompleted, - IsOpen = courseViewModel.IsOpen + IsOpen = courseViewModel.IsOpen, + LtiToolId = courseViewModel.LtiToolId }); return Ok(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs index ab5ff863e..7ee1f439f 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs @@ -91,6 +91,7 @@ public static CourseDTO ToCourseDto(this Course course) InviteCode = course.InviteCode, CourseMates = course.CourseMates.Select(cm => cm.ToCourseMateViewModel()).ToArray(), Homeworks = course.Homeworks.Select(h => h.ToHomeworkViewModel()).ToArray(), + LtiToolId = course.LtiToolId, }; public static CoursePreview ToCoursePreview(this Course course) @@ -134,6 +135,7 @@ public static CourseTemplate ToCourseTemplate(this CreateCourseViewModel createC Name = createCourseViewModel.Name, GroupName = string.Join(", ", createCourseViewModel.GroupNames), IsOpen = createCourseViewModel.IsOpen, + LtiToolId = createCourseViewModel.LtiToolId, }; public static CourseTemplate ToCourseTemplate(this CourseDTO course) @@ -143,6 +145,7 @@ public static CourseTemplate ToCourseTemplate(this CourseDTO course) GroupName = course.GroupName, IsOpen = course.IsOpen, Homeworks = course.Homeworks.Select(h => h.ToHomeworkTemplate()).ToList(), + LtiToolId = course.LtiToolId, }; public static HomeworkTemplate ToHomeworkTemplate(this HomeworkViewModel homework) @@ -175,6 +178,7 @@ public static Course ToCourse(this CourseTemplate courseTemplate) Name = courseTemplate.Name, GroupName = courseTemplate.GroupName, IsOpen = courseTemplate.IsOpen, + LtiToolId = courseTemplate.LtiToolId, }; public static Homework ToHomework(this HomeworkTemplate homeworkTemplate, long courseId) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.Designer.cs new file mode 100644 index 000000000..0ff3ea68e --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.Designer.cs @@ -0,0 +1,351 @@ +// +using System; +using HwProj.CoursesService.API.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace HwProj.CoursesService.API.Migrations +{ + [DbContext(typeof(CourseContext))] + [Migration("20260106172258_AddLtiToolIdToCourse")] + partial class AddLtiToolIdToCourse + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("MentorId"); + + b.Property("StudentId"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Assignments"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupName"); + + b.Property("InviteCode"); + + b.Property("IsCompleted"); + + b.Property("IsOpen"); + + b.Property("LtiToolId"); + + b.Property("MentorIds"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("FilterJson"); + + b.HasKey("Id"); + + b.ToTable("CourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("IsAccepted"); + + b.Property("StudentId"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("CourseMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupId"); + + b.Property("StudentId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasAlternateKey("GroupId", "StudentId"); + + b.ToTable("GroupMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("DeadlineDate"); + + b.Property("Description"); + + b.Property("HasDeadline"); + + b.Property("IsDeadlineStrict"); + + b.Property("PublicationDate"); + + b.Property("Tags"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("DeadlineDate"); + + b.Property("Description"); + + b.Property("HasDeadline"); + + b.Property("HomeworkId"); + + b.Property("IsDeadlineStrict"); + + b.Property("MaxRating"); + + b.Property("PublicationDate"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("HomeworkId"); + + b.ToTable("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + { + b.Property("TaskId"); + + b.Property("LtiLaunchUrl") + .IsRequired(); + + b.Property("ToolId"); + + b.HasKey("TaskId"); + + b.ToTable("TaskLtiUrls"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.Property("CourseMateId"); + + b.Property("Description"); + + b.Property("Tags"); + + b.HasKey("CourseMateId"); + + b.ToTable("StudentCharacteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupId"); + + b.Property("TaskId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("TasksModels"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Answer") + .HasMaxLength(1000); + + b.Property("IsPrivate"); + + b.Property("LecturerId"); + + b.Property("StudentId"); + + b.Property("TaskId"); + + b.Property("Text") + .HasMaxLength(1000); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Questions"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.Property("CourseId"); + + b.Property("UserId"); + + b.Property("CourseFilterId"); + + b.HasKey("CourseId", "UserId"); + + b.HasIndex("CourseFilterId"); + + b.ToTable("UserToCourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("Assignments") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("CourseMates") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group") + .WithMany("GroupMates") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("Homeworks") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") + .WithMany("Tasks") + .HasForeignKey("HomeworkId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") + .WithOne() + .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", "TaskId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseMate") + .WithOne("Characteristics") + .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group") + .WithMany("Tasks") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") + .WithMany() + .HasForeignKey("CourseFilterId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.cs new file mode 100644 index 000000000..d5eff9f26 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace HwProj.CoursesService.API.Migrations +{ + public partial class AddLtiToolIdToCourse : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LtiToolId", + table: "Courses", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LtiToolId", + table: "Courses"); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs index 415275f32..09aec4700 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -52,6 +52,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsOpen"); + b.Property("LtiToolId"); + b.Property("MentorIds"); b.Property("Name"); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs index e5f6af8a3..435d84de1 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs @@ -16,5 +16,6 @@ public class Course : IEntity public List CourseMates { get; set; } = new List(); public List Homeworks { get; set; } = new List(); public List Assignments { get; set; } = new List(); + public long? LtiToolId { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs index 35e66b44e..09b225007 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs @@ -11,5 +11,6 @@ public class CourseTemplate public bool IsOpen { get; set; } public List Homeworks { get; set; } = new List(); + public long? LtiToolId { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs index 928e548d4..9d8049c7c 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -148,6 +148,7 @@ private async Task AddFromTemplateAsync(CourseTemplate courseTemplate, L course.MentorIds = mentorId; course.InviteCode = Guid.NewGuid().ToString(); var courseId = await _coursesRepository.AddAsync(course); + course.LtiToolId = courseTemplate.LtiToolId; var homeworks = courseTemplate.Homeworks.Select(hwTemplate => hwTemplate.ToHomework(courseId)); var homeworkIds = await _homeworksRepository.AddRangeAsync(homeworks); @@ -213,7 +214,8 @@ public async Task UpdateAsync(long courseId, Course updated) Name = updated.Name, GroupName = updated.GroupName, IsCompleted = updated.IsCompleted, - IsOpen = updated.IsOpen + IsOpen = updated.IsOpen, + LtiToolId = updated.LtiToolId, }); } diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 7543f4938..38544ffc4 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -477,6 +477,12 @@ export interface CourseViewModel { * @memberof CourseViewModel */ isCompleted?: boolean; + /** + * + * @type {number} + * @memberof CourseViewModel + */ + ltiToolId?: number; /** * * @type {Array} @@ -544,6 +550,12 @@ export interface CreateCourseViewModel { * @memberof CreateCourseViewModel */ baseCourseId?: number; + /** + * + * @type {number} + * @memberof CreateCourseViewModel + */ + ltiToolId?: number; } /** * @@ -2788,6 +2800,12 @@ export interface UpdateCourseViewModel { * @memberof UpdateCourseViewModel */ isCompleted?: boolean; + /** + * + * @type {number} + * @memberof UpdateCourseViewModel + */ + ltiToolId?: number; } /** * @@ -10232,7 +10250,7 @@ export const TasksApiFetchParamCreator = function (configuration?: Configuration // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 localVarUrlObj.search = null; localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); - const needsSerialization = ("CreateTaskVCreateTaskViewModeliewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + const needsSerialization = ("CreateTaskViewModel" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; localVarRequestOptions.body = needsSerialization ? JSON.stringify(body || {}) : (body || ""); return { diff --git a/hwproj.front/src/components/Courses/AddCourseInfo.tsx b/hwproj.front/src/components/Courses/AddCourseInfo.tsx index e19df55ed..2ee44c720 100644 --- a/hwproj.front/src/components/Courses/AddCourseInfo.tsx +++ b/hwproj.front/src/components/Courses/AddCourseInfo.tsx @@ -115,33 +115,44 @@ const AddCourseInfo: FC = ({state, setState}) => { option.name ?? ""} + // Как отображать объект в списке (берем имя) + getOptionLabel={(option) => option.name || "Без названия"} + // Текущее значение. Ищем объект в массиве по ID. value={ - state.ltiToolId == null - ? null - : state.ltiTools.find(t => t.id === state.ltiToolId) ?? null + state.ltiToolId + ? state.ltiTools?.find(t => t.id === state.ltiToolId) || null + : null } + // Обработчик изменения onChange={(_, newValue) => { setState(prev => ({ ...prev, - ltiToolId: newValue?.id ?? null + // Если выбрали (newValue не null), берем ID. Иначе undefined. + ltiToolId: newValue ? newValue.id : undefined })); }} + // Рендер инпута renderInput={(params) => ( )} + + // Позволяет очистить выбор (крестик) + clearOnEscape fullWidth /> diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index c2309e0fe..aed9291ad 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -259,6 +259,7 @@ const Course: React.FC = () => { const setCurrentState = async () => { const course = await ApiSingleton.coursesApi.coursesGetCourseData(+courseId!) + console.log(course) // У пользователя изменилась роль (иначе он не может стать лектором в курсе), // однако он все ещё использует токен с прежней ролью @@ -489,6 +490,7 @@ const Course: React.FC = () => { {tabValue === "homeworks" && = (props) => { {x.id! < 0 && ( ) } - {x.id! < 0 && ( + {x.id! < 0 && props.ltiToolId && (
handleLtiImport(items, x)} /> diff --git a/hwproj.front/src/components/Courses/CreateCourse.tsx b/hwproj.front/src/components/Courses/CreateCourse.tsx index b086b8d52..1f1612156 100644 --- a/hwproj.front/src/components/Courses/CreateCourse.tsx +++ b/hwproj.front/src/components/Courses/CreateCourse.tsx @@ -57,7 +57,7 @@ export const CreateCourse: FC = () => { fetchingGroups: false, courseIsLoading: false, ltiTools: [], - ltiToolId: null, + ltiToolId: undefined, }) const {activeStep, completedSteps, baseCourses, selectedBaseCourse} = state diff --git a/hwproj.front/src/components/Courses/ICreateCourseState.tsx b/hwproj.front/src/components/Courses/ICreateCourseState.tsx index 979c29f5e..c9af4cc2a 100644 --- a/hwproj.front/src/components/Courses/ICreateCourseState.tsx +++ b/hwproj.front/src/components/Courses/ICreateCourseState.tsx @@ -35,7 +35,7 @@ export interface ICreateCourseState { courseIsLoading: boolean; ltiTools: LtiToolDto[]; - ltiToolId: number | null; + ltiToolId: number | undefined; } export interface IStepComponentProps { diff --git a/hwproj.front/src/components/Tasks/LtiImportButton.tsx b/hwproj.front/src/components/Tasks/LtiImportButton.tsx index 9186fca3c..4ca9c85da 100644 --- a/hwproj.front/src/components/Tasks/LtiImportButton.tsx +++ b/hwproj.front/src/components/Tasks/LtiImportButton.tsx @@ -19,7 +19,6 @@ export const LtiImportButton: FC = ({ courseId, toolId, on const [isLoading, setIsLoading] = useState(false); const submitLtiForm = (formData: any) => { - // ... (код отправки формы тот же самый, без изменений) ... const windowName = "lti_popup_" + new Date().getTime(); const width = 800; const height = 700; const left = (window.screen.width - width) / 2; From a83f7180270fce923323af2e6f06e971277827e0 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Wed, 7 Jan 2026 04:30:38 +0300 Subject: [PATCH 12/26] feat: made it possible to take into account the maximum score when importing from an external tool, and also made validating jwt tokens --- .../Lti/Controllers/LtiAuthController.cs | 9 +- .../LtiDeepLinkingReturnController.cs | 89 +++-- .../Lti/Controllers/MockToolController.cs | 154 +++++--- .../Lti/Models/LtiToolConfig.cs | 4 +- .../Lti/Models/LtiToolDto.cs | 2 + .../Lti/Services/ILtiKeyService.cs | 10 + .../Lti/Services/ILtiToolService.cs | 1 + .../Lti/Services/LtiKeyService.cs | 31 ++ .../Lti/Services/LtiToolService.cs | 38 +- .../HwProj.APIGateway.API/Startup.cs | 1 + ...eLtiToolIdToHomeworkTaskLtiUrl.Designer.cs | 349 ++++++++++++++++++ ...514_RemoveLtiToolIdToHomeworkTaskLtiUrl.cs | 23 ++ .../Migrations/CourseContextModelSnapshot.cs | 2 - .../Models/HomeworkTaskLtiUrl.cs | 2 - .../components/Courses/CourseExperimental.tsx | 10 +- .../src/components/Tasks/LtiImportButton.tsx | 3 +- 16 files changed, 618 insertions(+), 110 deletions(-) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiKeyService.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.Designer.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs index 601d18e87..9a9d8c9ba 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs @@ -35,7 +35,7 @@ public async Task AuthorizeLti( [FromQuery(Name = "nonce")] string nonce, [FromQuery(Name = "lti_message_hint")] string ltiMessageHint) { - LtiHintPayload payload; + LtiHintPayload? payload; try { var json = this.protector.Unprotect(ltiMessageHint); @@ -46,7 +46,12 @@ public async Task AuthorizeLti( return BadRequest("Invalid or expired lti_message_hint"); } - var tool = await toolService.GetByIdAsync(long.Parse(payload.ToolId)); + if (payload == null) + { + return BadRequest("Invalid or expired lti_message_hint"); + } + + var tool = await toolService.GetByIdAsync(long.Parse(payload.ToolId!)); if (tool == null) { return BadRequest("Tool not found"); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs index dcc89b1db..24005740a 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs @@ -1,81 +1,104 @@ +using System; using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Text.Json; +using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.IdentityModel.Tokens; // Убедитесь, что установлен пакет Microsoft.IdentityModel.Tokens +using Microsoft.IdentityModel.Tokens; namespace HwProj.APIGateway.API.Lti.Controllers; [Route("api/lti")] [ApiController] -public class LtiDeepLinkingReturnController : ControllerBase +public class LtiDeepLinkingReturnController( + ILtiToolService toolService, + ILtiKeyService ltiKeyService + ) : ControllerBase { - // Инструмент отправляет форму с полем JWT на этот адрес [HttpPost("deepLinkReturn")] - [AllowAnonymous] // Анонимно, так как запрос идет от браузера при редиректе из тула - public IActionResult OnDeepLinkingReturn([FromForm] IFormCollection form) + [AllowAnonymous] + public async Task OnDeepLinkingReturnAsync([FromForm] IFormCollection form) { - // 1. Проверяем наличие параметра JWT if (!form.ContainsKey("JWT")) { return BadRequest("Missing JWT parameter"); } string tokenString = form["JWT"]!; - - // 2. Разбиваем токен на части (Header.Payload.Signature) - var parts = tokenString.Split('.'); - if (parts.Length != 3) + var handler = new JwtSecurityTokenHandler(); + + if (!handler.CanReadToken(tokenString)) { return BadRequest("Invalid JWT structure"); } - // В ПРОДАКШЕНЕ ЗДЕСЬ НУЖНА ВАЛИДАЦИЯ ПОДПИСИ (Signature) - // Для этого нужно достать Public Key инструмента (JWKS) и проверить подпись. - // Пока мы просто доверяем содержимому для тестов. + var unverifiedToken = handler.ReadJwtToken(tokenString); + var issuer = unverifiedToken.Issuer; + + // 2. Ищем инструмент в БД по Issuer + // (Предполагается, что у toolService есть метод GetByIssuerAsync или аналогичный) + var tool = await toolService.GetByIssuerAsync(issuer); + if (tool == null) + { + return Unauthorized($"Unknown tool issuer: {issuer}"); + } + + // 3. Получаем публичные ключи (JWKS) инструмента через сервис + var signingKeys = await ltiKeyService.GetKeysAsync(tool.JwksEndpoint); - // 3. Декодируем Payload из Base64Url - string payloadJson; + // 4. Валидируем подпись try { - payloadJson = Base64UrlEncoder.Decode(parts[1]); + handler.ValidateToken(tokenString, new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = issuer, + + ValidateAudience = true, + ValidAudience = tool.ClientId, + + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + + ValidateIssuerSigningKey = true, + IssuerSigningKeys = signingKeys + }, out var validatedToken); } - catch + catch (Exception ex) { - return BadRequest("Invalid Base64 in JWT"); + return BadRequest($"Token signature validation failed: {ex.Message}"); } - - // 4. Парсим JSON вручную с помощью JsonDocument - // Это самый надежный способ, который не падает из-за несовпадения типов C# классов. - using var doc = JsonDocument.Parse(payloadJson); - var root = doc.RootElement; - // Имя поля по стандарту LTI 1.3 Deep Linking - var itemsClaimName = "https://purl.imsglobal.org/spec/lti-dl/claim/content_items"; + const string itemsClaimName = "https://purl.imsglobal.org/spec/lti-dl/claim/content_items"; var resultList = new List(); - // 5. Ищем массив content_items - if (root.TryGetProperty(itemsClaimName, out var itemsElement) && itemsElement.ValueKind == JsonValueKind.Array) + if (unverifiedToken.Payload.TryGetValue(itemsClaimName, out var itemsObject)) { - foreach (var rawItem in itemsElement.EnumerateArray()) + var jsonString = itemsObject.ToString(); + if (!string.IsNullOrEmpty(jsonString)) { - resultList.Add(rawItem.Clone().ToString()); + using var doc = JsonDocument.Parse(jsonString); + if (doc.RootElement.ValueKind == JsonValueKind.Array) + { + foreach (var rawItem in doc.RootElement.EnumerateArray()) + { + resultList.Add(rawItem.Clone().ToString()); + } + } } } - // Если список пуст (инструмент ничего не выбрал или формат неверен) if (resultList.Count == 0) { - // Просто закрываем окно return Content("", "text/html"); } - // 6. Сериализуем список обратно в JSON для передачи на фронтенд HwProj var responsePayloadJson = JsonSerializer.Serialize(resultList); - // 7. Генерируем HTML-страницу, которая передаст данные родительскому окну и закроется var htmlResponse = $@" diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs index 6c87467a3..ec2b2b9af 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs @@ -1,25 +1,51 @@ using System; using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; using System.IdentityModel.Tokens.Jwt; using System.Linq; -using System.Security.Claims; +using System.Net.Http; // Добавлено +using System.Security.Cryptography; using System.Text.Json; -using Newtonsoft.Json.Linq; +using System.Threading.Tasks; // Добавлено +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; [Route("api/mocktool")] [ApiController] public class MockToolController : ControllerBase { + private static readonly RsaSecurityKey _signingKey; + private static readonly string _keyId; + + // Добавляем фабрику для выполнения HTTP-запросов за ключами Платформы + private readonly IHttpClientFactory _httpClientFactory; + + // Внедряем зависимость через конструктор + public MockToolController(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + static MockToolController() + { + var rsa = RSA.Create(2048); + _keyId = Guid.NewGuid().ToString(); + _signingKey = new RsaSecurityKey(rsa) { KeyId = _keyId }; + } + + [HttpGet("jwks")] + public IActionResult GetJwks() + { + var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(_signingKey); + return Ok(new { keys = new[] { jwk } }); + } + [HttpPost("login")] public IActionResult Login([FromForm] string iss, [FromForm] string login_hint, [FromForm] string lti_message_hint) { -// Эмулируем редирект обратно на Платформу (Authorize) -// В реальном мире тут инструмент генерирует nonce и state var callbackUrl = $"{iss}/api/lti/authorize?" + $"client_id=mock-tool-client-id&" + $"response_type=id_token&" + - $"redirect_uri=http://localhost:5000/api/mocktool/callback&" + // Куда вернуть токен + $"redirect_uri=http://localhost:5000/api/mocktool/callback&" + $"login_hint={login_hint}&" + $"lti_message_hint={lti_message_hint}&" + $"scope=openid&state=xyz&nonce=123"; @@ -27,84 +53,114 @@ public IActionResult Login([FromForm] string iss, [FromForm] string login_hint, return Redirect(callbackUrl); } + // Делаем метод асинхронным (async Task) [HttpPost("callback")] - public IActionResult Callback([FromForm] string id_token) + public async Task Callback([FromForm] string id_token) { var handler = new JwtSecurityTokenHandler(); - // Читаем входящий токен (без валидации подписи, т.к. это мок) - var token = handler.ReadJwtToken(id_token); + // 1. Читаем токен БЕЗ валидации, чтобы узнать, кто его прислал (Issuer) + if (!handler.CanReadToken(id_token)) return BadRequest("Invalid Token"); + var unverifiedToken = handler.ReadJwtToken(id_token); - // Достаем URL возврата - var settingsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings"); + string issuer = unverifiedToken.Issuer; // Это URL вашего HwProj (например, http://localhost:5000) + + // 2. Определяем адрес JWKS Платформы. + // В реальном LTI этот URL часто передается при регистрации. + // Для теста предположим, что HwProj отдает ключи по стандартному пути: + string platformJwksUrl = $"{issuer}/api/lti/jwks"; + + // 3. Скачиваем ключи Платформы + var client = _httpClientFactory.CreateClient(); + string jwksJson; + try + { + jwksJson = await client.GetStringAsync(platformJwksUrl); + } + catch + { + return BadRequest($"Не удалось скачать ключи HwProj по адресу {platformJwksUrl}"); + } + + var platformKeySet = new JsonWebKeySet(jwksJson); + + // 4. ВАЛИДИРУЕМ ВХОДЯЩИЙ ТОКЕН ОТ ПЛАТФОРМЫ + try + { + handler.ValidateToken(id_token, new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = issuer, // Токен должен быть от HwProj + + ValidateAudience = true, + ValidAudience = "mock-tool-client-id", // Токен должен быть предназначен НАМ (этому инструменту) + + ValidateLifetime = true, // Не протух ли? + + ValidateIssuerSigningKey = true, + IssuerSigningKeys = platformKeySet.Keys // Проверяем подпись ключами Платформы + }, out var validatedToken); + } + catch (Exception ex) + { + return Unauthorized($"Ошибка проверки подписи HwProj: {ex.Message}"); + } + + // --- Если мы здесь, токен от HwProj настоящий. Продолжаем логику --- + + var settingsClaim = unverifiedToken.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings"); if (settingsClaim == null) return BadRequest("No deep linking settings found"); var settings = JsonDocument.Parse(settingsClaim.Value); var returnUrl = settings.RootElement.GetProperty("deep_link_return_url").GetString(); - // --- ГЕНЕРАЦИЯ ОТВЕТА --- - - // 1. Формируем Content Items, используя обычные C# объекты (Arrays/Anonymous Objects) - // Это гарантирует правильный JSON на выходе. + // Формируем ответ (как и раньше) var contentItems = new List> { - new Dictionary + new() { ["type"] = "ltiResourceLink", - ["title"] = "Тестовая Задача из Мока", + ["title"] = "Тестовая Задача0 (Secure)", + ["url"] = "http://localhost:5000/mock/task/0", + ["text"] = "Задача проверена двусторонней подписью!", + ["scoreMaximum"] = 15 + }, + new() + { + ["type"] = "ltiResourceLink", + ["title"] = "Тестовая Задача1 (Secure)", ["url"] = "http://localhost:5000/mock/task/1", - ["text"] = "Описание тестовой задачи" + ["text"] = "Задача проверена двусторонней подписью!", + ["scoreMaximum"] = 20 } }; - - contentItems.Add(new Dictionary - { - ["type"] = "ltiResourceLink", - ["title"] = "Тестовая Задача из Мока2", - ["url"] = "http://localhost:5000/mock/task/2", - ["text"] = "Описание тестовой задачи2" - }); - // 2. Собираем Payload var payload = new JwtPayload { - { "iss", "MockTool" }, - { "aud", "HwProj" }, + { "iss", "http://localhost:5000" }, + { "aud", "mock-tool-client-id" }, // HwProj ожидает этот Audience (или свой ClientId, зависит от настроек HwProj) { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, { "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() }, - { "nonce", "random-nonce-123" }, // LTI требует nonce + { "nonce", "random-nonce-123" }, { "https://purl.imsglobal.org/spec/lti-dl/claim/message_type", "LtiDeepLinkingResponse" }, { "https://purl.imsglobal.org/spec/lti-dl/claim/version", "1.3.0" }, { "https://purl.imsglobal.org/spec/lti-dl/claim/content_items", contentItems } }; - // 3. Создаем Header с "пустой" подписью, чтобы библиотека не ругалась - // В реальном LTI здесь должен быть реальный ключ. Для мока делаем "unsigned". - var header = new JwtHeader(); - - // ВАЖНО: JwtSecurityTokenHandler по умолчанию не дает создать токен без подписи. - // Мы обойдем это, создав токен вручную, или просто подпишем "мусорным" ключом. - // Самый простой способ для мока - подписать любым ключом, т.к. принимающая сторона (пока) не проверяет подпись. - - var securityKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("secret-key-must-be-at-least-16-chars")); - var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(securityKey, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256); - - // Пересоздаем хедер с алгоритмом - header = new JwtHeader(credentials); - - // 4. Генерируем строку + // Подписываем НАШ ответ НАШИМ ключом + var credentials = new SigningCredentials(_signingKey, SecurityAlgorithms.RsaSha256); + var header = new JwtHeader(credentials); var responseToken = new JwtSecurityToken(header, payload); var responseString = handler.WriteToken(responseToken); - // Рисуем форму авто-сабмита или кнопку var html = $@" -

Интерфейс Инструмента (Mock)

-

Задача выбрана. Нажмите кнопку, чтобы вернуться в HwProj.

+

Tool Interface (Secure)

+

The incoming token from HwProj has been successfully verified!

- +
"; diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs index 5c7efd115..a18e4f5eb 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs @@ -1,10 +1,12 @@ namespace HwProj.APIGateway.API.Lti.Models { - public class LtiToolConfig() + public class LtiToolConfig { public long Id { get; set; } public string Name { get; set; } + public string Issuer { get; set; } public string ClientId { get; set; } + public string JwksEndpoint { get; set; } public string InitiateLoginUri { get; set; } public string LaunchUrl { get; set; } public string DeepLink { get; set; } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs index 6f34f58c1..3c6caadcc 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs @@ -4,6 +4,7 @@ public class LtiToolDto( long id, string name, string clientId, + string jwksEndpoint, string initiateLoginUri, string launchUrl, string deepLink) @@ -11,6 +12,7 @@ public class LtiToolDto( public long Id { get; init; } = id; public string Name { get; init; } = name; public string ClientId { get; init; } = clientId; + public string JwksEndpoint { get; set; } = jwksEndpoint; public string InitiateLoginUri { get; init; } = initiateLoginUri; public string LaunchUrl { get; init; } = launchUrl; public string DeepLink { get; init; } = deepLink; diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiKeyService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiKeyService.cs new file mode 100644 index 000000000..2570abbe7 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiKeyService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; + +namespace HwProj.APIGateway.API.Lti.Services; + +public interface ILtiKeyService +{ + Task?> GetKeysAsync(string jwksUrl); +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs index 3c4561f02..8bd777ed8 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs @@ -8,4 +8,5 @@ public interface ILtiToolService { Task> GetAllAsync(); Task GetByIdAsync(long id); + Task GetByIssuerAsync(string issuer); } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs new file mode 100644 index 000000000..f4cb5ebbf --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs @@ -0,0 +1,31 @@ +using Microsoft.IdentityModel.Tokens; +using System.Net.Http; +using System.Threading.Tasks; +using System.Collections.Concurrent; +using System.Collections.Generic; +using HwProj.APIGateway.API.Lti.Services; + +public class LtiKeyService(IHttpClientFactory httpClientFactory) : ILtiKeyService +{ + private static readonly ConcurrentDictionary _keyCache = new(); + + public async Task?> GetKeysAsync(string jwksUrl) + { + if (string.IsNullOrEmpty(jwksUrl)) return null; + + if (_keyCache.TryGetValue(jwksUrl, out var keySet)) + { + return keySet.Keys; + } + + var client = httpClientFactory.CreateClient(); + var json = await client.GetStringAsync(jwksUrl); + keySet = new JsonWebKeySet(json); + + // В продакшене здесь стоит добавить Expire Policy (например, MemoryCache), + // чтобы обновлять ключи раз в сутки. + _keyCache.TryAdd(jwksUrl, keySet); + + return keySet.Keys; + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs index d3ed5f16d..8c079ebd3 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs @@ -13,13 +13,7 @@ public class LtiToolService(IOptions> options) : ILtiToolSer public Task> GetAllAsync() { var result = _tools - .Select(t => new LtiToolDto( - t.Id, - t.Name, - t.ClientId, - t.InitiateLoginUri, - t.LaunchUrl, - t.DeepLink)) + .Select(MapToDto) .ToList() .AsReadOnly(); @@ -29,17 +23,27 @@ public Task> GetAllAsync() public Task GetByIdAsync(long id) { var cfg = _tools.FirstOrDefault(t => t.Id == id); - if (cfg == null) - return Task.FromResult(null); + return Task.FromResult(cfg == null ? null : MapToDto(cfg)); + } - var dto = new LtiToolDto( - cfg.Id, - cfg.Name, - cfg.ClientId, - cfg.InitiateLoginUri, - cfg.LaunchUrl, - cfg.DeepLink); + public Task GetByIssuerAsync(string issuer) + { + // Ищем конфиг, где Issuer совпадает с тем, что пришел в токене + var cfg = _tools.FirstOrDefault(t => t.Issuer == issuer); + return Task.FromResult(cfg == null ? null : MapToDto(cfg)); + } - return Task.FromResult(dto); + // Вынес создание DTO в отдельный метод, чтобы не дублировать код + private static LtiToolDto MapToDto(LtiToolConfig t) + { + return new LtiToolDto( + t.Id, + t.Name, + t.ClientId, + t.JwksEndpoint, + t.InitiateLoginUri, + t.LaunchUrl, + t.DeepLink + ); } } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index a9400eab7..2c9c45b34 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -86,6 +86,7 @@ public void ConfigureServices(IServiceCollection services) services.Configure>(Configuration.GetSection("LtiTools")); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddScoped(); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.Designer.cs new file mode 100644 index 000000000..233549e45 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.Designer.cs @@ -0,0 +1,349 @@ +// +using System; +using HwProj.CoursesService.API.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace HwProj.CoursesService.API.Migrations +{ + [DbContext(typeof(CourseContext))] + [Migration("20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl")] + partial class RemoveLtiToolIdToHomeworkTaskLtiUrl + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("MentorId"); + + b.Property("StudentId"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Assignments"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupName"); + + b.Property("InviteCode"); + + b.Property("IsCompleted"); + + b.Property("IsOpen"); + + b.Property("LtiToolId"); + + b.Property("MentorIds"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("FilterJson"); + + b.HasKey("Id"); + + b.ToTable("CourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("IsAccepted"); + + b.Property("StudentId"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("CourseMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupId"); + + b.Property("StudentId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasAlternateKey("GroupId", "StudentId"); + + b.ToTable("GroupMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("DeadlineDate"); + + b.Property("Description"); + + b.Property("HasDeadline"); + + b.Property("IsDeadlineStrict"); + + b.Property("PublicationDate"); + + b.Property("Tags"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("DeadlineDate"); + + b.Property("Description"); + + b.Property("HasDeadline"); + + b.Property("HomeworkId"); + + b.Property("IsDeadlineStrict"); + + b.Property("MaxRating"); + + b.Property("PublicationDate"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("HomeworkId"); + + b.ToTable("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + { + b.Property("TaskId"); + + b.Property("LtiLaunchUrl") + .IsRequired(); + + b.HasKey("TaskId"); + + b.ToTable("TaskLtiUrls"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.Property("CourseMateId"); + + b.Property("Description"); + + b.Property("Tags"); + + b.HasKey("CourseMateId"); + + b.ToTable("StudentCharacteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupId"); + + b.Property("TaskId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("TasksModels"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Answer") + .HasMaxLength(1000); + + b.Property("IsPrivate"); + + b.Property("LecturerId"); + + b.Property("StudentId"); + + b.Property("TaskId"); + + b.Property("Text") + .HasMaxLength(1000); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Questions"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.Property("CourseId"); + + b.Property("UserId"); + + b.Property("CourseFilterId"); + + b.HasKey("CourseId", "UserId"); + + b.HasIndex("CourseFilterId"); + + b.ToTable("UserToCourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("Assignments") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("CourseMates") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group") + .WithMany("GroupMates") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("Homeworks") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") + .WithMany("Tasks") + .HasForeignKey("HomeworkId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") + .WithOne() + .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", "TaskId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseMate") + .WithOne("Characteristics") + .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group") + .WithMany("Tasks") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") + .WithMany() + .HasForeignKey("CourseFilterId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.cs new file mode 100644 index 000000000..585a1b6fb --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace HwProj.CoursesService.API.Migrations +{ + public partial class RemoveLtiToolIdToHomeworkTaskLtiUrl : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ToolId", + table: "TaskLtiUrls"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ToolId", + table: "TaskLtiUrls", + nullable: false, + defaultValue: 0); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs index 09aec4700..dd3b2b142 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -193,8 +193,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LtiLaunchUrl") .IsRequired(); - b.Property("ToolId"); - b.HasKey("TaskId"); b.ToTable("TaskLtiUrls"); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs index 549878356..8ebde9143 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs @@ -11,7 +11,5 @@ public class HomeworkTaskLtiUrl [Required] public string LtiLaunchUrl { get; set; } - - public int ToolId { get; set; } } } \ No newline at end of file diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index abbd04070..560fdba0b 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -503,11 +503,15 @@ export const CourseExperimental: FC = (props) => { if (!item.url) { return; } - + + const defaultRating = calculateSuggestedRating(homework); const taskId = currentCounter; const description = item.text && item.text.trim().length > 0 ? item.text : ""; + const targetRating = (item.scoreMaximum && item.scoreMaximum > 0) + ? item.scoreMaximum + : defaultRating; const newTask = { id: taskId, @@ -516,8 +520,8 @@ export const CourseExperimental: FC = (props) => { description: description, - maxRating: suggestedRating, - suggestedMaxRating: suggestedRating, + maxRating: targetRating, + suggestedMaxRating: targetRating, tags: homework.tags, isDeferred: homework.isDeferred, diff --git a/hwproj.front/src/components/Tasks/LtiImportButton.tsx b/hwproj.front/src/components/Tasks/LtiImportButton.tsx index 4ca9c85da..549c3df35 100644 --- a/hwproj.front/src/components/Tasks/LtiImportButton.tsx +++ b/hwproj.front/src/components/Tasks/LtiImportButton.tsx @@ -5,8 +5,9 @@ import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; export interface LtiItemDto { title: string; - url: string; text?: string; + url: string; + scoreMaximum: number; } interface LtiImportButtonProps { From 548e1e3276c4844062dc72e939268be21af6594a Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Wed, 18 Feb 2026 20:47:24 +0300 Subject: [PATCH 13/26] feat: support test launches through LTI. Final grading is not included. --- .../Controllers/SolutionsController.cs | 1 + .../Lti/Controllers/LtiAuthController.cs | 121 +++++---- .../LtiDeepLinkingReturnController.cs | 6 - .../Lti/Controllers/MockToolController.cs | 241 +++++++++++++----- .../Lti/Models/LtiPlatformConfig.cs | 1 + .../Lti/Services/ILtiTokenService.cs | 9 + .../Lti/Services/LtiTokenService.cs | 65 ++++- .../Models/Solutions/UserTaskSolutions.cs | 1 + hwproj.front/src/api/api.ts | 99 ++++++- .../components/Courses/CourseExperimental.tsx | 1 - .../components/Solutions/LtiLaunchButton.tsx | 76 ++++++ .../Solutions/TaskSolutionsPage.tsx | 52 ++-- .../src/components/Tasks/LtiImportButton.tsx | 11 +- 13 files changed, 527 insertions(+), 157 deletions(-) create mode 100644 hwproj.front/src/components/Solutions/LtiLaunchButton.tsx diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs index 1ab2fe869..1f9eb9c4e 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -139,6 +139,7 @@ public async Task GetStudentSolution(long taskId, string studentI return Ok(new UserTaskSolutionsPageData { CourseId = course.Id, + LtiToolId = course.LtiToolId, CourseMates = accounts, TaskSolutions = taskSolutions }); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs index 9a9d8c9ba..deaadd7ae 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs @@ -63,28 +63,30 @@ public async Task AuthorizeLti( } string idToken; - if (payload.Type == "DeepLinking") + switch (payload.Type) { - idToken = tokenService.CreateDeepLinkingToken( - clientId: clientId, - toolId: payload.ToolId, - courseId: payload.CourseId, - targetLinkUri: tool.DeepLink, - userId: payload.UserId, - nonce: nonce - ); - } - else - { - // (Логика для обычного запуска, пока опустим) - idToken = tokenService.CreateDeepLinkingToken( - clientId: clientId, - courseId: payload.CourseId, - toolId: payload.ToolId, - targetLinkUri: tool.DeepLink, - userId: payload.UserId, - nonce: nonce - ); + case "DeepLinking": + idToken = tokenService.CreateDeepLinkingToken( + clientId: clientId, + toolId: payload.ToolId!, + courseId: payload.CourseId!, + targetLinkUri: tool.DeepLink, + userId: payload.UserId, + nonce: nonce + ); + break; + case "ResourceLink": + idToken = tokenService.CreateResourceLinkToken( + clientId: clientId, + toolId: payload.ToolId!, + courseId: payload.CourseId!, + targetLinkUri: tool.LaunchUrl, + userId: payload.UserId, + nonce: nonce, + resourceLinkId: payload.ResourceLinkId!); + break; + default: + return BadRequest("Invalid or expired lti_message_hint"); } var html = $""" @@ -108,6 +110,7 @@ public async Task StartLti( [FromQuery] string? resourceLinkId, [FromQuery] string? courseId, [FromQuery] string? toolId, + [FromQuery] string? ltiLaunchUrl, [FromQuery] bool isDeepLink = false) { var userId = User.FindFirstValue("_id"); @@ -116,23 +119,22 @@ public async Task StartLti( return Unauthorized("User ID not found"); } - LtiToolDto? tool; string targetUrl; LtiHintPayload payload; - if (isDeepLink) + if (courseId == null || toolId == null) { - if (courseId == null || toolId == null) - { - return BadRequest("For Deep Linking, courseId and toolId are required."); - } + return BadRequest("For Deep Linking, courseId and toolId are required."); + } - tool = await toolService.GetByIdAsync(long.Parse(toolId)); - if (tool == null) - { - return NotFound("Tool not found"); - } + var tool = await toolService.GetByIdAsync(long.Parse(toolId)); + if (tool == null) + { + return NotFound("Tool not found"); + } + if (isDeepLink) + { targetUrl = !string.IsNullOrEmpty(tool.DeepLink) ? tool.DeepLink : tool.LaunchUrl; @@ -145,22 +147,16 @@ public async Task StartLti( ToolId = toolId }; } - else if (!string.IsNullOrEmpty(resourceLinkId)) + else if (!string.IsNullOrEmpty(resourceLinkId) && !string.IsNullOrEmpty(ltiLaunchUrl)) { - // Здесь логика поиска тула может быть сложнее (через LinkService) - tool = await toolService.GetByIdAsync(1); - - if (tool == null) - { - return NotFound("Tool not found"); - } - - targetUrl = tool.LaunchUrl; + targetUrl = ltiLaunchUrl; payload = new LtiHintPayload { Type = "ResourceLink", UserId = userId, + CourseId = courseId, + ToolId = toolId, ResourceLinkId = resourceLinkId }; } @@ -189,17 +185,38 @@ public async Task StartLti( return Ok(dto); } - private async Task CheckTheRequest( - string issOfTheTool, - string clientId, - string redirectUri, - string loginHint) + [HttpGet("closeLtiSession")] + public IActionResult CloseLtiSession() { - // - client_id существует и соответствует зарегистрированному Tool - // - redirect_uri допустим - // - пользователь аутентифицирован (Authorize уже проверил) - // - можешь сверить login_hint с текущим пользователем и т.д. - return true; + const string htmlContent = @" + + + + + Сессия завершена + + + + +
+

Работа с инструментом завершена

+

Вкладка должна закрыться автоматически.

+

Если этого не произошло, нажмите кнопку ниже:

+ +
+ + "; + + return Content(htmlContent, "text/html"); } private class LtiHintPayload diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs index 24005740a..5fe337422 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs @@ -46,10 +46,8 @@ public async Task OnDeepLinkingReturnAsync([FromForm] IFormCollec return Unauthorized($"Unknown tool issuer: {issuer}"); } - // 3. Получаем публичные ключи (JWKS) инструмента через сервис var signingKeys = await ltiKeyService.GetKeysAsync(tool.JwksEndpoint); - // 4. Валидируем подпись try { handler.ValidateToken(tokenString, new TokenValidationParameters @@ -106,20 +104,16 @@ public async Task OnDeepLinkingReturnAsync([FromForm] IFormCollec

Задача выбрана. Возвращаемся в HwProj...

- - -
-

Работа с инструментом завершена

-

Вкладка должна закрыться автоматически.

-

Если этого не произошло, нажмите кнопку ниже:

- -
- - "; + + + + + Сессия завершена + + + + +
+

Работа с инструментом завершена

+

Вкладка должна закрыться автоматически, а страница задачи обновиться.

+

Если этого не произошло, нажмите кнопку ниже:

+ +
+ + "; return Content(htmlContent, "text/html"); } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs index 71edc7341..12858f38b 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs @@ -3,9 +3,11 @@ using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Net.Http; +using System.Security.Claims; using System.Security.Cryptography; using System.Text.Json; using System.Threading.Tasks; +using LtiAdvantage.AssignmentGradeServices; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; @@ -262,36 +264,150 @@ public IActionResult SubmitDeepLinkingSelection( return Content(html, "text/html"); } + [HttpPost("send-score")] + public async Task SendScore( + [FromForm] string lineItemUrl, + [FromForm] string userId, + [FromForm] string platformIss, + [FromForm] string taskId, + [FromForm] string returnUrl) + { + var client = _httpClientFactory.CreateClient(); + + // --- ШАГ 1: Получаем Access Token от HwProj --- + var clientAssertion = CreateClientAssertion(platformIss); + + var tokenRequest = new Dictionary + { + ["grant_type"] = "client_credentials", + ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + ["client_assertion"] = clientAssertion, + ["scope"] = "https://purl.imsglobal.org/spec/lti-ags/scope/score" + }; + + var tokenResponse = await client.PostAsync($"{platformIss}/api/lti/token", new FormUrlEncodedContent(tokenRequest)); + var tokenContent = await tokenResponse.Content.ReadAsStringAsync(); + + if (!tokenResponse.IsSuccessStatusCode) + return BadRequest($"Ошибка получения токена HwProj: {tokenContent}"); + + var accessToken = JsonDocument.Parse(tokenContent).RootElement.GetProperty("access_token").GetString(); + + // --- ШАГ 2: Отправляем JSON с оценкой на ваш lineItemUrl --- + var scoreObj = new Score + { + UserId = userId, + ScoreGiven = 100.0, + ScoreMaximum = 100.0, + Comment = "Работа выполнена идеально (Отправлено из Mock Tool)", + GradingProgress = GradingProgress.FullyGraded, + TimeStamp = DateTime.UtcNow + }; + + var scoreRequest = new HttpRequestMessage(HttpMethod.Post, lineItemUrl) + { + Content = new StringContent(JsonSerializer.Serialize(scoreObj), System.Text.Encoding.UTF8, "application/vnd.ims.lti-ags.v1.score+json") + }; + + // Вставляем полученный токен в заголовок Authorization + scoreRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + var scoreResponse = await client.SendAsync(scoreRequest); + var scoreResult = await scoreResponse.Content.ReadAsStringAsync(); + + if (scoreResponse.IsSuccessStatusCode) + { + var html = $@" +
+

Успех!

+

Оценка 100 баллов успешно передана в HwProj (Задача: {taskId}).

+ Вернуться в курс +
"; + return Content(html, "text/html"); + } + + return BadRequest($"Ошибка при отправке оценки. Статус: {scoreResponse.StatusCode}. Детали: {scoreResult}"); + } + private IActionResult HandleResourceLink(JwtSecurityToken token) { - // ... (Этот метод оставьте как был в предыдущем ответе, он работает корректно) ... + // Извлекаем URL возврата var presentationClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/launch_presentation"); var presentationJson = JsonDocument.Parse(presentationClaim?.Value ?? "{}"); - var returnUrl = string.Empty; - if (presentationJson.RootElement.TryGetProperty("return_url", out var returnUrlProp)) - { - returnUrl = returnUrlProp.GetString(); - } + var returnUrl = presentationJson.RootElement.TryGetProperty("return_url", out var returnUrlProp) ? returnUrlProp.GetString() : ""; + // Извлекаем ID задачи var resourceLinkClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/resource_link"); var resourceLinkJson = JsonDocument.Parse(resourceLinkClaim?.Value ?? "{}"); - var taskId = ""; - if(resourceLinkJson.RootElement.TryGetProperty("id", out var idProp)) taskId = idProp.GetString(); + var taskId = resourceLinkJson.RootElement.TryGetProperty("id", out var idProp) ? idProp.GetString() : ""; + + // НОВОЕ: Извлекаем AGS (ссылку для выставления оценок) + var agsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint"); + var lineItemUrl = ""; + if (agsClaim != null) + { + var agsJson = JsonDocument.Parse(agsClaim.Value); + if (agsJson.RootElement.TryGetProperty("lineitem", out var lineItemProp)) + lineItemUrl = lineItemProp.GetString(); + } + + var userId = token.Subject; // Идентификатор студента в HwProj + var issuer = token.Issuer; // URL HwProj var html = $@" - + -

Решение задачи (Mock Tool)

-

ID задачи: {taskId}

-

Вы успешно зашли через LTI 1.3!

-
- +
+

Решение задачи (Mock Tool)

+

ID задачи: {taskId}

+

Студент: {userId}

+
+ +
+ + + + + + + +
+ +
+ +
"; return Content(html, "text/html"); } + + private string CreateClientAssertion(string platformIssuer) + { + var now = DateTime.UtcNow; + // URL, куда инструмент пойдет за токеном (ваш LtiAccessTokenController) + var tokenEndpoint = $"{platformIssuer}/api/lti/token"; + + var claims = new List + { + new(JwtRegisteredClaimNames.Iss, "http://localhost:5000"), // ClientId нашего инструмента + new(JwtRegisteredClaimNames.Sub, "mock-tool-client-id"), + new(JwtRegisteredClaimNames.Aud, tokenEndpoint), // Audience = URL вашего эндпоинта + new(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), + new(JwtRegisteredClaimNames.Exp, new DateTimeOffset(now.AddMinutes(5)).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var jwt = new JwtSecurityToken( + header: new JwtHeader(new SigningCredentials(_signingKey, SecurityAlgorithms.RsaSha256)), + payload: new JwtPayload(claims) + ); + + return new JwtSecurityTokenHandler().WriteToken(jwt); + } } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs index 736dbde1b..2ade51c96 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs @@ -6,6 +6,7 @@ public class LtiPlatformConfig public string OidcAuthorizationEndpoint { get; set; } public string DeepLinkReturnUrl { get; set; } public string ResourceLinkReturnUrl { get; set; } + public string AssignmentsGradesEndpoint { get; set; } public string JwksEndpoint { get; set; } public LtiSigningKeyConfig SigningKey { get; set; } } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs index 3c6caadcc..f923594fb 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs @@ -12,7 +12,7 @@ public class LtiToolDto( public long Id { get; init; } = id; public string Name { get; init; } = name; public string ClientId { get; init; } = clientId; - public string JwksEndpoint { get; set; } = jwksEndpoint; + public string JwksEndpoint { get; init; } = jwksEndpoint; public string InitiateLoginUri { get; init; } = initiateLoginUri; public string LaunchUrl { get; init; } = launchUrl; public string DeepLink { get; init; } = deepLink; diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs index 40bee1e5e..456e06959 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs @@ -21,4 +21,8 @@ public string CreateResourceLinkToken( string userId, string nonce, string resourceLinkId); + + public string GenerateAccessTokenForLti( + string clientId, + string scope); } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs index 8bd777ed8..8bb51f48d 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs @@ -9,4 +9,5 @@ public interface ILtiToolService Task> GetAllAsync(); Task GetByIdAsync(long id); Task GetByIssuerAsync(string issuer); + Task GetByClientIdAsync(string clientId); } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs index d9e7141e3..e2bfe4a52 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs @@ -1,10 +1,13 @@ using System; +using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; using System.Security.Cryptography; using HwProj.APIGateway.API.Lti.Models; using HwProj.APIGateway.API.LTI.Services; using LtiAdvantage.DeepLinking; using LtiAdvantage.Lti; +using LtiAdvantage.AssignmentGradeServices; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; @@ -79,14 +82,46 @@ public string CreateResourceLinkToken( LaunchPresentation = new LaunchPresentationClaimValueType { - DocumentTarget = DocumentTarget.Window, + DocumentTarget = DocumentTarget.Window, ReturnUrl = _options.ResourceLinkReturnUrl, + }, + + AssignmentGradeServices = new AssignmentGradeServicesClaimValueType + { + Scope = ["https://purl.imsglobal.org/spec/lti-ags/scope/score"], + LineItemUrl = _options.AssignmentsGradesEndpoint + "/" + resourceLinkId, } }; return this.CreateJwt(clientId, request); } + public string GenerateAccessTokenForLti(string clientId, string scope) + { + var now = DateTime.UtcNow; + + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, clientId), + + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + + new Claim("scope", scope) + }; + + var jwt = new JwtSecurityToken( + issuer: _options.Issuer, + audience: _options.Issuer, + claims: claims, + notBefore: now, + expires: now.AddHours(1), + signingCredentials: GetSigningCredentials() + ); + + return new JwtSecurityTokenHandler().WriteToken(jwt); + } + + private SigningCredentials GetSigningCredentials() { var keyConfig = _options.SigningKey; diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs index 8c079ebd3..9fc8cc29f 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs @@ -33,7 +33,12 @@ public Task> GetAllAsync() return Task.FromResult(cfg == null ? null : MapToDto(cfg)); } - // Вынес создание DTO в отдельный метод, чтобы не дублировать код + public Task GetByClientIdAsync(string clientId) + { + var cfg = _tools.FirstOrDefault(t => t.ClientId == clientId); + return Task.FromResult(cfg == null ? null : MapToDto(cfg)); + } + private static LtiToolDto MapToDto(LtiToolConfig t) { return new LtiToolDto( diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index 2c9c45b34..e5f7655f9 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Security.Cryptography; using System.Text; using System.Text.Json.Serialization; using HwProj.APIGateway.API.Filters; @@ -71,6 +72,27 @@ public void ConfigureServices(IServiceCollection services) new SymmetricSecurityKey(Encoding.ASCII.GetBytes(appSettings["SecurityKey"])), ValidateIssuerSigningKey = true }; + }) + .AddJwtBearer("LtiScheme", options => + { + var ltiConfig = Configuration.GetSection("LtiPlatform").Get(); + if (ltiConfig == null) return; + + var rsa = RSA.Create(); + + rsa.ImportFromPem(ltiConfig.SigningKey.PrivateKeyPem); + + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = ltiConfig.Issuer, + ValidateAudience = true, + ValidAudience = ltiConfig.Issuer, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + + IssuerSigningKeys = [new RsaSecurityKey(rsa)] + }; }); services.AddHttpClient(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs index d8bee3b4f..1d0e37826 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs @@ -81,11 +81,13 @@ public async Task GetAllData(long courseId) [HttpGet("getByTask/{taskId}")] public async Task GetByTask(long taskId) { - var userId = Request.GetUserIdFromHeader(); - var course = await _coursesService.GetByTaskAsync(taskId, userId); - if (course == null) return NotFound(); + return await GetByTaskInternal(taskId); + } - return Ok(course); + [HttpGet("getByTaskForLti/{taskId}/{userId}")] + public async Task GetByTaskForLti(long taskId, string userId) + { + return await GetByTaskInternal(taskId, userId, true); } [HttpPost("create")] @@ -260,5 +262,15 @@ public async Task GetMentorsToAssignedStudents(long courseId) var mentorsToAssignedStudents = await _courseFilterService.GetAssignedStudentsIds(courseId, mentorIds); return Ok(mentorsToAssignedStudents); } + + private async Task GetByTaskInternal( + long taskId, string? userId = null, bool isLtiRequest = false) + { + userId ??= Request.GetUserIdFromHeader(); + var course = await _coursesService.GetByTaskAsync(taskId, userId!); + if (course == null) return NotFound(); + + return Ok(course); + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs index 9d8049c7c..ee3a37eba 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -86,7 +86,7 @@ public async Task GetAllAsync() StudentsIds = g.GroupMates.Select(t => t.StudentId).ToArray() }).ToArray(); - var result = userId == string.Empty ? courseDto : await _courseFilterService.ApplyFilter(courseDto, userId); + var result = string.IsNullOrEmpty(userId) ? courseDto : await _courseFilterService.ApplyFilter(courseDto, userId); return result; } diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs index d17f454c0..f10209584 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs @@ -41,13 +41,12 @@ public async Task GetAllCourses() public async Task GetCourseByTask(long taskId) { - using var httpRequest = new HttpRequestMessage( - HttpMethod.Get, - _coursesServiceUri + $"api/Courses/getByTask/{taskId}"); + return await GetCourseByTaskInternal(taskId); + } - httpRequest.TryAddUserId(_httpContextAccessor); - var response = await _httpClient.SendAsync(httpRequest); - return response.IsSuccessStatusCode ? await response.DeserializeAsync() : null; + public async Task GetCourseByTaskForLti(long taskId, string userId) + { + return await GetCourseByTaskInternal(taskId, userId, true); } public async Task GetCourseById(long courseId) @@ -641,5 +640,19 @@ public async Task Ping() return false; } } + + private async Task GetCourseByTaskInternal( + long taskId, string? userId = null, bool isLtiRequest = false) + { + using var httpRequest = new HttpRequestMessage( + HttpMethod.Get, + _coursesServiceUri + $"api/Courses/" + + $"{(isLtiRequest ? $"getByTaskForLti/{taskId}/{userId}" : $"getByTask/{taskId}")}"); + + httpRequest.TryAddUserId(_httpContextAccessor); + var response = await _httpClient.SendAsync(httpRequest); + var result =response.IsSuccessStatusCode ? await response.DeserializeAsync() : null; + return result; + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs index 1c3f5266c..34d90f494 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs @@ -13,6 +13,7 @@ public interface ICoursesServiceClient /// Получить полную информацию о курсе без учетов фильтров для преподавателей Task> GetAllCourseData(long courseId); Task GetCourseByTask(long taskId); + Task GetCourseByTaskForLti(long taskId, string userId); Task DeleteCourse(long courseId); Task> CreateCourse(CreateCourseViewModel model); Task UpdateCourse(UpdateCourseViewModel model, long courseId); diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs index 0cd2215a7..5b1437fc0 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs @@ -1,12 +1,10 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; using AutoMapper; using HwProj.CoursesService.Client; using HwProj.Models.CoursesService; -using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.SolutionsService; using HwProj.Models.StatisticsService; using HwProj.SolutionsService.API.Domains; @@ -74,33 +72,28 @@ public async Task GetTaskSolutionsFromStudent(long taskId, string st [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] public async Task PostSolution(long taskId, [FromBody] PostSolutionModel solutionModel) { - var task = await _coursesClient.GetTask(taskId); - if (!task.CanSendSolution) - return BadRequest(); - - var solution = _mapper.Map(solutionModel); - var solutionId = await _solutionsService.PostOrUpdateAsync(taskId, solution); + return await PostSolutionInternal(taskId, solutionModel); + } - return Ok(solutionId); + [HttpPost("postSolutionForLti/{taskId}")] + [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] + public async Task PostSolutionForLti(long taskId, [FromBody] PostSolutionModel solutionModel) + { + return await PostSolutionInternal(taskId, solutionModel, true); } [HttpPost("rateSolution/{solutionId}")] public async Task RateSolution(long solutionId, [FromBody] RateSolutionModel rateSolutionModel) { - var solution = await _solutionsService.GetSolutionAsync(solutionId); - var task = await _coursesClient.GetTask(solution.TaskId); - var homework = await _coursesClient.GetHomework(task.HomeworkId); - var course = await _coursesClient.GetCourseById(homework.CourseId); - - var lecturerId = Request.GetUserIdFromHeader(); - if (course != null && lecturerId != null && course.MentorIds.Contains(lecturerId)) - { - await _solutionsService.RateSolutionAsync(solutionId, lecturerId, rateSolutionModel.Rating, rateSolutionModel.LecturerComment); - return Ok(); - } + return await this.RateSolutionInternal(solutionId, rateSolutionModel); + } - return Forbid(); + [HttpPost("rateSolutionForLti/{solutionId}")] + public async Task RateSolutionForLti(long solutionId, + [FromBody] RateSolutionModel rateSolutionModel) + { + return await this.RateSolutionInternal(solutionId, rateSolutionModel, true); } [HttpPost("rateEmptySolution/{taskId}")] @@ -309,5 +302,46 @@ public async Task GetSolutionActuality(long solutionId) { return await _solutionsService.GetSolutionActuality(solutionId); } + + private async Task PostSolutionInternal( + long taskId, PostSolutionModel solutionModel, bool isLtiRequest = false) + { + var task = await _coursesClient.GetTask(taskId); + if (!task.CanSendSolution) + return BadRequest(); + + var solution = _mapper.Map(solutionModel); + var solutionId = isLtiRequest ? + await _solutionsService.PostOrUpdateAsyncForLti(taskId, solution) : + await _solutionsService.PostOrUpdateAsync(taskId, solution); + + return Ok(solutionId); + } + + private async Task RateSolutionInternal(long solutionId, + RateSolutionModel rateSolutionModel, bool isLtiRequest = false) + { + var solution = await _solutionsService.GetSolutionAsync(solutionId); + var task = await _coursesClient.GetTask(solution.TaskId); + var homework = await _coursesClient.GetHomework(task.HomeworkId); + var course = isLtiRequest + ? await _coursesClient.GetCourseByTaskForLti(homework.CourseId, solution.StudentId) + : await _coursesClient.GetCourseByTask(homework.CourseId); + + var lecturerId = Request.GetUserIdFromHeader(); + if (isLtiRequest && course != null) + { + await _solutionsService.RateSolutionAsync(solutionId, lecturerId!, rateSolutionModel.Rating, rateSolutionModel.LecturerComment); + return Ok(); + } + + if (course != null && lecturerId != null && course.MentorIds.Contains(lecturerId)) + { + await _solutionsService.RateSolutionAsync(solutionId, lecturerId, rateSolutionModel.Rating, rateSolutionModel.LecturerComment); + return Ok(); + } + + return Forbid(); + } } } \ No newline at end of file diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/ISolutionsService.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/ISolutionsService.cs index 441a6433c..146f1b7e8 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/ISolutionsService.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/ISolutionsService.cs @@ -16,6 +16,7 @@ public interface ISolutionsService Task GetTaskSolutionsFromGroupAsync(long taskId, long groupId); Task PostOrUpdateAsync(long taskId, Solution solution); + Task PostOrUpdateAsyncForLti(long taskId, Solution solution); Task PostEmptySolutionWithRateAsync(long task, Solution solution); Task RateSolutionAsync(long solutionId, string lecturerId, int newRating, string lecturerComment); diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs index df1a61bd8..e5b4e9416 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs @@ -61,6 +61,7 @@ public Task GetSolutionAsync(long solutionId) public async Task GetTaskSolutionsFromStudentAsync(long taskId, string studentId) { var course = await _coursesServiceClient.GetCourseByTask(taskId); + if (course == null) return Array.Empty(); var studentGroupsIds = course.Groups @@ -105,45 +106,12 @@ public async Task GetTaskSolutionsFromStudentAsync(long taskId, stri public async Task PostOrUpdateAsync(long taskId, Solution solution) { - solution.PublicationDate = DateTime.UtcNow; - solution.TaskId = taskId; - - var task = await _coursesServiceClient.GetTask(solution.TaskId); - - var lastSolution = - await _solutionsRepository - .FindAll(s => s.TaskId == taskId && s.StudentId == solution.StudentId) - .OrderByDescending(t => t.PublicationDate) - .FirstOrDefaultAsync(); - - long? solutionId; - - if (lastSolution != null && lastSolution.State == SolutionState.Posted) - { - await _solutionsRepository.UpdateAsync(lastSolution.Id, x => new Solution - { - GithubUrl = solution.GithubUrl, - Comment = solution.Comment, - GroupId = solution.GroupId, - IsModified = true, - State = SolutionState.Posted, - }); - solutionId = lastSolution.Id; - } - else - { - solutionId = await _solutionsRepository.AddAsync(solution); - - var solutionModel = _mapper.Map(solution); - var course = await _coursesServiceClient.GetCourseByTask(solution.TaskId); - var student = await _authServiceClient.GetAccountData(solutionModel.StudentId); - var studentModel = _mapper.Map(student); - _eventBus.Publish(new StudentPassTaskEvent(course, solutionModel, studentModel, task)); - } + return await PostOrUpdateInternalAsync(taskId, solution); + } - if (task.Tags.Contains(HomeworkTags.Test)) - await TrySaveSolutionCommitsInfo(solutionId.Value, solution.GithubUrl); - return solutionId.Value; + public async Task PostOrUpdateAsyncForLti(long taskId, Solution solution) + { + return await PostOrUpdateInternalAsync(taskId, solution, true); } public async Task PostEmptySolutionWithRateAsync(long taskId, Solution solution) @@ -351,6 +319,52 @@ await client.PullRequest.Commits(pullRequest.Owner, pullRequest.RepoName, pullRe return solutionsActuality; } + private async Task PostOrUpdateInternalAsync( + long taskId, Solution solution, bool isLtiRequest = false) + { + solution.PublicationDate = DateTime.UtcNow; + solution.TaskId = taskId; + + var task = await _coursesServiceClient.GetTask(solution.TaskId); + + var lastSolution = + await _solutionsRepository + .FindAll(s => s.TaskId == taskId && s.StudentId == solution.StudentId) + .OrderByDescending(t => t.PublicationDate) + .FirstOrDefaultAsync(); + + long? solutionId; + + if (lastSolution != null && lastSolution.State == SolutionState.Posted) + { + await _solutionsRepository.UpdateAsync(lastSolution.Id, x => new Solution + { + GithubUrl = solution.GithubUrl, + Comment = solution.Comment, + GroupId = solution.GroupId, + IsModified = true, + State = SolutionState.Posted, + }); + solutionId = lastSolution.Id; + } + else + { + solutionId = await _solutionsRepository.AddAsync(solution); + + var solutionModel = _mapper.Map(solution); + var course = isLtiRequest ? + await _coursesServiceClient.GetCourseByTaskForLti(solution.TaskId, solution.StudentId) : + await _coursesServiceClient.GetCourseByTask(solution.TaskId); + var student = await _authServiceClient.GetAccountData(solutionModel.StudentId); + var studentModel = _mapper.Map(student); + // _eventBus.Publish(new StudentPassTaskEvent(course, solutionModel, studentModel, task)); + } + + if (task.Tags.Contains(HomeworkTags.Test)) + await TrySaveSolutionCommitsInfo(solutionId.Value, solution.GithubUrl); + return solutionId.Value; + } + private async Task TrySaveSolutionCommitsInfo(long solutionId, string solutionUrl) { var client = CreateGitHubClient(); diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.Client/ISolutionsServiceClient.cs b/HwProj.SolutionsService/HwProj.SolutionsService.Client/ISolutionsServiceClient.cs index 6a73f271a..05b846c17 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.Client/ISolutionsServiceClient.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.Client/ISolutionsServiceClient.cs @@ -10,8 +10,10 @@ public interface ISolutionsServiceClient Task GetSolutionById(long solutionId); Task GetUserSolutions(long taskId, string studentId); Task PostSolution(long taskId, PostSolutionModel model); + Task PostSolutionForLti(long taskId, PostSolutionModel model); Task PostEmptySolutionWithRate(long taskId, SolutionViewModel solution); Task RateSolution(long solutionId, RateSolutionModel rateSolutionModel); + Task RateSolutionForLti(long solutionId, RateSolutionModel rateSolutionModel); Task MarkSolution(long solutionId); Task DeleteSolution(long solutionId); Task PostGroupSolution(SolutionViewModel model, long taskId, long groupId); diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs b/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs index da26f52a9..c1d97476d 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.Client/SolutionsServiceClient.cs @@ -72,24 +72,12 @@ public async Task GetUserSolutions(long taskId, string studentId) public async Task PostSolution(long taskId, PostSolutionModel model) { - using var httpRequest = new HttpRequestMessage( - HttpMethod.Post, - _solutionServiceUri + $"api/Solutions/{taskId}") - { - Content = new StringContent( - JsonConvert.SerializeObject(model), - Encoding.UTF8, - "application/json") - }; - - httpRequest.TryAddUserId(_httpContextAccessor); - var response = await _httpClient.SendAsync(httpRequest); - if (response.IsSuccessStatusCode) - { - return await response.DeserializeAsync(); - } + return await PostSolutionInternal(taskId, model); + } - throw new ForbiddenException(); + public async Task PostSolutionForLti(long taskId, PostSolutionModel model) + { + return await PostSolutionInternal(taskId, model, true); } public async Task PostEmptySolutionWithRate(long taskId, SolutionViewModel model) @@ -111,11 +99,23 @@ public async Task PostEmptySolutionWithRate(long taskId, SolutionViewModel model } public async Task RateSolution(long solutionId, RateSolutionModel rateSolutionModel) + { + await RateSolutionInternal(solutionId, rateSolutionModel); + } + + public async Task RateSolutionForLti(long solutionId, RateSolutionModel rateSolutionModel) + { + await RateSolutionInternal(solutionId, rateSolutionModel, true); + } + + private async Task RateSolutionInternal( + long solutionId, RateSolutionModel rateSolutionModel, bool isLtiRequest = false) { using var httpRequest = new HttpRequestMessage( HttpMethod.Post, _solutionServiceUri + - $"api/Solutions/rateSolution/{solutionId}") + $"api/Solutions/" + + $"{(isLtiRequest ? "rateSolutionForLti" : "rateSolution")}/{solutionId}") { Content = new StringContent( JsonConvert.SerializeObject(rateSolutionModel), @@ -297,5 +297,29 @@ public async Task Ping() return false; } } + + private async Task PostSolutionInternal( + long taskId, PostSolutionModel model, bool isLtiRequest = false) + { + using var httpRequest = new HttpRequestMessage( + HttpMethod.Post, + _solutionServiceUri + $"api/Solutions/" + + $"{(isLtiRequest ? "postSolutionForLti/" : "")}{taskId}") + { + Content = new StringContent( + JsonConvert.SerializeObject(model), + Encoding.UTF8, + "application/json") + }; + + httpRequest.TryAddUserId(_httpContextAccessor); + var response = await _httpClient.SendAsync(httpRequest); + if (response.IsSuccessStatusCode) + { + return await response.DeserializeAsync(); + } + + throw new ForbiddenException(); + } } } diff --git a/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx b/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx index 55b3f4e70..c69aba833 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx @@ -80,7 +80,20 @@ const TaskSolutionsPage: FC = () => { const showOnlyNotSolved = filterState.some(x => x === "Только нерешенные") useEffect(() => { - getSolutions() + getSolutions(); + + const handleLtiMessage = (event: MessageEvent) => { + + if (event.data === 'lti_success_refresh') { + getSolutions(); + } + }; + + window.addEventListener("message", handleLtiMessage); + + return () => { + window.removeEventListener("message", handleLtiMessage); + }; }, []) useEffect(() => { From 8db5f7d9ce83fa9f653f7317baf0d5f6b837f54d Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Mon, 23 Feb 2026 11:44:12 +0300 Subject: [PATCH 15/26] refactor: deleted MockToolController --- .../Lti/Controllers/MockToolController.cs | 413 ------------------ 1 file changed, 413 deletions(-) delete mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs deleted file mode 100644 index 12858f38b..000000000 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs +++ /dev/null @@ -1,413 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Net.Http; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text.Json; -using System.Threading.Tasks; -using LtiAdvantage.AssignmentGradeServices; -using Microsoft.AspNetCore.Mvc; -using Microsoft.IdentityModel.Tokens; - -[Route("api/mocktool")] -[ApiController] -public class MockToolController : ControllerBase -{ - private static readonly RsaSecurityKey _signingKey; - private static readonly string _keyId; - private readonly IHttpClientFactory _httpClientFactory; - - // --- Имитация базы данных задач --- - private record MockTask(string Id, string Title, string Description, int Score); - private static readonly List _availableTasks = new() - { - new("1", "Интегралы (Mock)", "Вычислить определенный интеграл", 10), - new("2", "Производные (Mock)", "Найти производную сложной функции", 5), - new("3", "Пределы (Mock)", "Вычислить предел последовательности", 8), - new("4", "Ряды (Mock)", "Исследовать ряд на сходимость", 12), - new("5", "Диффуры (Mock)", "Решить линейное уравнение", 15) - }; - - public MockToolController(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - } - - static MockToolController() - { - var rsa = RSA.Create(2048); - _keyId = Guid.NewGuid().ToString(); - _signingKey = new RsaSecurityKey(rsa) { KeyId = _keyId }; - } - - [HttpGet("jwks")] - public IActionResult GetJwks() - { - var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(_signingKey); - return Ok(new { keys = new[] { jwk } }); - } - - [HttpPost("login")] - public IActionResult Login([FromForm] string iss, [FromForm] string login_hint, [FromForm] string lti_message_hint) - { - var callbackUrl = $"{iss}/api/lti/authorize?" + - $"client_id=mock-tool-client-id&" + - $"response_type=id_token&" + - $"redirect_uri=http://localhost:5000/api/mocktool/callback&" + - $"login_hint={login_hint}&" + - $"lti_message_hint={lti_message_hint}&" + - $"scope=openid&state=xyz&nonce={Guid.NewGuid()}"; - - return Redirect(callbackUrl); - } - - [HttpPost("callback")] - public async Task Callback([FromForm] string id_token) - { - var handler = new JwtSecurityTokenHandler(); - if (!handler.CanReadToken(id_token)) return BadRequest("Invalid Token"); - var unverifiedToken = handler.ReadJwtToken(id_token); - - // --- ВАЛИДАЦИЯ (Без изменений) --- - string issuer = unverifiedToken.Issuer; - string platformJwksUrl = $"{issuer}/api/lti/jwks"; - - var client = _httpClientFactory.CreateClient(); - string jwksJson; - try - { - jwksJson = await client.GetStringAsync(platformJwksUrl); - } - catch - { - return BadRequest($"Не удалось скачать ключи HwProj по адресу {platformJwksUrl}"); - } - - var platformKeySet = new JsonWebKeySet(jwksJson); - - try - { - handler.ValidateToken(id_token, new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = issuer, - ValidateAudience = true, - ValidAudience = "mock-tool-client-id", - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - IssuerSigningKeys = platformKeySet.Keys - }, out _); - } - catch (Exception ex) - { - return Unauthorized($"Ошибка проверки подписи HwProj: {ex.Message}"); - } - - var messageType = unverifiedToken.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/message_type")?.Value; - - if (messageType == "LtiDeepLinkingRequest") - { - // Здесь мы больше не отправляем ответ сразу, а рендерим UI - return RenderDeepLinkingSelectionUI(unverifiedToken); - } - else if (messageType == "LtiResourceLinkRequest") - { - return HandleResourceLink(unverifiedToken); - } - else - { - return BadRequest($"Unknown message type: {messageType}"); - } - } - - // --- ЭТАП 1: Отображение списка задач (HTML Form) --- - private IActionResult RenderDeepLinkingSelectionUI(JwtSecurityToken token) - { - var settingsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings"); - if (settingsClaim == null) return BadRequest("No deep linking settings found"); - - var settings = JsonDocument.Parse(settingsClaim.Value); - var returnUrl = settings.RootElement.GetProperty("deep_link_return_url").GetString(); - - // Нам нужно сохранить эти данные, чтобы использовать их на следующем шаге (в POST запросе) - // В реальном приложении это кэшируется, но здесь передадим через скрытые поля формы - var dataPayload = settings.RootElement.TryGetProperty("data", out var dataEl) ? dataEl.GetString() : ""; - - // Генерируем HTML списка задач - var tasksHtml = string.Join("", _availableTasks.Select(t => $@" -
- -
")); - - var html = $@" - - - - Выбор задач (Mock Tool) - - - -
-

Библиотека задач (Mock Tool)

-

Выберите задачи, которые хотите добавить в курс HwProj:

- -
- - - - - - -
- {tasksHtml} -
- -
- -
-
-
- - "; - - return Content(html, "text/html"); - } - - // --- ЭТАП 2: Обработка выбора и отправка в HwProj --- - [HttpPost("submit-selection")] - public IActionResult SubmitDeepLinkingSelection( - [FromForm] List selectedIds, - [FromForm] string returnUrl, - [FromForm] string? data, - [FromForm] string iss, - [FromForm] string aud) - { - // 1. Фильтруем задачи по выбору пользователя - var selectedTasks = _availableTasks.Where(t => selectedIds.Contains(t.Id)).ToList(); - - // 2. Формируем LTI Content Items - var contentItems = selectedTasks.Select(t => new Dictionary - { - ["type"] = "ltiResourceLink", - ["title"] = t.Title, - ["text"] = t.Description, - // Ссылка запуска этой конкретной задачи - ["url"] = $"http://localhost:5000/mock/task/{t.Id}", - ["scoreMaximum"] = t.Score, - // Кастомные параметры, если нужны - ["custom"] = new Dictionary { ["internal_id"] = t.Id } - }).ToList(); - - // 3. Создаем JWT ответ (LtiDeepLinkingResponse) - var payload = new JwtPayload - { - { "iss", iss }, // В LTI ответе issuer - это URL инструмента (но для подписи используем наши настройки) - { "aud", aud }, - { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, - { "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() }, - { "nonce", "resp-nonce-" + Guid.NewGuid() }, - { "https://purl.imsglobal.org/spec/lti-dl/claim/message_type", "LtiDeepLinkingResponse" }, - { "https://purl.imsglobal.org/spec/lti-dl/claim/version", "1.3.0" }, - { "https://purl.imsglobal.org/spec/lti-dl/claim/content_items", contentItems } - }; - - // Если HwProj прислал параметр 'data', мы ОБЯЗАНЫ вернуть его обратно - if (!string.IsNullOrEmpty(data)) - { - payload.Add("https://purl.imsglobal.org/spec/lti-dl/claim/data", data); - } - - // Подписываем токен НАШИМ ключом - var credentials = new SigningCredentials(_signingKey, SecurityAlgorithms.RsaSha256); - var header = new JwtHeader(credentials); - - // ВАЖНО: iss в токене должен совпадать с тем, что ожидает HwProj (обычно URL инструмента) - // Для локального теста оставим http://localhost:5000 - payload["iss"] = "http://localhost:5000"; - - var responseToken = new JwtSecurityToken(header, payload); - var responseString = new JwtSecurityTokenHandler().WriteToken(responseToken); - - // 4. Возвращаем авто-сабмит форму, которая отправит токен в HwProj - var html = $@" - - -
-

Возвращаемся в HwProj...

-

Передача {selectedTasks.Count} выбранных задач.

-
-
- -
- - "; - - return Content(html, "text/html"); - } - - [HttpPost("send-score")] - public async Task SendScore( - [FromForm] string lineItemUrl, - [FromForm] string userId, - [FromForm] string platformIss, - [FromForm] string taskId, - [FromForm] string returnUrl) - { - var client = _httpClientFactory.CreateClient(); - - // --- ШАГ 1: Получаем Access Token от HwProj --- - var clientAssertion = CreateClientAssertion(platformIss); - - var tokenRequest = new Dictionary - { - ["grant_type"] = "client_credentials", - ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", - ["client_assertion"] = clientAssertion, - ["scope"] = "https://purl.imsglobal.org/spec/lti-ags/scope/score" - }; - - var tokenResponse = await client.PostAsync($"{platformIss}/api/lti/token", new FormUrlEncodedContent(tokenRequest)); - var tokenContent = await tokenResponse.Content.ReadAsStringAsync(); - - if (!tokenResponse.IsSuccessStatusCode) - return BadRequest($"Ошибка получения токена HwProj: {tokenContent}"); - - var accessToken = JsonDocument.Parse(tokenContent).RootElement.GetProperty("access_token").GetString(); - - // --- ШАГ 2: Отправляем JSON с оценкой на ваш lineItemUrl --- - var scoreObj = new Score - { - UserId = userId, - ScoreGiven = 100.0, - ScoreMaximum = 100.0, - Comment = "Работа выполнена идеально (Отправлено из Mock Tool)", - GradingProgress = GradingProgress.FullyGraded, - TimeStamp = DateTime.UtcNow - }; - - var scoreRequest = new HttpRequestMessage(HttpMethod.Post, lineItemUrl) - { - Content = new StringContent(JsonSerializer.Serialize(scoreObj), System.Text.Encoding.UTF8, "application/vnd.ims.lti-ags.v1.score+json") - }; - - // Вставляем полученный токен в заголовок Authorization - scoreRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); - - var scoreResponse = await client.SendAsync(scoreRequest); - var scoreResult = await scoreResponse.Content.ReadAsStringAsync(); - - if (scoreResponse.IsSuccessStatusCode) - { - var html = $@" -
-

Успех!

-

Оценка 100 баллов успешно передана в HwProj (Задача: {taskId}).

- Вернуться в курс -
"; - return Content(html, "text/html"); - } - - return BadRequest($"Ошибка при отправке оценки. Статус: {scoreResponse.StatusCode}. Детали: {scoreResult}"); - } - - private IActionResult HandleResourceLink(JwtSecurityToken token) - { - // Извлекаем URL возврата - var presentationClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/launch_presentation"); - var presentationJson = JsonDocument.Parse(presentationClaim?.Value ?? "{}"); - var returnUrl = presentationJson.RootElement.TryGetProperty("return_url", out var returnUrlProp) ? returnUrlProp.GetString() : ""; - - // Извлекаем ID задачи - var resourceLinkClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/resource_link"); - var resourceLinkJson = JsonDocument.Parse(resourceLinkClaim?.Value ?? "{}"); - var taskId = resourceLinkJson.RootElement.TryGetProperty("id", out var idProp) ? idProp.GetString() : ""; - - // НОВОЕ: Извлекаем AGS (ссылку для выставления оценок) - var agsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint"); - var lineItemUrl = ""; - if (agsClaim != null) - { - var agsJson = JsonDocument.Parse(agsClaim.Value); - if (agsJson.RootElement.TryGetProperty("lineitem", out var lineItemProp)) - lineItemUrl = lineItemProp.GetString(); - } - - var userId = token.Subject; // Идентификатор студента в HwProj - var issuer = token.Issuer; // URL HwProj - - var html = $@" - - - - - - -
-

Решение задачи (Mock Tool)

-

ID задачи: {taskId}

-

Студент: {userId}

-
- -
- - - - - - - -
- -
- -
- - "; - return Content(html, "text/html"); - } - - private string CreateClientAssertion(string platformIssuer) - { - var now = DateTime.UtcNow; - // URL, куда инструмент пойдет за токеном (ваш LtiAccessTokenController) - var tokenEndpoint = $"{platformIssuer}/api/lti/token"; - - var claims = new List - { - new(JwtRegisteredClaimNames.Iss, "http://localhost:5000"), // ClientId нашего инструмента - new(JwtRegisteredClaimNames.Sub, "mock-tool-client-id"), - new(JwtRegisteredClaimNames.Aud, tokenEndpoint), // Audience = URL вашего эндпоинта - new(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), - new(JwtRegisteredClaimNames.Exp, new DateTimeOffset(now.AddMinutes(5)).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), - new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) - }; - - var jwt = new JwtSecurityToken( - header: new JwtHeader(new SigningCredentials(_signingKey, SecurityAlgorithms.RsaSha256)), - payload: new JwtPayload(claims) - ); - - return new JwtSecurityTokenHandler().WriteToken(jwt); - } -} \ No newline at end of file From 5233e52839f70cead7ee1146f89ba93f94f56be1 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Mon, 23 Feb 2026 14:25:22 +0300 Subject: [PATCH 16/26] fix: enabled notifications --- .../HwProj.SolutionsService.API/Services/SolutionsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs index e5b4e9416..9f9094410 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs @@ -357,7 +357,7 @@ await _coursesServiceClient.GetCourseByTaskForLti(solution.TaskId, solution.Stud await _coursesServiceClient.GetCourseByTask(solution.TaskId); var student = await _authServiceClient.GetAccountData(solutionModel.StudentId); var studentModel = _mapper.Map(student); - // _eventBus.Publish(new StudentPassTaskEvent(course, solutionModel, studentModel, task)); + _eventBus.Publish(new StudentPassTaskEvent(course, solutionModel, studentModel, task)); } if (task.Tags.Contains(HomeworkTags.Test)) From 55c2f76f09a577e53c311a44031ba8d00fc5c5e1 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Mon, 23 Feb 2026 22:51:02 +0300 Subject: [PATCH 17/26] fix and refactor: fixed a bug in the signature verification in LtiDeepLinkingReturnController, added a check for matching toolId and course.LtiToolId --- .../Controllers/LtiAccessTokenController.cs | 13 ++++--- .../Lti/Controllers/LtiAuthController.cs | 38 +++++++++++++++---- .../LtiDeepLinkingReturnController.cs | 15 ++++---- .../Lti/Models/LtiPlatformConfig.cs | 1 + .../Controllers/CoursesController.cs | 19 ++++++++-- .../CoursesServiceClient.cs | 24 +++++++++--- .../ICoursesServiceClient.cs | 1 + 7 files changed, 82 insertions(+), 29 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs index 433a8a1d4..f42dd09ec 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs @@ -1,11 +1,13 @@ using System; using System.IdentityModel.Tokens.Jwt; using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Models; using HwProj.APIGateway.API.Lti.Services; using HwProj.APIGateway.API.LTI.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace HwProj.APIGateway.API.Lti.Controllers; @@ -13,6 +15,7 @@ namespace HwProj.APIGateway.API.Lti.Controllers; [Route("api/lti")] [ApiController] public class LtiAccessTokenController( + IOptions options, ILtiToolService toolService, ILtiKeyService ltiKeyService, ILtiTokenService tokenService @@ -46,24 +49,24 @@ public async Task GetTokenAsync([FromForm] IFormCollection form) var unverifiedToken = handler.ReadJwtToken(clientAssertion); - var issuer = unverifiedToken.Issuer; + var clientId = unverifiedToken.Subject; - var tool = await toolService.GetByIssuerAsync(issuer); + var tool = await toolService.GetByClientIdAsync(clientId); if (tool == null) { - return Unauthorized(new { error = "invalid_client", error_description = $"Unknown issuer: {issuer}" }); + return Unauthorized(new { error = "invalid_client", error_description = $"Unknown clientId: {clientId}" }); } var signingKeys = await ltiKeyService.GetKeysAsync(tool.JwksEndpoint); try { - var tokenEndpointUrl = $"{Request.Scheme}://{Request.Host}{Request.Path}"; + var tokenEndpointUrl = options.Value.AccessTokenUrl; handler.ValidateToken(clientAssertion, new TokenValidationParameters { ValidateIssuer = true, - ValidIssuer = issuer, + ValidIssuer = unverifiedToken.Issuer, ValidateAudience = true, ValidAudience = tokenEndpointUrl, diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs index 281b076f6..1c768b51b 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs @@ -6,6 +6,7 @@ using HwProj.APIGateway.API.Lti.Models; using HwProj.APIGateway.API.Lti.Services; using HwProj.APIGateway.API.LTI.Services; +using HwProj.CoursesService.Client; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; @@ -16,6 +17,7 @@ namespace HwProj.APIGateway.API.Lti.Controllers; [Route("api/lti")] [ApiController] public class LtiAuthController( + ICoursesServiceClient coursesServiceClient, IOptions ltiPlatformOptions, ILtiToolService toolService, ILtiTokenService tokenService, @@ -46,12 +48,12 @@ public async Task AuthorizeLti( return BadRequest("Invalid or expired lti_message_hint"); } - if (payload == null) + if (payload?.ToolId == null || payload.CourseId == null) { return BadRequest("Invalid or expired lti_message_hint"); } - var tool = await toolService.GetByIdAsync(long.Parse(payload.ToolId!)); + var tool = await toolService.GetByIdAsync(long.Parse(payload.ToolId)); if (tool == null) { return BadRequest("Tool not found"); @@ -59,7 +61,18 @@ public async Task AuthorizeLti( if (tool.ClientId != clientId) { - return BadRequest($"Invalid client_id. Expected: {tool.ClientId}, Got: {clientId}"); + return BadRequest($"Invalid clientId. Expected: {tool.ClientId}, Got: {clientId}"); + } + + var course = await coursesServiceClient.GetCourseByIdForLti(long.Parse(payload.CourseId)); + if (course == null) + { + return NotFound("Course not found"); + } + + if (course.LtiToolId != tool.Id) + { + return BadRequest("The data is incorrect: the id of the instrument linked to the exchange rate does not match"); } string idToken; @@ -68,8 +81,8 @@ public async Task AuthorizeLti( case "DeepLinking": idToken = tokenService.CreateDeepLinkingToken( clientId: clientId, - toolId: payload.ToolId!, - courseId: payload.CourseId!, + toolId: payload.ToolId, + courseId: payload.CourseId, targetLinkUri: tool.DeepLink, userId: payload.UserId, nonce: nonce @@ -78,8 +91,8 @@ public async Task AuthorizeLti( case "ResourceLink": idToken = tokenService.CreateResourceLinkToken( clientId: clientId, - toolId: payload.ToolId!, - courseId: payload.CourseId!, + toolId: payload.ToolId, + courseId: payload.CourseId, targetLinkUri: tool.LaunchUrl, userId: payload.UserId, nonce: nonce, @@ -133,6 +146,17 @@ public async Task StartLti( return NotFound("Tool not found"); } + var course = await coursesServiceClient.GetCourseByIdForLti(long.Parse(courseId)); + if (course == null) + { + return NotFound("Course not found"); + } + + if (course.LtiToolId != long.Parse(toolId)) + { + return BadRequest("The data is incorrect: the id of the instrument linked to the exchange rate does not match"); + } + if (isDeepLink) { targetUrl = !string.IsNullOrEmpty(tool.DeepLink) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs index 5fe337422..bec1c4581 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs @@ -3,10 +3,12 @@ using System.IdentityModel.Tokens.Jwt; using System.Text.Json; using System.Threading.Tasks; +using HwProj.APIGateway.API.Lti.Models; using HwProj.APIGateway.API.Lti.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace HwProj.APIGateway.API.Lti.Controllers; @@ -14,6 +16,7 @@ namespace HwProj.APIGateway.API.Lti.Controllers; [Route("api/lti")] [ApiController] public class LtiDeepLinkingReturnController( + IOptions ltiPlatformOptions, ILtiToolService toolService, ILtiKeyService ltiKeyService ) : ControllerBase @@ -36,14 +39,12 @@ public async Task OnDeepLinkingReturnAsync([FromForm] IFormCollec } var unverifiedToken = handler.ReadJwtToken(tokenString); - var issuer = unverifiedToken.Issuer; + var clientId = unverifiedToken.Subject; - // 2. Ищем инструмент в БД по Issuer - // (Предполагается, что у toolService есть метод GetByIssuerAsync или аналогичный) - var tool = await toolService.GetByIssuerAsync(issuer); + var tool = await toolService.GetByClientIdAsync(clientId); if (tool == null) { - return Unauthorized($"Unknown tool issuer: {issuer}"); + return Unauthorized($"Unknown tool clientId: {clientId}"); } var signingKeys = await ltiKeyService.GetKeysAsync(tool.JwksEndpoint); @@ -53,10 +54,10 @@ public async Task OnDeepLinkingReturnAsync([FromForm] IFormCollec handler.ValidateToken(tokenString, new TokenValidationParameters { ValidateIssuer = true, - ValidIssuer = issuer, + ValidIssuer = unverifiedToken.Issuer, ValidateAudience = true, - ValidAudience = tool.ClientId, + ValidAudience = ltiPlatformOptions.Value.Issuer, ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(5), diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs index 2ade51c96..c40598a9e 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs @@ -7,6 +7,7 @@ public class LtiPlatformConfig public string DeepLinkReturnUrl { get; set; } public string ResourceLinkReturnUrl { get; set; } public string AssignmentsGradesEndpoint { get; set; } + public string AccessTokenUrl { get; set; } public string JwksEndpoint { get; set; } public LtiSigningKeyConfig SigningKey { get; set; } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs index 1d0e37826..ac696feaa 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs @@ -50,11 +50,13 @@ public async Task GetAll() [HttpGet("{courseId}")] public async Task Get(long courseId) { - var userId = Request.GetUserIdFromHeader(); - var course = await _coursesService.GetAsync(courseId, userId); - if (course == null) return NotFound(); + return await GetInternal(courseId); + } - return Ok(course); + [HttpGet("getForLti/{courseId}")] + public async Task GetForLti(long courseId) + { + return await GetInternal(courseId); } [HttpGet("getForMentor/{courseId}/{mentorId}")] @@ -263,6 +265,15 @@ public async Task GetMentorsToAssignedStudents(long courseId) return Ok(mentorsToAssignedStudents); } + private async Task GetInternal(long courseId) + { + var userId = Request.GetUserIdFromHeader(); + var course = await _coursesService.GetAsync(courseId, userId); + if (course == null) return NotFound(); + + return Ok(course); + } + private async Task GetByTaskInternal( long taskId, string? userId = null, bool isLtiRequest = false) { diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs index f10209584..5cd297221 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs @@ -51,13 +51,12 @@ public async Task GetAllCourses() public async Task GetCourseById(long courseId) { - using var httpRequest = new HttpRequestMessage( - HttpMethod.Get, - _coursesServiceUri + $"api/Courses/{courseId}"); + return await GetCourseByIdInternal(courseId); + } - httpRequest.TryAddUserId(_httpContextAccessor); - var response = await _httpClient.SendAsync(httpRequest); - return response.IsSuccessStatusCode ? await response.DeserializeAsync() : null; + public async Task GetCourseByIdForLti(long courseId) + { + return await GetCourseByIdInternal(courseId, true); } public async Task> GetCourseByIdForMentor(long courseId, string mentorId) @@ -641,6 +640,19 @@ public async Task Ping() } } + private async Task GetCourseByIdInternal( + long courseId, bool isLtiRequest = false) + { + using var httpRequest = new HttpRequestMessage( + HttpMethod.Get, + _coursesServiceUri + $"api/Courses/" + + $"{(isLtiRequest ? "getForLti/" : "")}{courseId}"); + + httpRequest.TryAddUserId(_httpContextAccessor); + var response = await _httpClient.SendAsync(httpRequest); + return response.IsSuccessStatusCode ? await response.DeserializeAsync() : null; + } + private async Task GetCourseByTaskInternal( long taskId, string? userId = null, bool isLtiRequest = false) { diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs index 34d90f494..e1624520a 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs @@ -9,6 +9,7 @@ public interface ICoursesServiceClient { Task GetAllCourses(); Task GetCourseById(long courseId); + Task GetCourseByIdForLti(long courseId); Task> GetCourseByIdForMentor(long courseId, string mentorId); /// Получить полную информацию о курсе без учетов фильтров для преподавателей Task> GetAllCourseData(long courseId); From 7ec4f6a459ec5f10d762126e552c6a1aa9f2b926 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Tue, 24 Feb 2026 08:15:30 +0300 Subject: [PATCH 18/26] feat: added a pop-up window to warn you before you start testing --- .../Lti/Controllers/MockToolController.cs | 321 ++++++++++++++++++ .../components/Solutions/LtiLaunchButton.tsx | 52 ++- 2 files changed, 363 insertions(+), 10 deletions(-) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs new file mode 100644 index 000000000..2addb708b --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text.Json; +using System.Threading.Tasks; +using LtiAdvantage.AssignmentGradeServices; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/mocktool")] +[ApiController] +public class MockToolController : ControllerBase +{ + private static readonly RsaSecurityKey _signingKey; + private static readonly string _keyId; + private readonly IHttpClientFactory _httpClientFactory; + + // --- IDENTITY SETTINGS (as in the sandbox) --- + // This value must match the one in the HwProj database (ClientId) + private const string ToolIss = "Local Mock Tool"; + private const string ToolNameId = "mock-tool-client-id"; + + private record MockTask(string Id, string Title, string Description, int Score); + private static readonly List _availableTasks = new() + { + new("1", "Integrals (Mock)", "Calculate definite integral", 10), + new("2", "Derivatives (Mock)", "Find the derivative of a complex function", 5), + new("3", "Limits (Mock)", "Calculate sequence limit", 8), + new("4", "Series (Mock)", "Investigate series for convergence", 12), + new("5", "Diff. Eqs (Mock)", "Solve linear equation", 15) + }; + + public MockToolController(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + static MockToolController() + { + var rsa = RSA.Create(2048); + _keyId = "mock-tool-key-id"; + _signingKey = new RsaSecurityKey(rsa) { KeyId = _keyId }; + } + + [HttpGet("jwks")] + public IActionResult GetJwks() + { + var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(_signingKey); + return Ok(new { keys = new[] { jwk } }); + } + + [HttpPost("login")] + public IActionResult Login([FromForm] string iss, [FromForm] string login_hint, [FromForm] string lti_message_hint) + { + // iss here is your platform address (ngrok) + var callbackUrl = $"{iss}/api/lti/authorize?" + + $"client_id={ToolNameId}&" + + $"response_type=id_token&" + + $"redirect_uri=http://localhost:5000/api/mocktool/callback&" + + $"login_hint={login_hint}&" + + $"lti_message_hint={lti_message_hint}&" + + $"scope=openid&state=xyz&nonce={Guid.NewGuid()}"; + + return Redirect(callbackUrl); + } + + [HttpPost("callback")] + public async Task Callback([FromForm] string id_token) + { + var handler = new JwtSecurityTokenHandler(); + if (!handler.CanReadToken(id_token)) return BadRequest("Invalid Token"); + var unverifiedToken = handler.ReadJwtToken(id_token); + + string issuer = unverifiedToken.Issuer; + string platformJwksUrl = $"{issuer}/api/lti/jwks"; + + var client = _httpClientFactory.CreateClient(); + string jwksJson; + try { + jwksJson = await client.GetStringAsync(platformJwksUrl); + } catch { + return BadRequest($"Failed to download HwProj keys from {platformJwksUrl}"); + } + + var platformKeySet = new JsonWebKeySet(jwksJson); + + try { + handler.ValidateToken(id_token, new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = issuer, + ValidateAudience = true, + ValidAudience = ToolNameId, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKeys = platformKeySet.Keys + }, out _); + } catch (Exception ex) { + return Unauthorized($"HwProj signature validation error: {ex.Message}"); + } + + var messageType = unverifiedToken.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/message_type")?.Value; + + if (messageType == "LtiDeepLinkingRequest") + return RenderDeepLinkingSelectionUI(unverifiedToken); + + if (messageType == "LtiResourceLinkRequest") + return HandleResourceLink(unverifiedToken); + + return BadRequest($"Unknown message type: {messageType}"); + } + + private IActionResult RenderDeepLinkingSelectionUI(JwtSecurityToken token) + { + var settingsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings"); + if (settingsClaim == null) return BadRequest("No deep linking settings found"); + + var settings = JsonDocument.Parse(settingsClaim.Value); + var returnUrl = settings.RootElement.GetProperty("deep_link_return_url").GetString(); + var dataPayload = settings.RootElement.TryGetProperty("data", out var dataEl) ? dataEl.GetString() : ""; + + var tasksHtml = string.Join("", _availableTasks.Select(t => $@" +
+ +
")); + + var html = $@" + + +

Select Tasks for HwProj

+
+ + + + {tasksHtml} +
+
+ + "; + + return Content(html, "text/html"); + } + + [HttpPost("submit-selection")] + public IActionResult SubmitDeepLinkingSelection( + [FromForm] List selectedIds, + [FromForm] string returnUrl, + [FromForm] string? data, + [FromForm] string platformIssuer) + { + var selectedTasks = _availableTasks.Where(t => selectedIds.Contains(t.Id)).ToList(); + + var contentItems = selectedTasks.Select(t => new Dictionary + { + ["type"] = "ltiResourceLink", + ["title"] = t.Title, + ["text"] = t.Description, + ["url"] = $"http://localhost:5000/mock/task/{t.Id}", + ["scoreMaximum"] = t.Score + }).ToList(); + + var payload = new JwtPayload + { + { "iss", ToolIss }, // Tool Name as Issuer + { "sub", ToolNameId }, // Tool Name as Subject + { "aud", platformIssuer }, // Platform (ngrok) as Audience + { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, + { "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() }, + { "nonce", Guid.NewGuid().ToString() }, + { "https://purl.imsglobal.org/spec/lti-dl/claim/message_type", "LtiDeepLinkingResponse" }, + { "https://purl.imsglobal.org/spec/lti-dl/claim/version", "1.3.0" }, + { "https://purl.imsglobal.org/spec/lti-dl/claim/content_items", contentItems } + }; + + if (!string.IsNullOrEmpty(data)) + payload.Add("https://purl.imsglobal.org/spec/lti-dl/claim/data", data); + + var credentials = new SigningCredentials(_signingKey, SecurityAlgorithms.RsaSha256); + var header = new JwtHeader(credentials); + var responseToken = new JwtSecurityToken(header, payload); + var responseString = new JwtSecurityTokenHandler().WriteToken(responseToken); + + var html = $@" + + +
+ +
+ + "; + + return Content(html, "text/html"); + } + + [HttpPost("send-score")] + public async Task SendScore( + [FromForm] string lineItemUrl, [FromForm] string userId, + [FromForm] string platformIss, [FromForm] string taskId, [FromForm] string returnUrl) + { + var client = _httpClientFactory.CreateClient(); + var clientAssertion = CreateClientAssertion(platformIss); + + var tokenRequest = new Dictionary { + ["grant_type"] = "client_credentials", + ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + ["client_assertion"] = clientAssertion, + ["scope"] = "https://purl.imsglobal.org/spec/lti-ags/scope/score" + }; + + var tokenResponse = await client.PostAsync($"{platformIss}/api/lti/token", new FormUrlEncodedContent(tokenRequest)); + if (!tokenResponse.IsSuccessStatusCode) return BadRequest("Error retrieving token"); + + var tokenContent = await tokenResponse.Content.ReadAsStringAsync(); + var accessToken = JsonDocument.Parse(tokenContent).RootElement.GetProperty("access_token").GetString(); + + var scoreObj = new Score { + UserId = userId, + ScoreGiven = 100.0, + ScoreMaximum = 100.0, + Comment = "Excellent! Task completed in Mock Tool.", + GradingProgress = GradingProgress.FullyGraded, + ActivityProgress = ActivityProgress.Completed, // Indicate that the activity is completed + TimeStamp = DateTime.UtcNow + }; + + var scoreRequest = new HttpRequestMessage(HttpMethod.Post, lineItemUrl) { + Content = new StringContent(JsonSerializer.Serialize(scoreObj), System.Text.Encoding.UTF8, "application/vnd.ims.lti-ags.v1.score+json") + }; + scoreRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + var scoreResponse = await client.SendAsync(scoreRequest); + + // --- NEW: Completion window with redirect --- + var statusColor = scoreResponse.IsSuccessStatusCode ? "green" : "red"; + var statusText = scoreResponse.IsSuccessStatusCode + ? "Score successfully submitted!" + : "Error submitting score."; + + var html = $@" + + + + + + +
+
{(scoreResponse.IsSuccessStatusCode ? "✓" : "✕")}
+

{statusText}

+

You will be redirected back to HwProj in 3 seconds...

+ Return Now +
+ + "; + + return Content(html, "text/html"); + } + + private IActionResult HandleResourceLink(JwtSecurityToken token) + { + var presentationClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/launch_presentation"); + var presentationJson = JsonDocument.Parse(presentationClaim?.Value ?? "{}"); + var returnUrl = presentationJson.RootElement.TryGetProperty("return_url", out var rProp) ? rProp.GetString() : ""; + + var resourceLinkClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/resource_link"); + var taskId = JsonDocument.Parse(resourceLinkClaim?.Value ?? "{}").RootElement.GetProperty("id").GetString(); + + var agsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint"); + var lineItemUrl = JsonDocument.Parse(agsClaim?.Value ?? "{}").RootElement.GetProperty("lineitem").GetString(); + + var html = $@" + + +

Performing task: {taskId}

+
+ + + + + + +
+ + "; + return Content(html, "text/html"); + } + + private string CreateClientAssertion(string platformIssuer) + { + var claims = new List { + new(JwtRegisteredClaimNames.Iss, ToolIss), // Name as Issuer + new(JwtRegisteredClaimNames.Sub, ToolNameId), // Name as Subject + new(JwtRegisteredClaimNames.Aud, $"{platformIssuer}/api/lti/token"), + new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), + new(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var jwt = new JwtSecurityToken( + header: new JwtHeader(new SigningCredentials(_signingKey, SecurityAlgorithms.RsaSha256)), + payload: new JwtPayload(claims) + ); + return new JwtSecurityTokenHandler().WriteToken(jwt); + } +} \ No newline at end of file diff --git a/hwproj.front/src/components/Solutions/LtiLaunchButton.tsx b/hwproj.front/src/components/Solutions/LtiLaunchButton.tsx index a73997cb3..f869df9f8 100644 --- a/hwproj.front/src/components/Solutions/LtiLaunchButton.tsx +++ b/hwproj.front/src/components/Solutions/LtiLaunchButton.tsx @@ -1,6 +1,8 @@ import React, { FC, useState } from "react"; import { LoadingButton } from "@mui/lab"; import ApiSingleton from "../../api/ApiSingleton"; +import {Button, Dialog, DialogActions, DialogContent, DialogTitle} from "@mui/material"; +import DialogContentText from "@material-ui/core/DialogContentText"; interface LtiLaunchButtonProps { courseId: number; @@ -11,6 +13,7 @@ interface LtiLaunchButtonProps { export const LtiLaunchButton: FC = ({ courseId, toolId, taskId, ltiLaunchUrl }) => { const [isLoading, setIsLoading] = useState(false); + const [openDialog, setOpenDialog] = useState(false); const submitLtiForm = (formData: any) => { const windowName = `lti_launch_task_${taskId}`; @@ -36,6 +39,7 @@ export const LtiLaunchButton: FC = ({ courseId, toolId, ta }; const handleLaunch = async () => { + setOpenDialog(false); setIsLoading(true); try { const response = await ApiSingleton.ltiAuthApi.ltiAuthStartLti( @@ -62,15 +66,43 @@ export const LtiLaunchButton: FC = ({ courseId, toolId, ta }; return ( - - Решить задачу - + <> + setOpenDialog(true)} + loading={isLoading} + > + Решить задачу + + + setOpenDialog(false)} + aria-labelledby="lti-warning-title" + aria-describedby="lti-warning-desc" + > + + Внимание + + + + Вы переходите к решению задачи во внешней системе. +

+ Обратите внимание: баллы за решение могут появиться в HwProj не сразу, а с небольшой задержкой после завершения работы. +
+
+ + + + +
+ ); }; \ No newline at end of file From 16a57bcdd8e23c81035518afeab50042f60dbfd0 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Sun, 1 Mar 2026 14:44:40 +0300 Subject: [PATCH 19/26] fix: added a parameter required for the protocol --- .../Lti/Controllers/JwksController.cs | 23 +- .../Lti/Controllers/LtiAuthController.cs | 10 +- .../LtiDeepLinkingReturnController.cs | 2 +- .../Lti/Services/ILtiTokenService.cs | 1 + .../Lti/Services/LtiTokenService.cs | 17 + .../HwProj.AuthService.API/appsettings.json | 4 +- .../ViewModels/HomeworkTaskViewModels.cs | 10 +- .../Controllers/HomeworksController.cs | 6 +- .../Controllers/TasksController.cs | 8 +- .../Domains/MappingExtensions.cs | 25 +- ...301070634_AddCustomParamsToLti.Designer.cs | 351 ++++++++ .../20260301070634_AddCustomParamsToLti.cs | 22 + .../Migrations/CourseContextModelSnapshot.cs | 2 + .../Models/HomeworkTaskLtiUrl.cs | 2 + .../Models/HomeworkTaskTemplate.cs | 2 +- .../Models/LtiLaunchData.cs | 5 + .../Repositories/ITasksRepository.cs | 6 +- .../Repositories/TasksRepository.cs | 24 +- .../Services/CoursesService.cs | 16 +- .../Services/HomeworksService.cs | 9 +- .../Services/ITasksService.cs | 8 +- .../Services/TasksService.cs | 25 +- .../appsettings.json | 4 +- .../appsettings.json | 4 +- .../Controllers/SolutionsController.cs | 2 +- .../Services/SolutionsService.cs | 2 +- .../appsettings.json | 4 +- hwproj.front/src/api/api.ts | 835 ++++++++++++++++-- .../components/Courses/CourseExperimental.tsx | 6 +- .../Homeworks/CourseHomeworkExperimental.tsx | 2 +- .../components/Solutions/LtiLaunchButton.tsx | 11 +- .../Solutions/TaskSolutionsPage.tsx | 5 +- .../Tasks/CourseTaskExperimental.tsx | 4 +- .../src/components/Tasks/LtiImportButton.tsx | 45 +- 34 files changed, 1329 insertions(+), 173 deletions(-) create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.Designer.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Models/LtiLaunchData.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs index 2e61fb688..976078f61 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs @@ -36,19 +36,20 @@ public IActionResult GetJwks() var publicParams = rsa.ExportParameters(false); - var securityKey = new RsaSecurityKey(publicParams) - { - KeyId = keyConfig.KeyId - }; - - var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(securityKey); - - jwk.Use = "sig"; - jwk.Alg = "RS256"; - var jwks = new { - keys = new[] { jwk } + keys = new[] + { + new + { + kty = "RSA", + e = Base64UrlEncoder.Encode(publicParams.Exponent), + n = Base64UrlEncoder.Encode(publicParams.Modulus), + kid = keyConfig.KeyId, + alg = "RS256", + use = "sig" + } + } }; return Ok(jwks); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs index 1c768b51b..f8cdb778b 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs @@ -83,7 +83,7 @@ public async Task AuthorizeLti( clientId: clientId, toolId: payload.ToolId, courseId: payload.CourseId, - targetLinkUri: tool.DeepLink, + targetLinkUri: redirectUri, userId: payload.UserId, nonce: nonce ); @@ -93,7 +93,8 @@ public async Task AuthorizeLti( clientId: clientId, toolId: payload.ToolId, courseId: payload.CourseId, - targetLinkUri: tool.LaunchUrl, + targetLinkUri: redirectUri, + ltiCustomParams: payload.Custom, userId: payload.UserId, nonce: nonce, resourceLinkId: payload.ResourceLinkId!); @@ -124,6 +125,7 @@ public async Task StartLti( [FromQuery] string? courseId, [FromQuery] string? toolId, [FromQuery] string? ltiLaunchUrl, + [FromQuery] string? ltiCustomParams, [FromQuery] bool isDeepLink = false) { var userId = User.FindFirstValue("_id"); @@ -181,7 +183,8 @@ public async Task StartLti( UserId = userId, CourseId = courseId, ToolId = toolId, - ResourceLinkId = resourceLinkId + ResourceLinkId = resourceLinkId, + Custom = ltiCustomParams }; } else @@ -254,5 +257,6 @@ private class LtiHintPayload public string? ResourceLinkId { get; set; } public string? CourseId { get; set; } public string? ToolId { get; set; } + public string? Custom { get; set; } } } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs index bec1c4581..6fdb7dfaf 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs @@ -39,7 +39,7 @@ public async Task OnDeepLinkingReturnAsync([FromForm] IFormCollec } var unverifiedToken = handler.ReadJwtToken(tokenString); - var clientId = unverifiedToken.Subject; + var clientId = unverifiedToken.Issuer; var tool = await toolService.GetByClientIdAsync(clientId); if (tool == null) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs index 456e06959..9864ea51e 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs @@ -18,6 +18,7 @@ public string CreateResourceLinkToken( string toolId, string courseId, string targetLinkUri, + string? ltiCustomParams, string userId, string nonce, string resourceLinkId); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs index e2bfe4a52..3ae9c6f07 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs @@ -3,6 +3,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; +using System.Text.Json; using HwProj.APIGateway.API.Lti.Models; using HwProj.APIGateway.API.LTI.Services; using LtiAdvantage.DeepLinking; @@ -57,6 +58,7 @@ public string CreateResourceLinkToken( string toolId, string courseId, string targetLinkUri, + string? ltiCustomParams, string userId, string nonce, string resourceLinkId) @@ -93,6 +95,21 @@ public string CreateResourceLinkToken( } }; + if (string.IsNullOrEmpty(ltiCustomParams)) + { + request.Custom = new Dictionary(); + return this.CreateJwt(clientId, request); + } + + try + { + request.Custom = JsonSerializer.Deserialize>(ltiCustomParams); + } + catch (JsonException) + { + request.Custom = new Dictionary(); + } + return this.CreateJwt(clientId, request); } diff --git a/HwProj.AuthService/HwProj.AuthService.API/appsettings.json b/HwProj.AuthService/HwProj.AuthService.API/appsettings.json index c2777316b..e95f4a1f7 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/appsettings.json +++ b/HwProj.AuthService/HwProj.AuthService.API/appsettings.json @@ -29,8 +29,8 @@ }, "EventBus": { "EventBusHostName": "localhost", - "EventBusUserName": "guest", - "EventBusPassword": "guest", + "EventBusUserName": "user", + "EventBusPassword": "password", "EventBusVirtualHost": "/", "EventBusQueueName": "AuthService", "EventBusRetryCount": "5" diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs index 29efee3ff..e77bea6e4 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkTaskViewModels.cs @@ -37,7 +37,7 @@ public class HomeworkTaskViewModel public bool IsDeferred { get; set; } [JsonProperty] - public string? LtiLaunchUrl { get; set; } + public LtiLaunchData? LtiLaunchData { get; set; } } public class HomeworkTaskForEditingViewModel @@ -67,6 +67,12 @@ public class CreateTaskViewModel public ActionOptions? ActionOptions { get; set; } - public string? LtiLaunchUrl { get; set; } + public LtiLaunchData? LtiLaunchData { get; set; } + } + + public class LtiLaunchData + { + public string LtiLaunchUrl { get; set; } + public string? CustomParams { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs index d003aad20..3184367fe 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs @@ -89,13 +89,13 @@ private async Task FillLtiUrls(HomeworkViewModel viewModel) if (viewModel.Tasks != null && viewModel.Tasks.Any()) { var taskIds = viewModel.Tasks.Select(t => t.Id).ToArray(); - var ltiUrls = await _tasksService.GetLtiUrlsForTasksAsync(taskIds); + var ltilaunchMultipleData = await _tasksService.GetLtiDataForTasksAsync(taskIds); foreach (var task in viewModel.Tasks) { - if (ltiUrls.TryGetValue(task.Id, out var url)) + if (ltilaunchMultipleData.TryGetValue(task.Id, out var ltiLaunchData)) { - task.LtiLaunchUrl = url; + task.LtiLaunchData = ltiLaunchData.ToLtiLaunchData(); } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs index 86f039f72..5713a88d9 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs @@ -48,7 +48,9 @@ public async Task GetTask(long taskId) var task = taskFromDb.ToHomeworkTaskViewModel(); - task.LtiLaunchUrl = await _tasksService.GetTaskLtiUrlAsync(taskId); + var ltiLaunchData = await _tasksService.GetTaskLtiDataAsync(taskId); + task.LtiLaunchData = ltiLaunchData.ToLtiLaunchData(); + return Ok(task); } @@ -78,7 +80,7 @@ public async Task AddTask(long homeworkId, [FromBody] CreateTaskV var task = await _tasksService.AddTaskAsync( homeworkId, taskViewModel.ToHomeworkTask(), - taskViewModel.LtiLaunchUrl + taskViewModel.LtiLaunchData.ToLtiLaunchData() ); return Ok(task); @@ -105,7 +107,7 @@ public async Task UpdateTask(long taskId, [FromBody] CreateTaskVi taskId, taskViewModel.ToHomeworkTask(), taskViewModel.ActionOptions ?? ActionOptions.Default, - taskViewModel.LtiLaunchUrl + taskViewModel.LtiLaunchData.ToLtiLaunchData() ); return Ok(updatedTask.ToHomeworkTaskViewModel()); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs index 7ee1f439f..e32e60b92 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs @@ -169,7 +169,12 @@ public static HomeworkTaskTemplate ToHomeworkTaskTemplate(this HomeworkTaskViewM IsDeadlineStrict = task.IsDeadlineStrict, HasSpecialPublicationDate = task.PublicationDate != null, HasSpecialDeadlineDate = task.DeadlineDate != null, - LtiLaunchUrl = task.LtiLaunchUrl + LtiLaunchData = task.LtiLaunchData == null ? null : + new LtiLaunchData + { + LtiLaunchUrl = task.LtiLaunchData.LtiLaunchUrl, + CustomParams = task.LtiLaunchData.CustomParams + } }; public static Course ToCourse(this CourseTemplate courseTemplate) @@ -205,5 +210,23 @@ public static HomeworkTask ToHomeworkTask(this HomeworkTaskTemplate taskTemplate PublicationDate = taskTemplate.HasSpecialPublicationDate ? DateToOverride : (DateTime?)null, DeadlineDate = taskTemplate.HasSpecialDeadlineDate ? DateToOverride : (DateTime?)null, }; + + public static LtiLaunchData? ToLtiLaunchData( + this HwProj.Models.CoursesService.ViewModels.LtiLaunchData? ltiLaunchData) + => ltiLaunchData == null ? null : + new LtiLaunchData + { + LtiLaunchUrl = ltiLaunchData.LtiLaunchUrl, + CustomParams = ltiLaunchData.CustomParams + }; + + public static HwProj.Models.CoursesService.ViewModels.LtiLaunchData? ToLtiLaunchData( + this LtiLaunchData? ltiLaunchData) + => ltiLaunchData == null ? null : + new HwProj.Models.CoursesService.ViewModels.LtiLaunchData + { + LtiLaunchUrl = ltiLaunchData.LtiLaunchUrl, + CustomParams = ltiLaunchData.CustomParams + }; } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.Designer.cs new file mode 100644 index 000000000..c7d99fcde --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.Designer.cs @@ -0,0 +1,351 @@ +// +using System; +using HwProj.CoursesService.API.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace HwProj.CoursesService.API.Migrations +{ + [DbContext(typeof(CourseContext))] + [Migration("20260301070634_AddCustomParamsToLti")] + partial class AddCustomParamsToLti + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("MentorId"); + + b.Property("StudentId"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Assignments"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupName"); + + b.Property("InviteCode"); + + b.Property("IsCompleted"); + + b.Property("IsOpen"); + + b.Property("LtiToolId"); + + b.Property("MentorIds"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("FilterJson"); + + b.HasKey("Id"); + + b.ToTable("CourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("IsAccepted"); + + b.Property("StudentId"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("CourseMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupId"); + + b.Property("StudentId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasAlternateKey("GroupId", "StudentId"); + + b.ToTable("GroupMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("DeadlineDate"); + + b.Property("Description"); + + b.Property("HasDeadline"); + + b.Property("IsDeadlineStrict"); + + b.Property("PublicationDate"); + + b.Property("Tags"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("DeadlineDate"); + + b.Property("Description"); + + b.Property("HasDeadline"); + + b.Property("HomeworkId"); + + b.Property("IsDeadlineStrict"); + + b.Property("MaxRating"); + + b.Property("PublicationDate"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("HomeworkId"); + + b.ToTable("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + { + b.Property("TaskId"); + + b.Property("CustomParams"); + + b.Property("LtiLaunchUrl") + .IsRequired(); + + b.HasKey("TaskId"); + + b.ToTable("TaskLtiUrls"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.Property("CourseMateId"); + + b.Property("Description"); + + b.Property("Tags"); + + b.HasKey("CourseMateId"); + + b.ToTable("StudentCharacteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupId"); + + b.Property("TaskId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("TasksModels"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Answer") + .HasMaxLength(1000); + + b.Property("IsPrivate"); + + b.Property("LecturerId"); + + b.Property("StudentId"); + + b.Property("TaskId"); + + b.Property("Text") + .HasMaxLength(1000); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Questions"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.Property("CourseId"); + + b.Property("UserId"); + + b.Property("CourseFilterId"); + + b.HasKey("CourseId", "UserId"); + + b.HasIndex("CourseFilterId"); + + b.ToTable("UserToCourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("Assignments") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("CourseMates") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group") + .WithMany("GroupMates") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("Homeworks") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") + .WithMany("Tasks") + .HasForeignKey("HomeworkId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") + .WithOne() + .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", "TaskId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseMate") + .WithOne("Characteristics") + .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group") + .WithMany("Tasks") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") + .WithMany() + .HasForeignKey("CourseFilterId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.cs new file mode 100644 index 000000000..923322f71 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace HwProj.CoursesService.API.Migrations +{ + public partial class AddCustomParamsToLti : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CustomParams", + table: "TaskLtiUrls", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CustomParams", + table: "TaskLtiUrls"); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs index dd3b2b142..bc8a631a0 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -190,6 +190,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("TaskId"); + b.Property("CustomParams"); + b.Property("LtiLaunchUrl") .IsRequired(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs index 8ebde9143..a89a91f24 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs @@ -11,5 +11,7 @@ public class HomeworkTaskLtiUrl [Required] public string LtiLaunchUrl { get; set; } + + public string? CustomParams { get; set; } } } \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs index 1fcc964e6..72be7a8b9 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskTemplate.cs @@ -16,6 +16,6 @@ public class HomeworkTaskTemplate public bool HasSpecialDeadlineDate { get; set; } - public string? LtiLaunchUrl { get; set; } + public LtiLaunchData? LtiLaunchData { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/LtiLaunchData.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/LtiLaunchData.cs new file mode 100644 index 000000000..d353d1461 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/LtiLaunchData.cs @@ -0,0 +1,5 @@ +public class LtiLaunchData +{ + public string LtiLaunchUrl { get; set; } + public string? CustomParams { get; set; } +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs index 3e5dc440f..5c4cad1e0 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs @@ -9,9 +9,9 @@ public interface ITasksRepository : ICrudRepository { Task GetWithHomeworkAsync(long id); - Task AddLtiUrlAsync(long taskId, string ltiUrl); - Task GetLtiUrlAsync(long taskId); + Task AddLtiUrlAsync(long taskId, LtiLaunchData ltiLaunchData); + Task GetLtiDataAsync(long taskId); - Task> GetLtiUrlsForTasksAsync(IEnumerable taskIds); + Task> GetLtiDataForTasksAsync(IEnumerable taskIds); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs index 9681321ad..ae418e2f2 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs @@ -22,13 +22,14 @@ public TasksRepository(CourseContext context) .FirstOrDefaultAsync(x => x.Id == id); } - public async Task AddLtiUrlAsync(long taskId, string ltiUrl) + public async Task AddLtiUrlAsync(long taskId, LtiLaunchData ltiLaunchData) { var existingRecord = await Context.Set().FindAsync(taskId); if (existingRecord != null) { - existingRecord.LtiLaunchUrl = ltiUrl; + existingRecord.LtiLaunchUrl = ltiLaunchData.LtiLaunchUrl; + existingRecord.CustomParams = ltiLaunchData.CustomParams; Context.Set().Update(existingRecord); } else @@ -36,7 +37,8 @@ public async Task AddLtiUrlAsync(long taskId, string ltiUrl) var ltiRecord = new HomeworkTaskLtiUrl { TaskId = taskId, - LtiLaunchUrl = ltiUrl + LtiLaunchUrl = ltiLaunchData.LtiLaunchUrl, + CustomParams = ltiLaunchData.CustomParams }; await Context.Set().AddAsync(ltiRecord); } @@ -44,18 +46,26 @@ public async Task AddLtiUrlAsync(long taskId, string ltiUrl) await Context.SaveChangesAsync(); } - public async Task GetLtiUrlAsync(long taskId) + public async Task GetLtiDataAsync(long taskId) { var record = await Context.Set().FindAsync(taskId); - return record?.LtiLaunchUrl; + return record == null ? null : new LtiLaunchData + { + LtiLaunchUrl = record.LtiLaunchUrl, + CustomParams = record.CustomParams + }; } - public async Task> GetLtiUrlsForTasksAsync(IEnumerable taskIds) + public async Task> GetLtiDataForTasksAsync(IEnumerable taskIds) { return await Context.Set() .Where(t => taskIds.Contains(t.TaskId)) - .ToDictionaryAsync(t => t.TaskId, t => t.LtiLaunchUrl); + .ToDictionaryAsync(t => t.TaskId, t => new LtiLaunchData + { + LtiLaunchUrl = t.LtiLaunchUrl, + CustomParams = t.CustomParams + }); } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs index ee3a37eba..74cf28f9b 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -77,7 +77,7 @@ public async Task GetAllAsync() var groups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(course.Id).ToArrayAsync(); var courseDto = course.ToCourseDto(); - await FillLtiUrlsForCourseDtos(new[] { courseDto }); + await FillLtiDataForCourseDtos(new[] { courseDto }); courseDto.Groups = groups.Select(g => new GroupViewModel @@ -167,9 +167,9 @@ private async Task AddFromTemplateAsync(CourseTemplate courseTemplate, L foreach (var pair in taskPairs) { - if (!string.IsNullOrEmpty(pair.Template.LtiLaunchUrl)) + if (pair.Template.LtiLaunchData != null) { - await _tasksRepository.AddLtiUrlAsync(pair.NewEntity.Id, pair.Template.LtiLaunchUrl); + await _tasksRepository.AddLtiUrlAsync(pair.NewEntity.Id, pair.Template.LtiLaunchData); } } @@ -306,7 +306,7 @@ public async Task GetUserCoursesAsync(string userId, string role) var result = await _courseFilterService.ApplyFiltersToCourses( userId, coursesWithValues.Select(c => c.ToCourseDto()).ToArray()); - await FillLtiUrlsForCourseDtos(result); + await FillLtiDataForCourseDtos(result); if (role == Roles.ExpertRole) { @@ -388,7 +388,7 @@ await _courseMatesRepository.FindAll(x => x.CourseId == courseId && x.StudentId return true; } - private async Task FillLtiUrlsForCourseDtos(IEnumerable courses) + private async Task FillLtiDataForCourseDtos(IEnumerable courses) { var allTasks = courses.SelectMany(c => c.Homeworks).SelectMany(h => h.Tasks).ToList(); @@ -396,13 +396,13 @@ private async Task FillLtiUrlsForCourseDtos(IEnumerable courses) { var taskIds = allTasks.Select(t => t.Id).ToArray(); - var ltiUrls = await _tasksRepository.GetLtiUrlsForTasksAsync(taskIds); + var ltiUrls = await _tasksRepository.GetLtiDataForTasksAsync(taskIds); foreach (var taskDto in allTasks) { - if (ltiUrls.TryGetValue(taskDto.Id, out var url)) + if (ltiUrls.TryGetValue(taskDto.Id, out var ltiLaunchData)) { - taskDto.LtiLaunchUrl = url; + taskDto.LtiLaunchData = ltiLaunchData.ToLtiLaunchData(); } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index eb234d33c..91891220c 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -50,12 +50,13 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo for (var i = 0; i < createdTasks.Count && i < taskModels.Count; i++) { - var url = taskModels[i].LtiLaunchUrl; - - if (!string.IsNullOrEmpty(url)) + var ltiLaunchData = taskModels[i].LtiLaunchData.ToLtiLaunchData(); + if (ltiLaunchData == null) { - await _tasksRepository.AddLtiUrlAsync(createdTasks[i].Id, url!); + continue; } + + await _tasksRepository.AddLtiUrlAsync(createdTasks[i].Id, ltiLaunchData); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs index ff1ba3cfb..171cbcbd8 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs @@ -9,10 +9,10 @@ public interface ITasksService { Task GetTaskAsync(long taskId); Task GetForEditingTaskAsync(long taskId); - Task AddTaskAsync(long homeworkId, HomeworkTask task, string? ltiLaunchUrl = null); + Task AddTaskAsync(long homeworkId, HomeworkTask task, LtiLaunchData? ltiLaunchData = null); Task DeleteTaskAsync(long taskId); - Task UpdateTaskAsync(long taskId, HomeworkTask update, ActionOptions options, string? ltiLaunchUrl = null); - Task GetTaskLtiUrlAsync(long taskId); - Task> GetLtiUrlsForTasksAsync(long[] taskIds); + Task UpdateTaskAsync(long taskId, HomeworkTask update, ActionOptions options, LtiLaunchData? ltiLaunchData = null); + Task GetTaskLtiDataAsync(long taskId); + Task> GetLtiDataForTasksAsync(long[] taskIds); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs index fa22ab7c1..b41f9fe22 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs @@ -41,7 +41,8 @@ public async Task GetForEditingTaskAsync(long taskId) return await _tasksRepository.GetWithHomeworkAsync(taskId); } - public async Task AddTaskAsync(long homeworkId, HomeworkTask task, string? ltiLaunchUrl = null) + public async Task AddTaskAsync( + long homeworkId, HomeworkTask task, LtiLaunchData? ltiLaunchData = null) { task.HomeworkId = homeworkId; @@ -50,9 +51,9 @@ public async Task AddTaskAsync(long homeworkId, HomeworkTask task, var taskId = await _tasksRepository.AddAsync(task); - if (!string.IsNullOrEmpty(ltiLaunchUrl)) + if (ltiLaunchData != null && !string.IsNullOrEmpty(ltiLaunchData.LtiLaunchUrl)) { - await _tasksRepository.AddLtiUrlAsync(taskId, ltiLaunchUrl!); + await _tasksRepository.AddLtiUrlAsync(taskId, ltiLaunchData); } var deadlineDate = task.DeadlineDate ?? homework.DeadlineDate; @@ -70,7 +71,11 @@ public async Task DeleteTaskAsync(long taskId) await _tasksRepository.DeleteAsync(taskId); } - public async Task UpdateTaskAsync(long taskId, HomeworkTask update, ActionOptions options, string? ltiLaunchUrl = null) + public async Task UpdateTaskAsync( + long taskId, + HomeworkTask update, + ActionOptions options, + LtiLaunchData? ltiLaunchData = null) { var task = await _tasksRepository.GetWithHomeworkAsync(taskId); if (task == null) throw new InvalidOperationException("Task not found"); @@ -94,9 +99,9 @@ public async Task UpdateTaskAsync(long taskId, HomeworkTask update PublicationDate = update.PublicationDate }); - if (ltiLaunchUrl != null) + if (ltiLaunchData != null && !string.IsNullOrEmpty(ltiLaunchData.LtiLaunchUrl)) { - await _tasksRepository.AddLtiUrlAsync(taskId, ltiLaunchUrl); + await _tasksRepository.AddLtiUrlAsync(taskId, ltiLaunchData); } @@ -106,14 +111,14 @@ public async Task UpdateTaskAsync(long taskId, HomeworkTask update return updatedTask; } - public async Task GetTaskLtiUrlAsync(long taskId) + public async Task GetTaskLtiDataAsync(long taskId) { - return await _tasksRepository.GetLtiUrlAsync(taskId); + return await _tasksRepository.GetLtiDataAsync(taskId); } - public async Task> GetLtiUrlsForTasksAsync(long[] taskIds) + public async Task> GetLtiDataForTasksAsync(long[] taskIds) { - return await _tasksRepository.GetLtiUrlsForTasksAsync(taskIds); + return await _tasksRepository.GetLtiDataForTasksAsync(taskIds); } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json b/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json index 4f4452b1b..e64da15b9 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json +++ b/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json @@ -11,8 +11,8 @@ "AllowedHosts": "*", "EventBus": { "EventBusHostName": "localhost", - "EventBusUserName": "guest", - "EventBusPassword": "guest", + "EventBusUserName": "user", + "EventBusPassword": "password", "EventBusVirtualHost": "/", "EventBusQueueName": "CoursesService", "EventBusRetryCount": "5" diff --git a/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json b/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json index f196fffa6..69b695e1b 100644 --- a/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json +++ b/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json @@ -11,8 +11,8 @@ "AllowedHosts": "*", "EventBus": { "EventBusHostName": "localhost", - "EventBusUserName": "guest", - "EventBusPassword": "guest", + "EventBusUserName": "user", + "EventBusPassword": "password", "EventBusVirtualHost": "/", "EventBusQueueName": "NotificationService", "EventBusRetryCount": "5" diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs index 5b1437fc0..6e9c59128 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Controllers/SolutionsController.cs @@ -20,7 +20,7 @@ namespace HwProj.SolutionsService.API.Controllers { [Route("api/[controller]")] - [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] + // [Authorize(AuthenticationSchemes = AuthSchemeConstants.UserIdAuthentication)] [ApiController] public class SolutionsController : Controller { diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs index 9f9094410..e5b4e9416 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/Services/SolutionsService.cs @@ -357,7 +357,7 @@ await _coursesServiceClient.GetCourseByTaskForLti(solution.TaskId, solution.Stud await _coursesServiceClient.GetCourseByTask(solution.TaskId); var student = await _authServiceClient.GetAccountData(solutionModel.StudentId); var studentModel = _mapper.Map(student); - _eventBus.Publish(new StudentPassTaskEvent(course, solutionModel, studentModel, task)); + // _eventBus.Publish(new StudentPassTaskEvent(course, solutionModel, studentModel, task)); } if (task.Tags.Contains(HomeworkTags.Test)) diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/appsettings.json b/HwProj.SolutionsService/HwProj.SolutionsService.API/appsettings.json index e00816874..b2b0e5a1b 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/appsettings.json +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/appsettings.json @@ -11,8 +11,8 @@ "AllowedHosts": "*", "EventBus": { "EventBusHostName": "localhost", - "EventBusUserName": "guest", - "EventBusPassword": "guest", + "EventBusUserName": "user", + "EventBusPassword": "password", "EventBusVirtualHost": "/", "EventBusQueueName": "SolutionsService", "EventBusRetryCount": "5" diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index db3740799..1f5e3df21 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -157,6 +157,19 @@ export interface ActionOptions { */ sendNotification?: boolean; } +/** + * + * @export + * @enum {string} + */ +export enum ActivityProgress { + None = 'None', + Completed = 'Completed', + Initialized = 'Initialized', + InProgress = 'InProgress', + Started = 'Started', + Submitted = 'Submitted' +} /** * * @export @@ -705,10 +718,10 @@ export interface CreateTaskViewModel { actionOptions?: ActionOptions; /** * - * @type {string} + * @type {LtiLaunchData} * @memberof CreateTaskViewModel */ - ltiLaunchUrl?: string; + ltiLaunchData?: LtiLaunchData; } /** * @@ -1054,6 +1067,19 @@ export interface GithubCredentials { */ githubId?: string; } +/** + * + * @export + * @enum {string} + */ +export enum GradingProgress { + None = 'None', + Failed = 'Failed', + FullyGraded = 'FullyGraded', + NotReady = 'NotReady', + Pending = 'Pending', + PendingManual = 'PendingManual' +} /** * * @export @@ -1235,10 +1261,10 @@ export interface HomeworkTaskViewModel { isDeferred?: boolean; /** * - * @type {string} + * @type {LtiLaunchData} * @memberof HomeworkTaskViewModel */ - ltiLaunchUrl?: string; + ltiLaunchData?: LtiLaunchData; } /** * @@ -1526,6 +1552,38 @@ export interface LtiDeepLinkReturnBody { */ form?: Array; } +/** + * + * @export + * @interface LtiLaunchData + */ +export interface LtiLaunchData { + /** + * + * @type {string} + * @memberof LtiLaunchData + */ + ltiLaunchUrl?: string; + /** + * + * @type {string} + * @memberof LtiLaunchData + */ + customParams?: string; +} +/** + * + * @export + * @interface LtiTokenBody + */ +export interface LtiTokenBody { + /** + * + * @type {Array} + * @memberof LtiTokenBody + */ + form?: Array; +} /** * * @export @@ -1632,6 +1690,74 @@ export interface MocktoolLoginBody { */ ltiMessageHint?: string; } +/** + * + * @export + * @interface MocktoolSendscoreBody + */ +export interface MocktoolSendscoreBody { + /** + * + * @type {string} + * @memberof MocktoolSendscoreBody + */ + lineItemUrl?: string; + /** + * + * @type {string} + * @memberof MocktoolSendscoreBody + */ + userId?: string; + /** + * + * @type {string} + * @memberof MocktoolSendscoreBody + */ + platformIss?: string; + /** + * + * @type {string} + * @memberof MocktoolSendscoreBody + */ + taskId?: string; + /** + * + * @type {string} + * @memberof MocktoolSendscoreBody + */ + returnUrl?: string; +} +/** + * + * @export + * @interface MocktoolSubmitselectionBody + */ +export interface MocktoolSubmitselectionBody { + /** + * + * @type {Array} + * @memberof MocktoolSubmitselectionBody + */ + selectedIds?: Array; + /** + * + * @type {string} + * @memberof MocktoolSubmitselectionBody + */ + returnUrl?: string; + /** + * + * @type {string} + * @memberof MocktoolSubmitselectionBody + */ + data?: string; + /** + * + * @type {string} + * @memberof MocktoolSubmitselectionBody + */ + platformIssuer?: string; +} /** * * @export @@ -1993,6 +2119,55 @@ export interface ScopeDTO { */ courseUnitId?: number; } +/** + * + * @export + * @interface Score + */ +export interface Score { + /** + * + * @type {ActivityProgress} + * @memberof Score + */ + activityProgress?: ActivityProgress; + /** + * + * @type {string} + * @memberof Score + */ + comment?: string; + /** + * + * @type {GradingProgress} + * @memberof Score + */ + gradingProgress?: GradingProgress; + /** + * + * @type {number} + * @memberof Score + */ + scoreGiven?: number; + /** + * + * @type {number} + * @memberof Score + */ + scoreMaximum?: number; + /** + * + * @type {Date} + * @memberof Score + */ + timestamp?: Date; + /** + * + * @type {string} + * @memberof Score + */ + userId?: string; +} /** * * @export @@ -7639,27 +7814,24 @@ export class JwksApi extends BaseAPI { } /** - * LtiAuthApi - fetch parameter creator + * LtiAccessTokenApi - fetch parameter creator * @export */ -export const LtiAuthApiFetchParamCreator = function (configuration?: Configuration) { +export const LtiAccessTokenApiFetchParamCreator = function (configuration?: Configuration) { return { /** * - * @param {string} [clientId] - * @param {string} [redirectUri] - * @param {string} [state] - * @param {string} [nonce] - * @param {string} [ltiMessageHint] + * @param {Array} [form] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options: any = {}): FetchArgs { - const localVarPath = `/api/lti/authorize`; + ltiAccessTokenGetToken(form?: Array, options: any = {}): FetchArgs { + const localVarPath = `/api/lti/token`; const localVarUrlObj = url.parse(localVarPath, true); - const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + const localVarFormParams = new URLSearchParams(); // authentication Bearer required if (configuration && configuration.apiKey) { @@ -7669,50 +7841,114 @@ export const LtiAuthApiFetchParamCreator = function (configuration?: Configurati localVarHeaderParameter["Authorization"] = localVarApiKeyValue; } - if (clientId !== undefined) { - localVarQueryParameter['client_id'] = clientId; - } - - if (redirectUri !== undefined) { - localVarQueryParameter['redirect_uri'] = redirectUri; - } - - if (state !== undefined) { - localVarQueryParameter['state'] = state; - } - - if (nonce !== undefined) { - localVarQueryParameter['nonce'] = nonce; + if (form) { + form.forEach((element) => { + localVarFormParams.append('form', element as any); + }) } - if (ltiMessageHint !== undefined) { - localVarQueryParameter['lti_message_hint'] = ltiMessageHint; - } + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; 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); + localVarRequestOptions.body = localVarFormParams.toString(); return { url: url.format(localVarUrlObj), options: localVarRequestOptions, }; }, + } +}; + +/** + * LtiAccessTokenApi - functional programming interface + * @export + */ +export const LtiAccessTokenApiFp = function(configuration?: Configuration) { + return { /** * - * @param {string} [resourceLinkId] - * @param {string} [courseId] - * @param {string} [toolId] - * @param {string} [ltiLaunchUrl] - * @param {boolean} [isDeepLink] + * @param {Array} [form] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, isDeepLink?: boolean, options: any = {}): FetchArgs { - const localVarPath = `/api/lti/start`; + ltiAccessTokenGetToken(form?: Array, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAccessTokenApiFetchParamCreator(configuration).ltiAccessTokenGetToken(form, 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; + } + }); + }; + }, + } +}; + +/** + * LtiAccessTokenApi - factory interface + * @export + */ +export const LtiAccessTokenApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAccessTokenGetToken(form?: Array, options?: any) { + return LtiAccessTokenApiFp(configuration).ltiAccessTokenGetToken(form, options)(fetch, basePath); + }, + }; +}; + +/** + * LtiAccessTokenApi - object-oriented interface + * @export + * @class LtiAccessTokenApi + * @extends {BaseAPI} + */ +export class LtiAccessTokenApi extends BaseAPI { + /** + * + * @param {Array} [form] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiAccessTokenApi + */ + public ltiAccessTokenGetToken(form?: Array, options?: any) { + return LtiAccessTokenApiFp(this.configuration).ltiAccessTokenGetToken(form, options)(this.fetch, this.basePath); + } + +} +/** + * LtiAssignmentsGradesControllersApi - fetch parameter creator + * @export + */ +export const LtiAssignmentsGradesControllersApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {number} taskId + * @param {Score} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAssignmentsGradesControllersUpdateTaskScore(taskId: number, body?: Score, options: any = {}): FetchArgs { + // verify required parameter 'taskId' is not null or undefined + if (taskId === null || taskId === undefined) { + throw new RequiredError('taskId','Required parameter taskId was null or undefined when calling ltiAssignmentsGradesControllersUpdateTaskScore.'); + } + const localVarPath = `/api/lti/lineItem/{taskId}` + .replace(`{${"taskId"}}`, encodeURIComponent(String(taskId))); const localVarUrlObj = url.parse(localVarPath, true); - const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -7724,30 +7960,14 @@ export const LtiAuthApiFetchParamCreator = function (configuration?: Configurati localVarHeaderParameter["Authorization"] = localVarApiKeyValue; } - if (resourceLinkId !== undefined) { - localVarQueryParameter['resourceLinkId'] = resourceLinkId; - } - - if (courseId !== undefined) { - localVarQueryParameter['courseId'] = courseId; - } - - if (toolId !== undefined) { - localVarQueryParameter['toolId'] = toolId; - } - - if (ltiLaunchUrl !== undefined) { - localVarQueryParameter['ltiLaunchUrl'] = ltiLaunchUrl; - } - - if (isDeepLink !== undefined) { - localVarQueryParameter['isDeepLink'] = isDeepLink; - } + localVarHeaderParameter['Content-Type'] = 'application/vnd.ims.lti-ags.v1.score+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 = ("Score" !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.body = needsSerialization ? JSON.stringify(body || {}) : (body || ""); return { url: url.format(localVarUrlObj), @@ -7758,22 +7978,243 @@ export const LtiAuthApiFetchParamCreator = function (configuration?: Configurati }; /** - * LtiAuthApi - functional programming interface + * LtiAssignmentsGradesControllersApi - functional programming interface * @export */ -export const LtiAuthApiFp = function(configuration?: Configuration) { +export const LtiAssignmentsGradesControllersApiFp = function(configuration?: Configuration) { return { /** * - * @param {string} [clientId] - * @param {string} [redirectUri] - * @param {string} [state] - * @param {string} [nonce] - * @param {string} [ltiMessageHint] + * @param {number} taskId + * @param {Score} [body] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + ltiAssignmentsGradesControllersUpdateTaskScore(taskId: number, body?: Score, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAssignmentsGradesControllersApiFetchParamCreator(configuration).ltiAssignmentsGradesControllersUpdateTaskScore(taskId, 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; + } + }); + }; + }, + } +}; + +/** + * LtiAssignmentsGradesControllersApi - factory interface + * @export + */ +export const LtiAssignmentsGradesControllersApiFactory = function (configuration?: Configuration, fetch?: FetchAPI, basePath?: string) { + return { + /** + * + * @param {number} taskId + * @param {Score} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAssignmentsGradesControllersUpdateTaskScore(taskId: number, body?: Score, options?: any) { + return LtiAssignmentsGradesControllersApiFp(configuration).ltiAssignmentsGradesControllersUpdateTaskScore(taskId, body, options)(fetch, basePath); + }, + }; +}; + +/** + * LtiAssignmentsGradesControllersApi - object-oriented interface + * @export + * @class LtiAssignmentsGradesControllersApi + * @extends {BaseAPI} + */ +export class LtiAssignmentsGradesControllersApi extends BaseAPI { + /** + * + * @param {number} taskId + * @param {Score} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiAssignmentsGradesControllersApi + */ + public ltiAssignmentsGradesControllersUpdateTaskScore(taskId: number, body?: Score, options?: any) { + return LtiAssignmentsGradesControllersApiFp(this.configuration).ltiAssignmentsGradesControllersUpdateTaskScore(taskId, body, options)(this.fetch, this.basePath); + } + +} +/** + * LtiAuthApi - fetch parameter creator + * @export + */ +export const LtiAuthApiFetchParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {string} [clientId] + * @param {string} [redirectUri] + * @param {string} [state] + * @param {string} [nonce] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/lti/authorize`; + 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 (clientId !== undefined) { + localVarQueryParameter['client_id'] = clientId; + } + + if (redirectUri !== undefined) { + localVarQueryParameter['redirect_uri'] = redirectUri; + } + + if (state !== undefined) { + localVarQueryParameter['state'] = state; + } + + if (nonce !== undefined) { + localVarQueryParameter['nonce'] = nonce; + } + + if (ltiMessageHint !== undefined) { + localVarQueryParameter['lti_message_hint'] = ltiMessageHint; + } + + 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. + * @throws {RequiredError} + */ + ltiAuthCloseLtiSession(options: any = {}): FetchArgs { + const localVarPath = `/api/lti/closeLtiSession`; + 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; + } + + 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} [resourceLinkId] + * @param {string} [courseId] + * @param {string} [toolId] + * @param {string} [ltiLaunchUrl] + * @param {string} [ltiCustomParams] + * @param {boolean} [isDeepLink] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options: any = {}): FetchArgs { + const localVarPath = `/api/lti/start`; + 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 (resourceLinkId !== undefined) { + localVarQueryParameter['resourceLinkId'] = resourceLinkId; + } + + if (courseId !== undefined) { + localVarQueryParameter['courseId'] = courseId; + } + + if (toolId !== undefined) { + localVarQueryParameter['toolId'] = toolId; + } + + if (ltiLaunchUrl !== undefined) { + localVarQueryParameter['ltiLaunchUrl'] = ltiLaunchUrl; + } + + if (ltiCustomParams !== undefined) { + localVarQueryParameter['ltiCustomParams'] = ltiCustomParams; + } + + if (isDeepLink !== undefined) { + localVarQueryParameter['isDeepLink'] = isDeepLink; + } + + 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, + }; + }, + } +}; + +/** + * LtiAuthApi - functional programming interface + * @export + */ +export const LtiAuthApiFp = function(configuration?: Configuration) { + return { + /** + * + * @param {string} [clientId] + * @param {string} [redirectUri] + * @param {string} [state] + * @param {string} [nonce] + * @param {string} [ltiMessageHint] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthAuthorizeLti(clientId, redirectUri, state, nonce, ltiMessageHint, options); return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { @@ -7785,18 +8226,36 @@ export const LtiAuthApiFp = function(configuration?: Configuration) { }); }; }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthCloseLtiSession(options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthCloseLtiSession(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 {string} [resourceLinkId] * @param {string} [courseId] * @param {string} [toolId] * @param {string} [ltiLaunchUrl] + * @param {string} [ltiCustomParams] * @param {boolean} [isDeepLink] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, isDeepLink?: boolean, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, ltiLaunchUrl, isDeepLink, options); + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, ltiLaunchUrl, ltiCustomParams, isDeepLink, 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) { @@ -7829,18 +8288,27 @@ export const LtiAuthApiFactory = function (configuration?: Configuration, fetch? ltiAuthAuthorizeLti(clientId?: string, redirectUri?: string, state?: string, nonce?: string, ltiMessageHint?: string, options?: any) { return LtiAuthApiFp(configuration).ltiAuthAuthorizeLti(clientId, redirectUri, state, nonce, ltiMessageHint, options)(fetch, basePath); }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + ltiAuthCloseLtiSession(options?: any) { + return LtiAuthApiFp(configuration).ltiAuthCloseLtiSession(options)(fetch, basePath); + }, /** * * @param {string} [resourceLinkId] * @param {string} [courseId] * @param {string} [toolId] * @param {string} [ltiLaunchUrl] + * @param {string} [ltiCustomParams] * @param {boolean} [isDeepLink] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, isDeepLink?: boolean, options?: any) { - return LtiAuthApiFp(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, ltiLaunchUrl, isDeepLink, options)(fetch, basePath); + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any) { + return LtiAuthApiFp(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, ltiLaunchUrl, ltiCustomParams, isDeepLink, options)(fetch, basePath); }, }; }; @@ -7867,19 +8335,30 @@ export class LtiAuthApi extends BaseAPI { return LtiAuthApiFp(this.configuration).ltiAuthAuthorizeLti(clientId, redirectUri, state, nonce, ltiMessageHint, options)(this.fetch, this.basePath); } + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LtiAuthApi + */ + public ltiAuthCloseLtiSession(options?: any) { + return LtiAuthApiFp(this.configuration).ltiAuthCloseLtiSession(options)(this.fetch, this.basePath); + } + /** * * @param {string} [resourceLinkId] * @param {string} [courseId] * @param {string} [toolId] * @param {string} [ltiLaunchUrl] + * @param {string} [ltiCustomParams] * @param {boolean} [isDeepLink] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof LtiAuthApi */ - public ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, isDeepLink?: boolean, options?: any) { - return LtiAuthApiFp(this.configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, ltiLaunchUrl, isDeepLink, options)(this.fetch, this.basePath); + public ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any) { + return LtiAuthApiFp(this.configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, ltiLaunchUrl, ltiCustomParams, isDeepLink, options)(this.fetch, this.basePath); } } @@ -8290,6 +8769,121 @@ export const MockToolApiFetchParamCreator = function (configuration?: Configurat localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); localVarRequestOptions.body = localVarFormParams.toString(); + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} [lineItemUrl] + * @param {string} [userId] + * @param {string} [platformIss] + * @param {string} [taskId] + * @param {string} [returnUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSendScore(lineItemUrl?: string, userId?: string, platformIss?: string, taskId?: string, returnUrl?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/mocktool/send-score`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + const localVarFormParams = new URLSearchParams(); + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (lineItemUrl !== undefined) { + localVarFormParams.set('lineItemUrl', lineItemUrl as any); + } + + if (userId !== undefined) { + localVarFormParams.set('userId', userId as any); + } + + if (platformIss !== undefined) { + localVarFormParams.set('platformIss', platformIss as any); + } + + if (taskId !== undefined) { + localVarFormParams.set('taskId', taskId as any); + } + + if (returnUrl !== undefined) { + localVarFormParams.set('returnUrl', returnUrl as any); + } + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + 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); + localVarRequestOptions.body = localVarFormParams.toString(); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {Array} [selectedIds] + * @param {string} [returnUrl] + * @param {string} [data] + * @param {string} [platformIssuer] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSubmitDeepLinkingSelection(selectedIds?: Array, returnUrl?: string, data?: string, platformIssuer?: string, options: any = {}): FetchArgs { + const localVarPath = `/api/mocktool/submit-selection`; + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'POST' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + const localVarFormParams = new URLSearchParams(); + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + if (selectedIds) { + selectedIds.forEach((element) => { + localVarFormParams.append('selectedIds', element as any); + }) + } + + if (returnUrl !== undefined) { + localVarFormParams.set('returnUrl', returnUrl as any); + } + + if (data !== undefined) { + localVarFormParams.set('data', data as any); + } + + if (platformIssuer !== undefined) { + localVarFormParams.set('platformIssuer', platformIssuer as any); + } + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + 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); + localVarRequestOptions.body = localVarFormParams.toString(); + return { url: url.format(localVarUrlObj), options: localVarRequestOptions, @@ -8359,6 +8953,49 @@ export const MockToolApiFp = function(configuration?: Configuration) { }); }; }, + /** + * + * @param {string} [lineItemUrl] + * @param {string} [userId] + * @param {string} [platformIss] + * @param {string} [taskId] + * @param {string} [returnUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSendScore(lineItemUrl?: string, userId?: string, platformIss?: string, taskId?: string, returnUrl?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = MockToolApiFetchParamCreator(configuration).mockToolSendScore(lineItemUrl, userId, platformIss, taskId, returnUrl, 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 {Array} [selectedIds] + * @param {string} [returnUrl] + * @param {string} [data] + * @param {string} [platformIssuer] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSubmitDeepLinkingSelection(selectedIds?: Array, returnUrl?: string, data?: string, platformIssuer?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = MockToolApiFetchParamCreator(configuration).mockToolSubmitDeepLinkingSelection(selectedIds, returnUrl, data, platformIssuer, 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; + } + }); + }; + }, } }; @@ -8396,6 +9033,31 @@ export const MockToolApiFactory = function (configuration?: Configuration, fetch mockToolLogin(iss?: string, loginHint?: string, ltiMessageHint?: string, options?: any) { return MockToolApiFp(configuration).mockToolLogin(iss, loginHint, ltiMessageHint, options)(fetch, basePath); }, + /** + * + * @param {string} [lineItemUrl] + * @param {string} [userId] + * @param {string} [platformIss] + * @param {string} [taskId] + * @param {string} [returnUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSendScore(lineItemUrl?: string, userId?: string, platformIss?: string, taskId?: string, returnUrl?: string, options?: any) { + return MockToolApiFp(configuration).mockToolSendScore(lineItemUrl, userId, platformIss, taskId, returnUrl, options)(fetch, basePath); + }, + /** + * + * @param {Array} [selectedIds] + * @param {string} [returnUrl] + * @param {string} [data] + * @param {string} [platformIssuer] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mockToolSubmitDeepLinkingSelection(selectedIds?: Array, returnUrl?: string, data?: string, platformIssuer?: string, options?: any) { + return MockToolApiFp(configuration).mockToolSubmitDeepLinkingSelection(selectedIds, returnUrl, data, platformIssuer, options)(fetch, basePath); + }, }; }; @@ -8440,6 +9102,35 @@ export class MockToolApi extends BaseAPI { return MockToolApiFp(this.configuration).mockToolLogin(iss, loginHint, ltiMessageHint, options)(this.fetch, this.basePath); } + /** + * + * @param {string} [lineItemUrl] + * @param {string} [userId] + * @param {string} [platformIss] + * @param {string} [taskId] + * @param {string} [returnUrl] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MockToolApi + */ + public mockToolSendScore(lineItemUrl?: string, userId?: string, platformIss?: string, taskId?: string, returnUrl?: string, options?: any) { + return MockToolApiFp(this.configuration).mockToolSendScore(lineItemUrl, userId, platformIss, taskId, returnUrl, options)(this.fetch, this.basePath); + } + + /** + * + * @param {Array} [selectedIds] + * @param {string} [returnUrl] + * @param {string} [data] + * @param {string} [platformIssuer] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MockToolApi + */ + public mockToolSubmitDeepLinkingSelection(selectedIds?: Array, returnUrl?: string, data?: string, platformIssuer?: string, options?: any) { + return MockToolApiFp(this.configuration).mockToolSubmitDeepLinkingSelection(selectedIds, returnUrl, data, platformIssuer, options)(this.fetch, this.basePath); + } + } /** * NotificationsApi - fetch parameter creator diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index 256f27205..37fee6337 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -500,7 +500,7 @@ export const CourseExperimental: FC = (props) => { items.forEach(item => { - if (!item.url) { + if (!item.ltiLaunchData) { return; } @@ -526,7 +526,7 @@ export const CourseExperimental: FC = (props) => { tags: homework.tags, isDeferred: homework.isDeferred, - ltiLaunchUrl: item.url + ltiLaunchData: item.ltiLaunchData }; props.onTaskUpdate({ task: newTask }); @@ -668,7 +668,7 @@ export const CourseExperimental: FC = (props) => { {t.title}{getTip(x)} - {(t.ltiLaunchUrl) && ( + {(t.ltiLaunchData) && ( = ({ courseId, toolId, taskId, ltiLaunchUrl }) => { +export const LtiLaunchButton: FC = ({ courseId, toolId, taskId, ltiLaunchData }) => { const [isLoading, setIsLoading] = useState(false); const [openDialog, setOpenDialog] = useState(false); @@ -43,14 +44,14 @@ export const LtiLaunchButton: FC = ({ courseId, toolId, ta setIsLoading(true); try { const response = await ApiSingleton.ltiAuthApi.ltiAuthStartLti( - String(taskId), // resourceLinkId + String(taskId), String(courseId), String(toolId), - ltiLaunchUrl, + ltiLaunchData.ltiLaunchUrl, + ltiLaunchData.customParams, false ); - // Обработка ответа (как в вашем коде) let dto = response; if (response && typeof (response as any).json === 'function') { dto = await (response as any).json(); diff --git a/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx b/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx index c69aba833..1249d4e3f 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx @@ -238,12 +238,12 @@ const TaskSolutionsPage: FC = () => { - {task.ltiLaunchUrl ? ( + {task.ltiLaunchData ? ( ) : ( task.canSendSolution && ( @@ -251,7 +251,6 @@ const TaskSolutionsPage: FC = () => { fullWidth size="large" variant="contained" - // Негативный цвет, если решение уже есть color={lastSolution ? "secondary" : "primary"} onClick={(e) => { e.persist() diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index 93deb7a26..4d5e1cb53 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -99,7 +99,7 @@ const CourseTaskEditor: FC<{ deadlineDateNotSet: metadata?.hasDeadline === true && !metadata.deadlineDate, maxRating: maxRating, hasErrors: hasErrors, - ltiLaunchUrl: props.speculativeTask.ltiLaunchUrl + ltiLaunchData: props.speculativeTask.ltiLaunchData } props.onUpdate({task: update}) }, [title, description, maxRating, metadata, hasErrors]) @@ -118,7 +118,7 @@ const CourseTaskEditor: FC<{ description: description, maxRating: maxRating, actionOptions: editOptions, - ltiLaunchUrl: props.speculativeTask.ltiLaunchUrl + ltiLaunchData: props.speculativeTask.ltiLaunchData } const updatedTask = isNewTask diff --git a/hwproj.front/src/components/Tasks/LtiImportButton.tsx b/hwproj.front/src/components/Tasks/LtiImportButton.tsx index 4c326cb16..18ff8a7bb 100644 --- a/hwproj.front/src/components/Tasks/LtiImportButton.tsx +++ b/hwproj.front/src/components/Tasks/LtiImportButton.tsx @@ -1,12 +1,13 @@ import React, { FC, useEffect, useState } from "react"; -import { LoadingButton } from "@mui/lab"; +import Button from "@mui/material/Button"; import ApiSingleton from "../../api/ApiSingleton"; import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; +import {LtiLaunchData} from "@/api"; export interface LtiItemDto { title: string; text?: string; - url: string; + ltiLaunchData: LtiLaunchData; scoreMaximum: number; } @@ -17,8 +18,6 @@ interface LtiImportButtonProps { } export const LtiImportButton: FC = ({ courseId, toolId, onImport }) => { - const [isLoading, setIsLoading] = useState(false); - const submitLtiForm = (formData: any) => { const windowName = "lti_tab_" + new Date().getTime(); window.open('about:blank', windowName); @@ -43,20 +42,22 @@ export const LtiImportButton: FC = ({ courseId, toolId, on }; const handleStartLti = async () => { - setIsLoading(true); try { const response = await ApiSingleton.ltiAuthApi.ltiAuthStartLti( - undefined, String(courseId), String(toolId), undefined, true + undefined, + String(courseId), String(toolId), + undefined, + undefined, + true ); let dto = response; + console.log(dto); if (response && typeof (response as any).json === 'function') { dto = await (response as any).json(); } submitLtiForm(dto); - setTimeout(() => setIsLoading(false), 30000); } catch (e) { console.error(e); - setIsLoading(false); } }; @@ -67,22 +68,34 @@ export const LtiImportButton: FC = ({ courseId, toolId, on const rawItems = Array.isArray(payload) ? payload : [payload]; - const items = rawItems.map((item: any) => { + const items: LtiItemDto[] = rawItems.map((item: any) => { + let parsedItem = item; if (typeof item === 'string') { try { - return JSON.parse(item); + parsedItem = JSON.parse(item); } catch (e) { console.error("Ошибка парсинга JSON от LTI:", item); return null; } } - return item; - }).filter(item => item !== null); + + const mappedItem: LtiItemDto = { + title: parsedItem.title || "Задача из внешнего инструмента", + text: parsedItem.text || "", + ltiLaunchData: { + ltiLaunchUrl: parsedItem.url, + customParams: parsedItem.custom ? JSON.stringify(parsedItem.custom) : undefined + }, + + scoreMaximum: parsedItem.lineItem?.scoreMaximum || 10 + }; + + return mappedItem; + }).filter((item): item is LtiItemDto => item !== null); if (items.length > 0) { onImport(items); } - setIsLoading(false); } }; window.addEventListener("message", handleLtiMessage); @@ -90,12 +103,12 @@ export const LtiImportButton: FC = ({ courseId, toolId, on }, [onImport]); return ( - } > Импорт из внешнего инструмента - + ); }; \ No newline at end of file From 4075fdea08c0523cd95f177dc8d9ad5b87be4f9b Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Sun, 1 Mar 2026 15:37:08 +0300 Subject: [PATCH 20/26] refactor: added appsettings.json --- .../HwProj.APIGateway.API/appsettings.json | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json new file mode 100644 index 000000000..ecacb33b4 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json @@ -0,0 +1,42 @@ +{ + "Services": { + "Auth": "http://localhost:5001", + "Courses": "http://localhost:5002", + "Notifications": "http://localhost:5006", + "Solutions": "http://localhost:5007", + "Content": "http://localhost:5008" + }, + "Security": { + "SecurityKey": "U8_.wpvk93fPWG Date: Mon, 2 Mar 2026 16:12:24 +0300 Subject: [PATCH 21/26] refactor: updated appsettings.json and deleted MockToolController --- .../Lti/Controllers/MockToolController.cs | 321 ------------------ .../HwProj.APIGateway.API/appsettings.json | 2 + 2 files changed, 2 insertions(+), 321 deletions(-) delete mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs deleted file mode 100644 index 2addb708b..000000000 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs +++ /dev/null @@ -1,321 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Net.Http; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text.Json; -using System.Threading.Tasks; -using LtiAdvantage.AssignmentGradeServices; -using Microsoft.AspNetCore.Mvc; -using Microsoft.IdentityModel.Tokens; - -namespace HwProj.APIGateway.API.Lti.Controllers; - -[Route("api/mocktool")] -[ApiController] -public class MockToolController : ControllerBase -{ - private static readonly RsaSecurityKey _signingKey; - private static readonly string _keyId; - private readonly IHttpClientFactory _httpClientFactory; - - // --- IDENTITY SETTINGS (as in the sandbox) --- - // This value must match the one in the HwProj database (ClientId) - private const string ToolIss = "Local Mock Tool"; - private const string ToolNameId = "mock-tool-client-id"; - - private record MockTask(string Id, string Title, string Description, int Score); - private static readonly List _availableTasks = new() - { - new("1", "Integrals (Mock)", "Calculate definite integral", 10), - new("2", "Derivatives (Mock)", "Find the derivative of a complex function", 5), - new("3", "Limits (Mock)", "Calculate sequence limit", 8), - new("4", "Series (Mock)", "Investigate series for convergence", 12), - new("5", "Diff. Eqs (Mock)", "Solve linear equation", 15) - }; - - public MockToolController(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - } - - static MockToolController() - { - var rsa = RSA.Create(2048); - _keyId = "mock-tool-key-id"; - _signingKey = new RsaSecurityKey(rsa) { KeyId = _keyId }; - } - - [HttpGet("jwks")] - public IActionResult GetJwks() - { - var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(_signingKey); - return Ok(new { keys = new[] { jwk } }); - } - - [HttpPost("login")] - public IActionResult Login([FromForm] string iss, [FromForm] string login_hint, [FromForm] string lti_message_hint) - { - // iss here is your platform address (ngrok) - var callbackUrl = $"{iss}/api/lti/authorize?" + - $"client_id={ToolNameId}&" + - $"response_type=id_token&" + - $"redirect_uri=http://localhost:5000/api/mocktool/callback&" + - $"login_hint={login_hint}&" + - $"lti_message_hint={lti_message_hint}&" + - $"scope=openid&state=xyz&nonce={Guid.NewGuid()}"; - - return Redirect(callbackUrl); - } - - [HttpPost("callback")] - public async Task Callback([FromForm] string id_token) - { - var handler = new JwtSecurityTokenHandler(); - if (!handler.CanReadToken(id_token)) return BadRequest("Invalid Token"); - var unverifiedToken = handler.ReadJwtToken(id_token); - - string issuer = unverifiedToken.Issuer; - string platformJwksUrl = $"{issuer}/api/lti/jwks"; - - var client = _httpClientFactory.CreateClient(); - string jwksJson; - try { - jwksJson = await client.GetStringAsync(platformJwksUrl); - } catch { - return BadRequest($"Failed to download HwProj keys from {platformJwksUrl}"); - } - - var platformKeySet = new JsonWebKeySet(jwksJson); - - try { - handler.ValidateToken(id_token, new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = issuer, - ValidateAudience = true, - ValidAudience = ToolNameId, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - IssuerSigningKeys = platformKeySet.Keys - }, out _); - } catch (Exception ex) { - return Unauthorized($"HwProj signature validation error: {ex.Message}"); - } - - var messageType = unverifiedToken.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/message_type")?.Value; - - if (messageType == "LtiDeepLinkingRequest") - return RenderDeepLinkingSelectionUI(unverifiedToken); - - if (messageType == "LtiResourceLinkRequest") - return HandleResourceLink(unverifiedToken); - - return BadRequest($"Unknown message type: {messageType}"); - } - - private IActionResult RenderDeepLinkingSelectionUI(JwtSecurityToken token) - { - var settingsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings"); - if (settingsClaim == null) return BadRequest("No deep linking settings found"); - - var settings = JsonDocument.Parse(settingsClaim.Value); - var returnUrl = settings.RootElement.GetProperty("deep_link_return_url").GetString(); - var dataPayload = settings.RootElement.TryGetProperty("data", out var dataEl) ? dataEl.GetString() : ""; - - var tasksHtml = string.Join("", _availableTasks.Select(t => $@" -
- -
")); - - var html = $@" - - -

Select Tasks for HwProj

-
- - - - {tasksHtml} -
-
- - "; - - return Content(html, "text/html"); - } - - [HttpPost("submit-selection")] - public IActionResult SubmitDeepLinkingSelection( - [FromForm] List selectedIds, - [FromForm] string returnUrl, - [FromForm] string? data, - [FromForm] string platformIssuer) - { - var selectedTasks = _availableTasks.Where(t => selectedIds.Contains(t.Id)).ToList(); - - var contentItems = selectedTasks.Select(t => new Dictionary - { - ["type"] = "ltiResourceLink", - ["title"] = t.Title, - ["text"] = t.Description, - ["url"] = $"http://localhost:5000/mock/task/{t.Id}", - ["scoreMaximum"] = t.Score - }).ToList(); - - var payload = new JwtPayload - { - { "iss", ToolIss }, // Tool Name as Issuer - { "sub", ToolNameId }, // Tool Name as Subject - { "aud", platformIssuer }, // Platform (ngrok) as Audience - { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, - { "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() }, - { "nonce", Guid.NewGuid().ToString() }, - { "https://purl.imsglobal.org/spec/lti-dl/claim/message_type", "LtiDeepLinkingResponse" }, - { "https://purl.imsglobal.org/spec/lti-dl/claim/version", "1.3.0" }, - { "https://purl.imsglobal.org/spec/lti-dl/claim/content_items", contentItems } - }; - - if (!string.IsNullOrEmpty(data)) - payload.Add("https://purl.imsglobal.org/spec/lti-dl/claim/data", data); - - var credentials = new SigningCredentials(_signingKey, SecurityAlgorithms.RsaSha256); - var header = new JwtHeader(credentials); - var responseToken = new JwtSecurityToken(header, payload); - var responseString = new JwtSecurityTokenHandler().WriteToken(responseToken); - - var html = $@" - - -
- -
- - "; - - return Content(html, "text/html"); - } - - [HttpPost("send-score")] - public async Task SendScore( - [FromForm] string lineItemUrl, [FromForm] string userId, - [FromForm] string platformIss, [FromForm] string taskId, [FromForm] string returnUrl) - { - var client = _httpClientFactory.CreateClient(); - var clientAssertion = CreateClientAssertion(platformIss); - - var tokenRequest = new Dictionary { - ["grant_type"] = "client_credentials", - ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", - ["client_assertion"] = clientAssertion, - ["scope"] = "https://purl.imsglobal.org/spec/lti-ags/scope/score" - }; - - var tokenResponse = await client.PostAsync($"{platformIss}/api/lti/token", new FormUrlEncodedContent(tokenRequest)); - if (!tokenResponse.IsSuccessStatusCode) return BadRequest("Error retrieving token"); - - var tokenContent = await tokenResponse.Content.ReadAsStringAsync(); - var accessToken = JsonDocument.Parse(tokenContent).RootElement.GetProperty("access_token").GetString(); - - var scoreObj = new Score { - UserId = userId, - ScoreGiven = 100.0, - ScoreMaximum = 100.0, - Comment = "Excellent! Task completed in Mock Tool.", - GradingProgress = GradingProgress.FullyGraded, - ActivityProgress = ActivityProgress.Completed, // Indicate that the activity is completed - TimeStamp = DateTime.UtcNow - }; - - var scoreRequest = new HttpRequestMessage(HttpMethod.Post, lineItemUrl) { - Content = new StringContent(JsonSerializer.Serialize(scoreObj), System.Text.Encoding.UTF8, "application/vnd.ims.lti-ags.v1.score+json") - }; - scoreRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); - - var scoreResponse = await client.SendAsync(scoreRequest); - - // --- NEW: Completion window with redirect --- - var statusColor = scoreResponse.IsSuccessStatusCode ? "green" : "red"; - var statusText = scoreResponse.IsSuccessStatusCode - ? "Score successfully submitted!" - : "Error submitting score."; - - var html = $@" - - - - - - -
-
{(scoreResponse.IsSuccessStatusCode ? "✓" : "✕")}
-

{statusText}

-

You will be redirected back to HwProj in 3 seconds...

- Return Now -
- - "; - - return Content(html, "text/html"); - } - - private IActionResult HandleResourceLink(JwtSecurityToken token) - { - var presentationClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/launch_presentation"); - var presentationJson = JsonDocument.Parse(presentationClaim?.Value ?? "{}"); - var returnUrl = presentationJson.RootElement.TryGetProperty("return_url", out var rProp) ? rProp.GetString() : ""; - - var resourceLinkClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/resource_link"); - var taskId = JsonDocument.Parse(resourceLinkClaim?.Value ?? "{}").RootElement.GetProperty("id").GetString(); - - var agsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint"); - var lineItemUrl = JsonDocument.Parse(agsClaim?.Value ?? "{}").RootElement.GetProperty("lineitem").GetString(); - - var html = $@" - - -

Performing task: {taskId}

-
- - - - - - -
- - "; - return Content(html, "text/html"); - } - - private string CreateClientAssertion(string platformIssuer) - { - var claims = new List { - new(JwtRegisteredClaimNames.Iss, ToolIss), // Name as Issuer - new(JwtRegisteredClaimNames.Sub, ToolNameId), // Name as Subject - new(JwtRegisteredClaimNames.Aud, $"{platformIssuer}/api/lti/token"), - new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), - new(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), - new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) - }; - - var jwt = new JwtSecurityToken( - header: new JwtHeader(new SigningCredentials(_signingKey, SecurityAlgorithms.RsaSha256)), - payload: new JwtPayload(claims) - ); - return new JwtSecurityTokenHandler().WriteToken(jwt); - } -} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json index ecacb33b4..1e2357a7e 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json +++ b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json @@ -33,7 +33,9 @@ { "id": 1, "name": "", + "Issuer": "", "clientId": "", + "JwksEndpoint": "", "initiateLoginUri": "", "launchUrl": "", "deepLinking": "" From 7d2c0fb0df9c953017559607722de80597ca370a Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Fri, 20 Mar 2026 11:06:05 +0300 Subject: [PATCH 22/26] feat: added LtiMockTool --- .../Lti/Controllers/MockToolController.cs | 352 ++++++++++++++++++ .../src/components/Tasks/LtiImportButton.tsx | 2 +- 2 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs new file mode 100644 index 000000000..4aca2acd1 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/MockToolController.cs @@ -0,0 +1,352 @@ +#if DEBUG +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text.Json; +using System.Threading.Tasks; +using LtiAdvantage.AssignmentGradeServices; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; + +namespace HwProj.APIGateway.API.Lti.Controllers; + +[Route("api/mocktool")] +[ApiController] +public class MockToolController(IHttpClientFactory httpClientFactory) : ControllerBase +{ + private static readonly RsaSecurityKey SigningKey; + + private const string ToolIss = "Local Mock Tool"; + private const string ToolNameId = "mock-tool-client-id"; + + private record MockTask(string Id, string Title, string Description, int Score); + private static readonly List AvailableTasks = + [ + new MockTask("1", "Integrals (Mock)", "Calculate definite integral", 10), + new MockTask("2", "Derivatives (Mock)", "Find the derivative of a complex function", 5), + new MockTask("3", "Limits (Mock)", "Calculate sequence limit", 8), + new MockTask("4", "Series (Mock)", "Investigate series for convergence", 12), + new MockTask("5", "Diff. Eqs (Mock)", "Solve linear equation", 15) + ]; + + static MockToolController() + { + var rsa = RSA.Create(2048); + var keyId = "mock-tool-key-id"; + SigningKey = new RsaSecurityKey(rsa) { KeyId = keyId }; + } + + [HttpGet("jwks")] + public IActionResult GetJwks() + { + var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(SigningKey); + return Ok(new { keys = new[] { jwk } }); + } + + [HttpPost("login")] + public IActionResult Login([FromForm] string iss, [FromForm] string login_hint, [FromForm] string lti_message_hint) + { + var callbackUrl = $"{iss}/api/lti/authorize?" + + $"client_id={ToolNameId}&" + + $"response_type=id_token&" + + $"redirect_uri=http://localhost:5000/api/mocktool/callback&" + + $"login_hint={login_hint}&" + + $"lti_message_hint={lti_message_hint}&" + + $"scope=openid&state=xyz&nonce={Guid.NewGuid()}"; + + return Redirect(callbackUrl); + } + + [HttpPost("callback")] + public async Task Callback([FromForm] string id_token) + { + var handler = new JwtSecurityTokenHandler(); + if (!handler.CanReadToken(id_token)) return BadRequest("Invalid Token"); + var unverifiedToken = handler.ReadJwtToken(id_token); + + var issuer = unverifiedToken.Issuer; + var platformJwksUrl = $"{issuer}/api/lti/jwks"; + + var client = httpClientFactory.CreateClient(); + string jwksJson; + try { + jwksJson = await client.GetStringAsync(platformJwksUrl); + } catch { + return BadRequest($"Failed to download HwProj keys from {platformJwksUrl}"); + } + + var platformKeySet = new JsonWebKeySet(jwksJson); + + try { + handler.ValidateToken(id_token, new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = issuer, + ValidateAudience = true, + ValidAudience = ToolNameId, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKeys = platformKeySet.Keys + }, out _); + } catch (Exception ex) { + return Unauthorized($"HwProj signature validation error: {ex.Message}"); + } + + var messageType = unverifiedToken.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/message_type")?.Value; + + return messageType switch + { + "LtiDeepLinkingRequest" => RenderDeepLinkingSelectionUI(unverifiedToken), + "LtiResourceLinkRequest" => HandleResourceLink(unverifiedToken), + _ => BadRequest($"Unknown message type: {messageType}") + }; + } + + private IActionResult RenderDeepLinkingSelectionUI(JwtSecurityToken token) + { + var settingsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings"); + if (settingsClaim == null) return BadRequest("No deep linking settings found"); + + var settings = JsonDocument.Parse(settingsClaim.Value); + var returnUrl = settings.RootElement.GetProperty("deep_link_return_url").GetString(); + var dataPayload = settings.RootElement.TryGetProperty("data", out var dataEl) ? dataEl.GetString() : ""; + + var tasksHtml = string.Join("", AvailableTasks.Select(t => $@" +
+ +
")); + + var html = $@" + + +

Select Tasks for HwProj

+
+ + + + {tasksHtml} +
+
+ + "; + + return Content(html, "text/html"); + } + + [HttpPost("submit-selection")] + public IActionResult SubmitDeepLinkingSelection( + [FromForm] List selectedIds, + [FromForm] string returnUrl, + [FromForm] string? data, + [FromForm] string platformIssuer) + { + var selectedTasks = AvailableTasks.Where(t => selectedIds.Contains(t.Id)).ToList(); + + var contentItems = selectedTasks.Select(t => new Dictionary + { + ["type"] = "ltiResourceLink", + ["title"] = t.Title, + ["text"] = t.Description, + ["url"] = $"http://localhost:5000/mock/task/{t.Id}", + + ["lineItem"] = new Dictionary + { + ["scoreMaximum"] = t.Score, + ["label"] = t.Title + }, + + ["custom"] = new Dictionary + { + { "internal_task_id", t.Id } + } + + }).ToList(); + + var payload = new JwtPayload + { + { "iss", ToolIss }, + { "sub", ToolNameId }, + { "aud", platformIssuer }, + { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, + { "exp", DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds() }, + { "nonce", Guid.NewGuid().ToString() }, + { "https://purl.imsglobal.org/spec/lti-dl/claim/message_type", "LtiDeepLinkingResponse" }, + { "https://purl.imsglobal.org/spec/lti-dl/claim/version", "1.3.0" }, + { "https://purl.imsglobal.org/spec/lti-dl/claim/content_items", contentItems } + }; + + if (!string.IsNullOrEmpty(data)) + payload.Add("https://purl.imsglobal.org/spec/lti-dl/claim/data", data); + + var credentials = new SigningCredentials(SigningKey, SecurityAlgorithms.RsaSha256); + var header = new JwtHeader(credentials); + var responseToken = new JwtSecurityToken(header, payload); + var responseString = new JwtSecurityTokenHandler().WriteToken(responseToken); + + var html = $@" + + +
+ +
+ + "; + + return Content(html, "text/html"); + } + + private IActionResult HandleResourceLink(JwtSecurityToken token) + { + var presentationClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/launch_presentation"); + var presentationJson = JsonDocument.Parse(presentationClaim?.Value ?? "{}"); + var returnUrl = presentationJson.RootElement.TryGetProperty("return_url", out var rProp) ? rProp.GetString() : ""; + + var customClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/custom"); + var customJson = JsonDocument.Parse(customClaim?.Value ?? "{}"); + + string toolTaskId = null; + if (customJson.RootElement.TryGetProperty("internal_task_id", out var idProp)) + { + toolTaskId = idProp.GetString(); + } + + if (string.IsNullOrEmpty(toolTaskId)) + { + var resourceLinkClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti/claim/resource_link"); + toolTaskId = JsonDocument.Parse(resourceLinkClaim?.Value ?? "{}").RootElement.GetProperty("id").GetString(); + } + + var currentTask = AvailableTasks.FirstOrDefault(t => t.Id == toolTaskId); + + var scoreToDisplay = currentTask?.Score ?? 0; + var titleToDisplay = currentTask?.Title ?? $"Task ID: {toolTaskId} (Not Found)"; + var descToDisplay = currentTask?.Description ?? "Description not available"; + + var agsClaim = token.Claims.FirstOrDefault(c => c.Type == "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint"); + var lineItemUrl = JsonDocument.Parse(agsClaim?.Value ?? "{}").RootElement.GetProperty("lineitem").GetString(); + + var html = $@" + + +

Performing: {titleToDisplay}

+

{descToDisplay}

+
+ + + + + + + + +
+ + "; + + return Content(html, "text/html"); + } + + [HttpPost("send-score")] + public async Task SendScore( + [FromForm] string lineItemUrl, [FromForm] string userId, + [FromForm] string platformIss, [FromForm] string taskId, [FromForm] string returnUrl) + { + var currentTask = AvailableTasks.FirstOrDefault(t => t.Id == taskId); + + if (currentTask == null) + { + return BadRequest($"Task with internal ID '{taskId}' not found in the tool database. (Check if DeepLinking passed custom params correctly)"); + } + + var client = httpClientFactory.CreateClient(); + var clientAssertion = CreateClientAssertion(platformIss); + + var tokenRequest = new Dictionary { + ["grant_type"] = "client_credentials", + ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + ["client_assertion"] = clientAssertion, + ["scope"] = "https://purl.imsglobal.org/spec/lti-ags/scope/score" + }; + + var tokenResponse = await client.PostAsync($"{platformIss}/api/lti/token", new FormUrlEncodedContent(tokenRequest)); + if (!tokenResponse.IsSuccessStatusCode) return BadRequest($"Error retrieving token from {platformIss}"); + + var tokenContent = await tokenResponse.Content.ReadAsStringAsync(); + var accessToken = JsonDocument.Parse(tokenContent).RootElement.GetProperty("access_token").GetString(); + + var scoreObj = new Score { + UserId = userId, + ScoreGiven = currentTask.Score, + ScoreMaximum = currentTask.Score, + Comment = $"Excellent! Task '{currentTask.Title}' completed.", + GradingProgress = GradingProgress.FullyGraded, + ActivityProgress = ActivityProgress.Completed, + TimeStamp = DateTime.UtcNow + }; + + var scoreRequest = new HttpRequestMessage(HttpMethod.Post, $"{lineItemUrl}/scores") { + Content = new StringContent(JsonSerializer.Serialize(scoreObj), System.Text.Encoding.UTF8, "application/vnd.ims.lti-ags.v1.score+json") + }; + scoreRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + var scoreResponse = await client.SendAsync(scoreRequest); + + var statusColor = scoreResponse.IsSuccessStatusCode ? "green" : "red"; + var statusText = scoreResponse.IsSuccessStatusCode + ? $"Score of {currentTask.Score} successfully submitted!" + : $"Error submitting score: {scoreResponse.StatusCode}"; + + var html = $@" + + + + + + +
+

{statusText}

+

You will be redirected back to HwProj in 3 seconds...

+ Return Now +
+ + "; + + return Content(html, "text/html"); + } + + private static string CreateClientAssertion(string platformIssuer) + { + var claims = new List { + new(JwtRegisteredClaimNames.Iss, ToolIss), + new(JwtRegisteredClaimNames.Sub, ToolNameId), + new(JwtRegisteredClaimNames.Aud, $"{platformIssuer}/api/lti/token"), + new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), + new(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var jwt = new JwtSecurityToken( + header: new JwtHeader(new SigningCredentials(SigningKey, SecurityAlgorithms.RsaSha256)), + payload: new JwtPayload(claims) + ); + + return new JwtSecurityTokenHandler().WriteToken(jwt); + } +} +#endif \ No newline at end of file diff --git a/hwproj.front/src/components/Tasks/LtiImportButton.tsx b/hwproj.front/src/components/Tasks/LtiImportButton.tsx index 18ff8a7bb..784b5452e 100644 --- a/hwproj.front/src/components/Tasks/LtiImportButton.tsx +++ b/hwproj.front/src/components/Tasks/LtiImportButton.tsx @@ -51,10 +51,10 @@ export const LtiImportButton: FC = ({ courseId, toolId, on true ); let dto = response; - console.log(dto); if (response && typeof (response as any).json === 'function') { dto = await (response as any).json(); } + submitLtiForm(dto); } catch (e) { console.error(e); From cb113d1eece106f4baa8cfa50444e6c21ca4c214 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Sat, 21 Mar 2026 13:20:11 +0300 Subject: [PATCH 23/26] refactor: made almost all the necessary edits --- .../Controllers/CoursesController.cs | 2 +- .../Controllers/SolutionsController.cs | 2 +- .../HwProj.APIGateway.API.csproj | 4 - .../LtiPlatformConfig.cs | 2 +- .../LtiToolConfig.cs | 3 +- .../Lti/Controllers/JwksController.cs | 2 +- .../Controllers/LtiAccessTokenController.cs | 4 +- .../LtiAssignmentsGradesControllers.cs | 6 +- .../Lti/Controllers/LtiAuthController.cs | 37 +- .../LtiDeepLinkingReturnController.cs | 7 +- .../Lti/Controllers/LtiToolsController.cs | 10 +- .../Lti/DTOs/AuthorizePostFormDto.cs | 8 + .../Lti/DTOs/LtiToolDto.cs | 9 + .../Lti/Mappings/LtiToolMapper.cs | 19 + .../Lti/Models/AuthorizePostFormDto.cs | 10 - .../Lti/Models/LtiDeepLinkingContentItem.cs | 9 - .../Lti/Models/LtiToolDto.cs | 20 - .../Lti/Services/ILtiTokenService.cs | 4 +- .../Lti/Services/ILtiToolService.cs | 10 +- .../Lti/Services/LtiKeyService.cs | 60 ++- .../Lti/Services/LtiTokenService.cs | 8 +- .../Lti/Services/LtiToolService.cs | 49 +-- .../Models/Solutions/UserTaskSolutions.cs | 2 +- .../HwProj.APIGateway.API/Startup.cs | 3 +- .../ViewModels/CourseViewModels.cs | 8 +- .../Controllers/CoursesController.cs | 2 +- .../Controllers/HomeworksController.cs | 10 +- .../Controllers/TasksController.cs | 9 +- .../Domains/MappingExtensions.cs | 8 +- ...9_RenameAndChangeTypeLtiToolId.Designer.cs | 382 ++++++++++++++++++ ...0320150939_RenameAndChangeTypeLtiToolId.cs | 31 ++ .../Migrations/CourseContextModelSnapshot.cs | 8 +- .../Models/Course.cs | 2 +- .../Models/CourseContext.cs | 6 +- .../Models/CourseTemplate.cs | 2 +- ...LtiUrl.cs => HomeworkTaskLtiLaunchData.cs} | 3 +- .../Repositories/ITasksRepository.cs | 3 +- .../Repositories/TasksRepository.cs | 27 +- .../Services/CoursesService.cs | 47 ++- .../Services/HomeworksService.cs | 2 +- .../Services/ITasksService.cs | 5 +- .../Services/TasksService.cs | 20 +- hwproj.front/src/api/api.ts | 82 ++-- .../src/components/Courses/AddCourseInfo.tsx | 13 +- .../src/components/Courses/Course.tsx | 3 +- .../components/Courses/CourseExperimental.tsx | 6 +- .../src/components/Courses/CreateCourse.tsx | 4 +- .../components/Courses/ICreateCourseState.tsx | 2 +- .../components/Solutions/LtiLaunchButton.tsx | 6 +- .../Solutions/TaskSolutionsPage.tsx | 74 ++-- .../src/components/Tasks/LtiImportButton.tsx | 6 +- 51 files changed, 760 insertions(+), 301 deletions(-) rename HwProj.APIGateway/HwProj.APIGateway.API/Lti/{Models => Configuration}/LtiPlatformConfig.cs (91%) rename HwProj.APIGateway/HwProj.APIGateway.API/Lti/{Models => Configuration}/LtiToolConfig.cs (82%) create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/AuthorizePostFormDto.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/LtiToolDto.cs create mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Mappings/LtiToolMapper.cs delete mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/AuthorizePostFormDto.cs delete mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiDeepLinkingContentItem.cs delete mode 100644 HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.Designer.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.cs rename HwProj.CoursesService/HwProj.CoursesService.API/Models/{HomeworkTaskLtiUrl.cs => HomeworkTaskLtiLaunchData.cs} (86%) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs index f61596906..2ec8caf2e 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs @@ -310,7 +310,7 @@ private async Task ToCourseViewModel(CourseDTO course) Homeworks = course.Homeworks, IsCompleted = course.IsCompleted, IsOpen = course.IsOpen, - LtiToolId = course.LtiToolId, + LtiToolName = course.LtiToolName, }; } } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs index 1f9eb9c4e..66ebce612 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/SolutionsController.cs @@ -139,7 +139,7 @@ public async Task GetStudentSolution(long taskId, string studentI return Ok(new UserTaskSolutionsPageData { CourseId = course.Id, - LtiToolId = course.LtiToolId, + LtiToolName = course.LtiToolName, CourseMates = accounts, TaskSolutions = taskSolutions }); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj index 077296375..94bc3d075 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj +++ b/HwProj.APIGateway/HwProj.APIGateway.API/HwProj.APIGateway.API.csproj @@ -27,8 +27,4 @@ - - - - diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiPlatformConfig.cs similarity index 91% rename from HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs rename to HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiPlatformConfig.cs index c40598a9e..3eb3af1db 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiPlatformConfig.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiPlatformConfig.cs @@ -1,4 +1,4 @@ -namespace HwProj.APIGateway.API.Lti.Models; +namespace HwProj.APIGateway.API.Lti.Configuration; public class LtiPlatformConfig { diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiToolConfig.cs similarity index 82% rename from HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs rename to HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiToolConfig.cs index a18e4f5eb..1be3f2390 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolConfig.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Configuration/LtiToolConfig.cs @@ -1,8 +1,7 @@ -namespace HwProj.APIGateway.API.Lti.Models +namespace HwProj.APIGateway.API.Lti.Configuration { public class LtiToolConfig { - public long Id { get; set; } public string Name { get; set; } public string Issuer { get; set; } public string ClientId { get; set; } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs index 976078f61..cf5685043 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/JwksController.cs @@ -1,5 +1,5 @@ using System.Security.Cryptography; -using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.Configuration; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs index f42dd09ec..848be6640 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAccessTokenController.cs @@ -1,7 +1,7 @@ using System; using System.IdentityModel.Tokens.Jwt; using System.Threading.Tasks; -using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.Configuration; using HwProj.APIGateway.API.Lti.Services; using HwProj.APIGateway.API.LTI.Services; using Microsoft.AspNetCore.Authorization; @@ -51,7 +51,7 @@ public async Task GetTokenAsync([FromForm] IFormCollection form) var clientId = unverifiedToken.Subject; - var tool = await toolService.GetByClientIdAsync(clientId); + var tool = toolService.GetByClientId(clientId); if (tool == null) { return Unauthorized(new { error = "invalid_client", error_description = $"Unknown clientId: {clientId}" }); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAssignmentsGradesControllers.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAssignmentsGradesControllers.cs index 6c577a821..ab6e61295 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAssignmentsGradesControllers.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAssignmentsGradesControllers.cs @@ -39,7 +39,7 @@ public async Task UpdateTaskScore(long taskId, [FromBody] Score s return Unauthorized("Unknown tool client id."); } - var tool = await toolService.GetByClientIdAsync(toolClientId); + var tool = toolService.GetByClientId(toolClientId); if (tool == null) { return BadRequest("Tool not found."); @@ -51,7 +51,7 @@ public async Task UpdateTaskScore(long taskId, [FromBody] Score s return BadRequest("The task does not belong to any course."); } - if (course.LtiToolId != tool.Id) + if (course.LtiToolName != tool.Name) { return BadRequest("This tool does not apply to this course."); } @@ -70,7 +70,7 @@ public async Task UpdateTaskScore(long taskId, [FromBody] Score s { return NotFound(ex.Message); } - catch (Exception ex) + catch (Exception) { return StatusCode(500, "Internal Server Error"); } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs index f8cdb778b..08123878a 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiAuthController.cs @@ -3,7 +3,8 @@ using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; -using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.Configuration; +using HwProj.APIGateway.API.Lti.DTOs; using HwProj.APIGateway.API.Lti.Services; using HwProj.APIGateway.API.LTI.Services; using HwProj.CoursesService.Client; @@ -48,12 +49,12 @@ public async Task AuthorizeLti( return BadRequest("Invalid or expired lti_message_hint"); } - if (payload?.ToolId == null || payload.CourseId == null) + if (payload?.ToolName == null || payload.CourseId == null) { return BadRequest("Invalid or expired lti_message_hint"); } - var tool = await toolService.GetByIdAsync(long.Parse(payload.ToolId)); + var tool = toolService.GetByName(payload.ToolName); if (tool == null) { return BadRequest("Tool not found"); @@ -70,7 +71,7 @@ public async Task AuthorizeLti( return NotFound("Course not found"); } - if (course.LtiToolId != tool.Id) + if (course.LtiToolName != tool.Name) { return BadRequest("The data is incorrect: the id of the instrument linked to the exchange rate does not match"); } @@ -81,7 +82,6 @@ public async Task AuthorizeLti( case "DeepLinking": idToken = tokenService.CreateDeepLinkingToken( clientId: clientId, - toolId: payload.ToolId, courseId: payload.CourseId, targetLinkUri: redirectUri, userId: payload.UserId, @@ -91,7 +91,6 @@ public async Task AuthorizeLti( case "ResourceLink": idToken = tokenService.CreateResourceLinkToken( clientId: clientId, - toolId: payload.ToolId, courseId: payload.CourseId, targetLinkUri: redirectUri, ltiCustomParams: payload.Custom, @@ -123,7 +122,7 @@ public async Task AuthorizeLti( public async Task StartLti( [FromQuery] string? resourceLinkId, [FromQuery] string? courseId, - [FromQuery] string? toolId, + [FromQuery] string? toolName, [FromQuery] string? ltiLaunchUrl, [FromQuery] string? ltiCustomParams, [FromQuery] bool isDeepLink = false) @@ -137,12 +136,12 @@ public async Task StartLti( string targetUrl; LtiHintPayload payload; - if (courseId == null || toolId == null) + if (courseId == null || toolName == null) { return BadRequest("For Deep Linking, courseId and toolId are required."); } - var tool = await toolService.GetByIdAsync(long.Parse(toolId)); + var tool = toolService.GetByName(toolName); if (tool == null) { return NotFound("Tool not found"); @@ -154,7 +153,7 @@ public async Task StartLti( return NotFound("Course not found"); } - if (course.LtiToolId != long.Parse(toolId)) + if (course.LtiToolName != toolName) { return BadRequest("The data is incorrect: the id of the instrument linked to the exchange rate does not match"); } @@ -170,7 +169,7 @@ public async Task StartLti( Type = "DeepLinking", UserId = userId, CourseId = courseId, - ToolId = toolId + ToolName = toolName }; } else if (!string.IsNullOrEmpty(resourceLinkId) && !string.IsNullOrEmpty(ltiLaunchUrl)) @@ -182,7 +181,7 @@ public async Task StartLti( Type = "ResourceLink", UserId = userId, CourseId = courseId, - ToolId = toolId, + ToolName = toolName, ResourceLinkId = resourceLinkId, Custom = ltiCustomParams }; @@ -195,19 +194,17 @@ public async Task StartLti( var json = JsonSerializer.Serialize(payload); var messageHint = this.protector.Protect(json); - var dto = new AuthorizePostFormDto() - { - ActionUrl = tool.InitiateLoginUri, - Method = "POST", - Fields = new Dictionary + var dto = new AuthorizePostFormDto( + tool.InitiateLoginUri, + "POST", + new Dictionary { ["iss"] = ltiPlatformOptions.Value.Issuer, ["login_hint"] = userId, ["target_link_uri"] = targetUrl, ["lti_message_hint"] = messageHint, ["client_id"] = tool.ClientId, - } - }; + }); return Ok(dto); } @@ -256,7 +253,7 @@ private class LtiHintPayload public string UserId { get; set; } public string? ResourceLinkId { get; set; } public string? CourseId { get; set; } - public string? ToolId { get; set; } + public string? ToolName { get; set; } public string? Custom { get; set; } } } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs index 6fdb7dfaf..42e1ede95 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiDeepLinkingReturnController.cs @@ -3,7 +3,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Text.Json; using System.Threading.Tasks; -using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.Configuration; using HwProj.APIGateway.API.Lti.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -39,9 +39,9 @@ public async Task OnDeepLinkingReturnAsync([FromForm] IFormCollec } var unverifiedToken = handler.ReadJwtToken(tokenString); - var clientId = unverifiedToken.Issuer; + var clientId = unverifiedToken.Subject; - var tool = await toolService.GetByClientIdAsync(clientId); + var tool = toolService.GetByClientId(clientId); if (tool == null) { return Unauthorized($"Unknown tool clientId: {clientId}"); @@ -98,6 +98,7 @@ public async Task OnDeepLinkingReturnAsync([FromForm] IFormCollec var responsePayloadJson = JsonSerializer.Serialize(resultList); + // language=html var htmlResponse = $@" diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs index 00f0c6b52..d73563940 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Controllers/LtiToolsController.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Net; using System.Threading.Tasks; -using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.DTOs; using HwProj.APIGateway.API.Lti.Services; using Microsoft.AspNetCore.Mvc; @@ -13,17 +13,17 @@ public class LtiToolsController(ILtiToolService toolService) : ControllerBase { [HttpGet] [ProducesResponseType(typeof(IReadOnlyList), (int)HttpStatusCode.OK)] - public async Task>> GetAll() + public ActionResult> GetAll() { - var tools = await toolService.GetAllAsync(); + var tools = toolService.GetAll(); return Ok(tools); } [HttpGet("{id:long}")] [ProducesResponseType(typeof(LtiToolDto), (int)HttpStatusCode.OK)] - public async Task> Get(long id) + public ActionResult Get(string name) { - var tool = await toolService.GetByIdAsync(id); + var tool = toolService.GetByName(name); if (tool == null) { return NotFound(); diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/AuthorizePostFormDto.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/AuthorizePostFormDto.cs new file mode 100644 index 000000000..738d66eaa --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/AuthorizePostFormDto.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace HwProj.APIGateway.API.Lti.DTOs; + +public record AuthorizePostFormDto( + string ActionUrl, + string Method, + Dictionary Fields); \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/LtiToolDto.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/LtiToolDto.cs new file mode 100644 index 000000000..28815f192 --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/DTOs/LtiToolDto.cs @@ -0,0 +1,9 @@ +namespace HwProj.APIGateway.API.Lti.DTOs; + +public record LtiToolDto( + string Name, + string ClientId, + string JwksEndpoint, + string InitiateLoginUri, + string LaunchUrl, + string DeepLink); \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Mappings/LtiToolMapper.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Mappings/LtiToolMapper.cs new file mode 100644 index 000000000..08f9b329b --- /dev/null +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Mappings/LtiToolMapper.cs @@ -0,0 +1,19 @@ +using HwProj.APIGateway.API.Lti.Configuration; +using HwProj.APIGateway.API.Lti.DTOs; + +namespace HwProj.APIGateway.API.Lti.Mappings; + +public static class LtiToolMapper +{ + public static LtiToolDto LtiToolConfigToDto(this LtiToolConfig t) + { + return new LtiToolDto( + t.Name, + t.ClientId, + t.JwksEndpoint, + t.InitiateLoginUri, + t.LaunchUrl, + t.DeepLink + ); + } +} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/AuthorizePostFormDto.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/AuthorizePostFormDto.cs deleted file mode 100644 index 40cfed0d6..000000000 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/AuthorizePostFormDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace HwProj.APIGateway.API.Lti.Models; - -public class AuthorizePostFormDto -{ - public string ActionUrl { get; set; } - public string Method { get; set; } = "POST"; - public Dictionary Fields { get; set; } = new(); -} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiDeepLinkingContentItem.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiDeepLinkingContentItem.cs deleted file mode 100644 index 44dc95a79..000000000 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiDeepLinkingContentItem.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace HwProj.APIGateway.API.Lti.Models; - -public class LtiDeepLinkingContentItem -{ - public string Type { get; set; } // "ltiResourceLink" - public string Url { get; set; } // Ссылка на запуск (Launch URL) - public string Title { get; set; } // Название задачи - public string Text { get; set; } // Описание -} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs deleted file mode 100644 index f923594fb..000000000 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Models/LtiToolDto.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace HwProj.APIGateway.API.Lti.Models -{ - public class LtiToolDto( - long id, - string name, - string clientId, - string jwksEndpoint, - string initiateLoginUri, - string launchUrl, - string deepLink) - { - public long Id { get; init; } = id; - public string Name { get; init; } = name; - public string ClientId { get; init; } = clientId; - public string JwksEndpoint { get; init; } = jwksEndpoint; - public string InitiateLoginUri { get; init; } = initiateLoginUri; - public string LaunchUrl { get; init; } = launchUrl; - public string DeepLink { get; init; } = deepLink; - } -} \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs index 9864ea51e..1de7f1e0b 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiTokenService.cs @@ -5,9 +5,8 @@ namespace HwProj.APIGateway.API.LTI.Services; public interface ILtiTokenService { - string CreateDeepLinkingToken( + public string CreateDeepLinkingToken( string clientId, - string toolId, string courseId, string targetLinkUri, string userId, @@ -15,7 +14,6 @@ string CreateDeepLinkingToken( public string CreateResourceLinkToken( string clientId, - string toolId, string courseId, string targetLinkUri, string? ltiCustomParams, diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs index 8bb51f48d..a5e79f979 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/ILtiToolService.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; using System.Threading.Tasks; -using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.DTOs; namespace HwProj.APIGateway.API.Lti.Services; public interface ILtiToolService { - Task> GetAllAsync(); - Task GetByIdAsync(long id); - Task GetByIssuerAsync(string issuer); - Task GetByClientIdAsync(string clientId); + IReadOnlyList GetAll(); + LtiToolDto? GetByName(string name); + LtiToolDto? GetByIssuer(string issuer); + LtiToolDto? GetByClientId(string clientId); } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs index f4cb5ebbf..050b0ad39 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiKeyService.cs @@ -1,31 +1,63 @@ +using System; using Microsoft.IdentityModel.Tokens; +using Microsoft.Extensions.Caching.Memory; using System.Net.Http; using System.Threading.Tasks; -using System.Collections.Concurrent; using System.Collections.Generic; using HwProj.APIGateway.API.Lti.Services; -public class LtiKeyService(IHttpClientFactory httpClientFactory) : ILtiKeyService +public class LtiKeyService(IHttpClientFactory httpClientFactory, IMemoryCache keycMemoryCache) : ILtiKeyService { - private static readonly ConcurrentDictionary _keyCache = new(); public async Task?> GetKeysAsync(string jwksUrl) { - if (string.IsNullOrEmpty(jwksUrl)) return null; + if (string.IsNullOrEmpty(jwksUrl)) + { + return null; + } - if (_keyCache.TryGetValue(jwksUrl, out var keySet)) + if (keycMemoryCache.TryGetValue(jwksUrl, out JsonWebKeySet? keySet)) { - return keySet.Keys; + return keySet!.Keys; } - var client = httpClientFactory.CreateClient(); - var json = await client.GetStringAsync(jwksUrl); - keySet = new JsonWebKeySet(json); - - // В продакшене здесь стоит добавить Expire Policy (например, MemoryCache), - // чтобы обновлять ключи раз в сутки. - _keyCache.TryAdd(jwksUrl, keySet); + try + { + var client = httpClientFactory.CreateClient(); + client.Timeout = TimeSpan.FromSeconds(30); + + using var response = await client.GetAsync(jwksUrl); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + keySet = new JsonWebKeySet(json); + + if (response.Headers.CacheControl?.NoCache == true || + response.Headers.CacheControl?.NoStore == true || + response.Headers.CacheControl?.Private == true) + { + return keySet.Keys; + } + + const int ageByDefault = 24; + var cacheDuration = TimeSpan.FromHours(ageByDefault); - return keySet.Keys; + if (response.Headers.CacheControl?.MaxAge.HasValue == true) + { + cacheDuration = response.Headers.CacheControl.MaxAge.Value; + } + + var cacheOptions = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(cacheDuration) + .SetPriority(CacheItemPriority.High); //Не нужно же удалять первым при нехватке памяти? + + keycMemoryCache.Set(jwksUrl, keySet, cacheOptions); + + return keySet.Keys; + } + catch + { + return null; + } } } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs index 3ae9c6f07..e02222cbc 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiTokenService.cs @@ -4,7 +4,7 @@ using System.Security.Claims; using System.Security.Cryptography; using System.Text.Json; -using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.Configuration; using HwProj.APIGateway.API.LTI.Services; using LtiAdvantage.DeepLinking; using LtiAdvantage.Lti; @@ -20,7 +20,6 @@ public class LtiTokenService(IOptions options) : ILtiTokenSer public string CreateDeepLinkingToken( string clientId, - string toolId, string courseId, string targetLinkUri, string userId, @@ -28,7 +27,7 @@ public string CreateDeepLinkingToken( { var request = new LtiDeepLinkingRequest { - DeploymentId = toolId, + DeploymentId = clientId, Nonce = nonce, UserId = userId, TargetLinkUri = targetLinkUri, @@ -55,7 +54,6 @@ public string CreateDeepLinkingToken( public string CreateResourceLinkToken( string clientId, - string toolId, string courseId, string targetLinkUri, string? ltiCustomParams, @@ -65,7 +63,7 @@ public string CreateResourceLinkToken( { var request = new LtiResourceLinkRequest { - DeploymentId = toolId, + DeploymentId = clientId, Nonce = nonce, UserId = userId, TargetLinkUri = targetLinkUri, diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs index 9fc8cc29f..b1c1c8354 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Lti/Services/LtiToolService.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.Configuration; +using HwProj.APIGateway.API.Lti.DTOs; +using HwProj.APIGateway.API.Lti.Mappings; using Microsoft.Extensions.Options; namespace HwProj.APIGateway.API.Lti.Services; @@ -10,45 +12,18 @@ public class LtiToolService(IOptions> options) : ILtiToolSer { private readonly IReadOnlyList _tools = (options.Value ?? []).AsReadOnly(); - public Task> GetAllAsync() - { - var result = _tools - .Select(MapToDto) + public IReadOnlyList GetAll() + => _tools + .Select(LtiToolMapper.LtiToolConfigToDto) .ToList() .AsReadOnly(); - - return Task.FromResult>(result); - } - public Task GetByIdAsync(long id) - { - var cfg = _tools.FirstOrDefault(t => t.Id == id); - return Task.FromResult(cfg == null ? null : MapToDto(cfg)); - } + public LtiToolDto? GetByName(string name) + => _tools.FirstOrDefault(t => t.Name == name)?.LtiToolConfigToDto(); - public Task GetByIssuerAsync(string issuer) - { - // Ищем конфиг, где Issuer совпадает с тем, что пришел в токене - var cfg = _tools.FirstOrDefault(t => t.Issuer == issuer); - return Task.FromResult(cfg == null ? null : MapToDto(cfg)); - } + public LtiToolDto? GetByIssuer(string issuer) + => _tools.FirstOrDefault(t => t.Issuer == issuer)?.LtiToolConfigToDto(); - public Task GetByClientIdAsync(string clientId) - { - var cfg = _tools.FirstOrDefault(t => t.ClientId == clientId); - return Task.FromResult(cfg == null ? null : MapToDto(cfg)); - } - - private static LtiToolDto MapToDto(LtiToolConfig t) - { - return new LtiToolDto( - t.Id, - t.Name, - t.ClientId, - t.JwksEndpoint, - t.InitiateLoginUri, - t.LaunchUrl, - t.DeepLink - ); - } + public LtiToolDto? GetByClientId(string clientId) + => _tools.FirstOrDefault(t => t.ClientId == clientId)?.LtiToolConfigToDto(); } \ No newline at end of file diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Models/Solutions/UserTaskSolutions.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Models/Solutions/UserTaskSolutions.cs index f576438a1..a964901d6 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Models/Solutions/UserTaskSolutions.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Models/Solutions/UserTaskSolutions.cs @@ -32,7 +32,7 @@ public class TaskSolutionStatisticsPageData public class UserTaskSolutionsPageData { public long CourseId { get; set; } - public long? LtiToolId { get; set; } + public string? LtiToolName { get; set; } public AccountDataDto[] CourseMates { get; set; } public HomeworksGroupUserTaskSolutions[] TaskSolutions { get; set; } } diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs index e98ae78c8..5ebde98ae 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs @@ -3,7 +3,7 @@ using System.Text; using System.Text.Json.Serialization; using HwProj.APIGateway.API.Filters; -using HwProj.APIGateway.API.Lti.Models; +using HwProj.APIGateway.API.Lti.Configuration; using HwProj.APIGateway.API.Lti.Services; using HwProj.APIGateway.API.LTI.Services; using HwProj.AuthService.Client; @@ -95,6 +95,7 @@ public void ConfigureServices(IServiceCollection services) }; }); + services.AddMemoryCache(); services.AddHttpClient(); services.AddHttpContextAccessor(); diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs index 106e5150c..7bffc7e7b 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs @@ -18,7 +18,7 @@ public class CreateCourseViewModel public bool FetchStudents { get; set; } [Required] public bool IsOpen { get; set; } public long? BaseCourseId { get; set; } - public long? LtiToolId { get; set; } + public string? LtiToolName { get; set; } } public class UpdateCourseViewModel @@ -32,7 +32,7 @@ public class UpdateCourseViewModel [Required] public bool IsOpen { get; set; } public bool IsCompleted { get; set; } - public long? LtiToolId { get; set; } + public string? LtiToolName { get; set; } } public class CourseDTO : CoursePreview @@ -44,7 +44,7 @@ public class CourseDTO : CoursePreview public GroupViewModel[] Groups { get; set; } = Array.Empty(); public IEnumerable AcceptedStudents => CourseMates.Where(t => t.IsAccepted); public IEnumerable NewStudents => CourseMates.Where(t => !t.IsAccepted); - public long? LtiToolId { get; set; } + public string? LtiToolName { get; set; } } public class CourseViewModel @@ -54,7 +54,7 @@ public class CourseViewModel public string GroupName { get; set; } public bool IsOpen { get; set; } public bool IsCompleted { get; set; } - public long? LtiToolId { get; set; } + public string? LtiToolName { get; set; } public AccountDataDto[] Mentors { get; set; } public AccountDataDto[] AcceptedStudents { get; set; } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs index 64c4b3b2e..fbd288cc2 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CoursesController.cs @@ -127,7 +127,7 @@ public async Task UpdateCourse(long courseId, [FromBody] UpdateCo GroupName = courseViewModel.GroupName, IsCompleted = courseViewModel.IsCompleted, IsOpen = courseViewModel.IsOpen, - LtiToolId = courseViewModel.LtiToolId + LtiToolName = courseViewModel.LtiToolName }); return Ok(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs index 3184367fe..a2d6e20dc 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs @@ -32,7 +32,7 @@ public async Task AddHomework(long courseId, var newHomework = await _homeworksService.AddHomeworkAsync(courseId, homeworkViewModel); var responseViewModel = newHomework.ToHomeworkViewModel(); - await FillLtiUrls(responseViewModel); + await FillLtiLaunchDataForTasks(responseViewModel); return Ok(responseViewModel); } @@ -43,7 +43,7 @@ public async Task GetHomework(long homeworkId) var homeworkFromDb = await _homeworksService.GetHomeworkAsync(homeworkId); var homeworkViewModel = homeworkFromDb.ToHomeworkViewModel(); - await FillLtiUrls(homeworkViewModel); + await FillLtiLaunchDataForTasks(homeworkViewModel); return homeworkViewModel; } @@ -55,7 +55,7 @@ public async Task GetForEditingHomework(long homeworkId) var homeworkFromDb = await _homeworksService.GetForEditingHomeworkAsync(homeworkId); var homeworkViewModel = homeworkFromDb.ToHomeworkViewModel(); - await FillLtiUrls(homeworkViewModel); + await FillLtiLaunchDataForTasks(homeworkViewModel); return homeworkViewModel; } @@ -79,12 +79,12 @@ public async Task UpdateHomework(long homeworkId, var updatedHomework = await _homeworksService.UpdateHomeworkAsync(homeworkId, homeworkViewModel); var responseViewModel = updatedHomework.ToHomeworkViewModel(); - await FillLtiUrls(responseViewModel); + await FillLtiLaunchDataForTasks(responseViewModel); return Ok(responseViewModel); } - private async Task FillLtiUrls(HomeworkViewModel viewModel) + private async Task FillLtiLaunchDataForTasks(HomeworkViewModel viewModel) { if (viewModel.Tasks != null && viewModel.Tasks.Any()) { diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs index 28453142e..607c58816 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/TasksController.cs @@ -45,9 +45,8 @@ public async Task GetTask(long taskId, [FromQuery] bool withCrite if (!lecturers.Contains(userId)) return BadRequest(); } - var ltiLaunchData = await _tasksService.GetTaskLtiDataAsync(taskId); var taskViewModel = task.ToHomeworkTaskViewModel(); - taskViewModel.LtiLaunchData = ltiLaunchData.ToLtiLaunchData(); + await _tasksService.FillTaskViewModelWithLtiLaunchDataAsync(taskViewModel, taskId); return Ok(taskViewModel); } @@ -76,8 +75,7 @@ public async Task AddTask(long homeworkId, [FromBody] PostTaskVie var task = await _tasksService.AddTaskAsync( homeworkId, - taskViewModel, - taskViewModel.LtiLaunchData.ToLtiLaunchData()); + taskViewModel); return Ok(task); } @@ -102,8 +100,7 @@ public async Task UpdateTask(long taskId, [FromBody] PostTaskView var updatedTask = await _tasksService.UpdateTaskAsync(taskId, taskViewModel, - taskViewModel.ActionOptions ?? ActionOptions.Default, - taskViewModel.LtiLaunchData.ToLtiLaunchData()); + taskViewModel.ActionOptions ?? ActionOptions.Default); return Ok(updatedTask.ToHomeworkTaskViewModel()); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs index bf471af84..7cf2de604 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs @@ -103,7 +103,7 @@ public static CourseDTO ToCourseDto(this Course course) InviteCode = course.InviteCode, CourseMates = course.CourseMates.Select(cm => cm.ToCourseMateViewModel()).ToArray(), Homeworks = course.Homeworks.Select(h => h.ToHomeworkViewModel()).ToArray(), - LtiToolId = course.LtiToolId, + LtiToolName = course.LtiToolName, }; public static CoursePreview ToCoursePreview(this Course course) @@ -157,7 +157,7 @@ public static CourseTemplate ToCourseTemplate(this CreateCourseViewModel createC Name = createCourseViewModel.Name, GroupName = string.Join(", ", createCourseViewModel.GroupNames), IsOpen = createCourseViewModel.IsOpen, - LtiToolId = createCourseViewModel.LtiToolId, + LtiToolName = createCourseViewModel.LtiToolName, }; public static CourseTemplate ToCourseTemplate(this Course course) @@ -167,7 +167,7 @@ public static CourseTemplate ToCourseTemplate(this Course course) GroupName = course.GroupName, IsOpen = course.IsOpen, Homeworks = course.Homeworks.Select(h => h.ToHomeworkTemplate()).ToList(), - LtiToolId = course.LtiToolId, + LtiToolName = course.LtiToolName, }; public static HomeworkTemplate ToHomeworkTemplate(this Homework homework) @@ -200,7 +200,7 @@ public static Course ToCourse(this CourseTemplate courseTemplate) Name = courseTemplate.Name, GroupName = courseTemplate.GroupName, IsOpen = courseTemplate.IsOpen, - LtiToolId = courseTemplate.LtiToolId, + LtiToolName = courseTemplate.LtiToolName, }; public static Homework ToHomework(this HomeworkTemplate homeworkTemplate, long courseId) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.Designer.cs new file mode 100644 index 000000000..044ee315c --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.Designer.cs @@ -0,0 +1,382 @@ +// +using System; +using HwProj.CoursesService.API.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace HwProj.CoursesService.API.Migrations +{ + [DbContext(typeof(CourseContext))] + [Migration("20260320150939_RenameAndChangeTypeLtiToolId")] + partial class RenameAndChangeTypeLtiToolId + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("MentorId"); + + b.Property("StudentId"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Assignments"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupName"); + + b.Property("InviteCode"); + + b.Property("IsCompleted"); + + b.Property("IsOpen"); + + b.Property("LtiToolName"); + + b.Property("MentorIds"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("FilterJson"); + + b.HasKey("Id"); + + b.ToTable("CourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("IsAccepted"); + + b.Property("StudentId"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("CourseMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("MaxPoints"); + + b.Property("Name"); + + b.Property("TaskId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Criteria"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupId"); + + b.Property("StudentId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasAlternateKey("GroupId", "StudentId"); + + b.ToTable("GroupMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CourseId"); + + b.Property("DeadlineDate"); + + b.Property("Description"); + + b.Property("HasDeadline"); + + b.Property("IsDeadlineStrict"); + + b.Property("PublicationDate"); + + b.Property("Tags"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("DeadlineDate"); + + b.Property("Description"); + + b.Property("HasDeadline"); + + b.Property("HomeworkId"); + + b.Property("IsBonusExplicit"); + + b.Property("IsDeadlineStrict"); + + b.Property("MaxRating"); + + b.Property("PublicationDate"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("HomeworkId"); + + b.ToTable("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => + { + b.Property("TaskId"); + + b.Property("CustomParams"); + + b.Property("LtiLaunchUrl") + .IsRequired(); + + b.HasKey("TaskId"); + + b.ToTable("TaskLtiData"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.Property("CourseMateId"); + + b.Property("Description"); + + b.Property("Tags"); + + b.HasKey("CourseMateId"); + + b.ToTable("StudentCharacteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("GroupId"); + + b.Property("TaskId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("TasksModels"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Answer") + .HasMaxLength(1000); + + b.Property("IsPrivate"); + + b.Property("LecturerId"); + + b.Property("StudentId"); + + b.Property("TaskId"); + + b.Property("Text") + .HasMaxLength(1000); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Questions"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.Property("CourseId"); + + b.Property("UserId"); + + b.Property("CourseFilterId"); + + b.HasKey("CourseId", "UserId"); + + b.HasIndex("CourseFilterId"); + + b.ToTable("UserToCourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("Assignments") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("CourseMates") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "Task") + .WithMany("Criteria") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group") + .WithMany("GroupMates") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course") + .WithMany("Homeworks") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") + .WithMany("Tasks") + .HasForeignKey("HomeworkId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") + .WithOne() + .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", "TaskId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseMate") + .WithOne("Characteristics") + .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group") + .WithMany("Tasks") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") + .WithMany() + .HasForeignKey("CourseFilterId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.cs new file mode 100644 index 000000000..85ae00f61 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace HwProj.CoursesService.API.Migrations +{ + public partial class RenameAndChangeTypeLtiToolId : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LtiToolId", + table: "Courses"); + + migrationBuilder.AddColumn( + name: "LtiToolName", + table: "Courses", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LtiToolName", + table: "Courses"); + + migrationBuilder.AddColumn( + name: "LtiToolId", + table: "Courses", + nullable: true); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs index 808452a61..ee14be8a0 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -52,7 +52,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsOpen"); - b.Property("LtiToolId"); + b.Property("LtiToolName"); b.Property("MentorIds"); @@ -209,7 +209,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Tasks"); }); - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => { b.Property("TaskId"); @@ -343,11 +343,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => { b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") .WithOne() - .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", "TaskId") + .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", "TaskId") .OnDelete(DeleteBehavior.Cascade); }); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs index 435d84de1..1ea32aa29 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Course.cs @@ -16,6 +16,6 @@ public class Course : IEntity public List CourseMates { get; set; } = new List(); public List Homeworks { get; set; } = new List(); public List Assignments { get; set; } = new List(); - public long? LtiToolId { get; set; } + public string? LtiToolName { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs index d5698a019..eb5a2df36 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs @@ -16,7 +16,7 @@ public sealed class CourseContext : DbContext public DbSet UserToCourseFilters { get; set; } public DbSet Questions { get; set; } public DbSet Criteria { get; set; } - public DbSet TaskLtiData { get; set; } + public DbSet TaskLtiData { get; set; } public CourseContext(DbContextOptions options) : base(options) @@ -29,10 +29,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasIndex(a => a.CourseId); modelBuilder.Entity().HasKey(u => new { u.CourseId, u.UserId }); modelBuilder.Entity().HasIndex(t => t.TaskId); - modelBuilder.Entity() + modelBuilder.Entity() .HasOne() .WithOne() - .HasForeignKey(u => u.TaskId) + .HasForeignKey(u => u.TaskId) .OnDelete(DeleteBehavior.Cascade); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs index 09b225007..afcdd5358 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseTemplate.cs @@ -11,6 +11,6 @@ public class CourseTemplate public bool IsOpen { get; set; } public List Homeworks { get; set; } = new List(); - public long? LtiToolId { get; set; } + public string? LtiToolName { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiLaunchData.cs similarity index 86% rename from HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs rename to HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiLaunchData.cs index a89a91f24..2edd3c907 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiUrl.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiLaunchData.cs @@ -3,7 +3,7 @@ namespace HwProj.CoursesService.API.Models { - public class HomeworkTaskLtiUrl + public class HomeworkTaskLtiLaunchData { [Key] [DatabaseGenerated(DatabaseGeneratedOption.None)] @@ -12,6 +12,7 @@ public class HomeworkTaskLtiUrl [Required] public string LtiLaunchUrl { get; set; } + /// JSON public string? CustomParams { get; set; } } } \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs index 8f8277b73..de41befe9 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/ITasksRepository.cs @@ -9,7 +9,8 @@ namespace HwProj.CoursesService.API.Repositories { public interface ITasksRepository : ICrudRepository { - Task AddLtiUrlAsync(long taskId, LtiLaunchData ltiLaunchData); + Task AddOrUpdateLtiLaunchDataAsync(long taskId, LtiLaunchData ltiLaunchData); + Task AddRangeLtiLaunchDataAsync(IEnumerable ltiLaunchData); Task GetLtiDataAsync(long taskId); Task> GetLtiDataForTasksAsync(IEnumerable taskIds); Task GetWithHomeworkAsync(long id); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs index ecbdc435e..b2ea8a2e5 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs @@ -54,33 +54,46 @@ public Task GetWithHomeworkAsync(long id) .FirstOrDefaultAsync(x => x.Id == id); } - public async Task AddLtiUrlAsync(long taskId, LtiLaunchData ltiLaunchData) + public async Task AddOrUpdateLtiLaunchDataAsync(long taskId, LtiLaunchData ltiLaunchData) { - var existingRecord = await Context.Set().FindAsync(taskId); + var existingRecord = await Context.Set().FindAsync(taskId); if (existingRecord != null) { existingRecord.LtiLaunchUrl = ltiLaunchData.LtiLaunchUrl; existingRecord.CustomParams = ltiLaunchData.CustomParams; - Context.Set().Update(existingRecord); + Context.Set().Update(existingRecord); } else { - var ltiRecord = new HomeworkTaskLtiUrl + var ltiRecord = new HomeworkTaskLtiLaunchData { TaskId = taskId, LtiLaunchUrl = ltiLaunchData.LtiLaunchUrl, CustomParams = ltiLaunchData.CustomParams }; - await Context.Set().AddAsync(ltiRecord); + await Context.Set().AddAsync(ltiRecord); } await Context.SaveChangesAsync(); } + public async Task AddRangeLtiLaunchDataAsync(IEnumerable ltiLaunchData) + { + var ltiLaunchDataList = ltiLaunchData as HomeworkTaskLtiLaunchData[] ?? ltiLaunchData.ToArray(); + if (!ltiLaunchDataList.Any()) + { + return; + } + + await Context.Set().AddRangeAsync(ltiLaunchDataList); + + await Context.SaveChangesAsync(); + } + public async Task GetLtiDataAsync(long taskId) { - var record = await Context.Set().FindAsync(taskId); + var record = await Context.Set().FindAsync(taskId); return record == null ? null : new LtiLaunchData { @@ -91,7 +104,7 @@ public async Task AddLtiUrlAsync(long taskId, LtiLaunchData ltiLaunchData) public async Task> GetLtiDataForTasksAsync(IEnumerable taskIds) { - return await Context.Set() + return await Context.Set() .Where(t => taskIds.Contains(t.TaskId)) .ToDictionaryAsync(t => t.TaskId, t => new LtiLaunchData { diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs index 50f4893de..2f1b37036 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -78,7 +78,7 @@ public async Task GetAllAsync() var groups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(course.Id).ToArrayAsync(); var courseDto = course.ToCourseDto(); - await FillLtiDataForCourseDtos(new[] { courseDto }); + await FillNecessaryLtiDataForCourseDtos(courseDto); courseDto.Groups = groups.Select(g => new GroupViewModel @@ -112,15 +112,24 @@ public async Task AddAsync(CreateCourseViewModel courseViewModel, string m } courseTemplate.Homeworks ??= new List(); - if (baseCourse != null) + if (baseCourse?.LtiToolName != null) { + var allTaskIds = baseCourse.Homeworks + .SelectMany(h => h.Tasks.Select(t => t.Id)); + + var ltiDataDict = await _tasksRepository.GetLtiDataForTasksAsync(allTaskIds); + foreach (var homework in baseCourse.Homeworks) { var homeworkTemplate = homework.ToHomeworkTemplate(); + + // Здесь опираемся на порядок for (var i = 0; i < homeworkTemplate.Tasks.Count; i++) { - homeworkTemplate.Tasks[i].LtiLaunchData = - await _tasksRepository.GetLtiDataAsync(homework.Tasks[i].Id); + if (ltiDataDict.TryGetValue(homework.Tasks[i].Id, out var ltiData)) + { + homeworkTemplate.Tasks[i].LtiLaunchData = ltiData; + } } courseTemplate.Homeworks.Add(homeworkTemplate); @@ -169,7 +178,7 @@ private async Task AddFromTemplateAsync(CourseTemplate courseTemplate, L course.MentorIds = mentorId; course.InviteCode = Guid.NewGuid().ToString(); var courseId = await _coursesRepository.AddAsync(course); - course.LtiToolId = courseTemplate.LtiToolId; + course.LtiToolName = courseTemplate.LtiToolName; var homeworks = courseTemplate.Homeworks.Select(hwTemplate => hwTemplate.ToHomework(courseId)); var homeworkIds = await _homeworksRepository.AddRangeAsync(homeworks); @@ -186,14 +195,26 @@ private async Task AddFromTemplateAsync(CourseTemplate courseTemplate, L var tasksToSave = taskPairs.Select(x => x.NewEntity); await _tasksRepository.AddRangeAsync(tasksToSave); + var ltiDataToSave = new List(); + foreach (var pair in taskPairs) { if (pair.Template.LtiLaunchData != null) { - await _tasksRepository.AddLtiUrlAsync(pair.NewEntity.Id, pair.Template.LtiLaunchData); + ltiDataToSave.Add(new HomeworkTaskLtiLaunchData + { + TaskId = pair.NewEntity.Id, + LtiLaunchUrl = pair.Template.LtiLaunchData.LtiLaunchUrl, + CustomParams = pair.Template.LtiLaunchData.CustomParams + }); } } + if (ltiDataToSave.Any()) + { + await _tasksRepository.AddRangeLtiLaunchDataAsync(ltiDataToSave); + } + if (studentIds.Any()) { var students = studentIds.Select(studentId => new CourseMate @@ -236,7 +257,7 @@ public async Task UpdateAsync(long courseId, Course updated) GroupName = updated.GroupName, IsCompleted = updated.IsCompleted, IsOpen = updated.IsOpen, - LtiToolId = updated.LtiToolId, + LtiToolName = updated.LtiToolName, }); } @@ -327,7 +348,7 @@ public async Task GetUserCoursesAsync(string userId, string role) var result = await _courseFilterService.ApplyFiltersToCourses( userId, coursesWithValues.Select(c => c.ToCourseDto()).ToArray()); - await FillLtiDataForCourseDtos(result); + await FillNecessaryLtiDataForCourseDtos(result); if (role == Roles.ExpertRole) { @@ -409,9 +430,15 @@ await _courseMatesRepository.FindAll(x => x.CourseId == courseId && x.StudentId return true; } - private async Task FillLtiDataForCourseDtos(IEnumerable courses) + private async Task FillNecessaryLtiDataForCourseDtos(params CourseDTO[] courses) { - var allTasks = courses.SelectMany(c => c.Homeworks).SelectMany(h => h.Tasks).ToList(); + var ltiCourses = courses.Where(c => c.LtiToolName != null).ToArray(); + if (!ltiCourses.Any()) + { + return; + } + + var allTasks = ltiCourses.SelectMany(c => c.Homeworks).SelectMany(h => h.Tasks).ToList(); if (allTasks.Any()) { diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 7c89caaf0..afda302aa 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -56,7 +56,7 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo continue; } - await _tasksRepository.AddLtiUrlAsync(createdTasks[i].Id, ltiLaunchData); + await _tasksRepository.AddOrUpdateLtiLaunchDataAsync(createdTasks[i].Id, ltiLaunchData); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs index a634a2d04..f41119934 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ITasksService.cs @@ -12,8 +12,9 @@ public interface ITasksService Task GetForEditingTaskAsync(long taskId); Task GetTaskLtiDataAsync(long taskId); Task> GetLtiDataForTasksAsync(long[] taskIds); - Task AddTaskAsync(long homeworkId, PostTaskViewModel taskViewModel, LtiLaunchData? ltiLaunchData = null); + Task AddTaskAsync(long homeworkId, PostTaskViewModel taskViewModel); Task DeleteTaskAsync(long taskId); - Task UpdateTaskAsync(long taskId, PostTaskViewModel taskViewModel, ActionOptions options, LtiLaunchData? ltiLaunchData = null); + Task UpdateTaskAsync(long taskId, PostTaskViewModel taskViewModel, ActionOptions options); + Task FillTaskViewModelWithLtiLaunchDataAsync(HomeworkTaskViewModel taskViewModel, long taskId); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs index 4196a64c6..3694aa0ba 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/TasksService.cs @@ -46,8 +46,7 @@ public async Task GetForEditingTaskAsync(long taskId) public async Task AddTaskAsync( long homeworkId, - PostTaskViewModel taskViewModel, - LtiLaunchData? ltiLaunchData = null) + PostTaskViewModel taskViewModel) { var task = taskViewModel.ToHomeworkTask(); task.HomeworkId = homeworkId; @@ -57,9 +56,9 @@ public async Task AddTaskAsync( var taskId = await _tasksRepository.AddAsync(task); - if (ltiLaunchData != null && !string.IsNullOrEmpty(ltiLaunchData.LtiLaunchUrl)) + if (taskViewModel.LtiLaunchData != null && !string.IsNullOrEmpty(taskViewModel.LtiLaunchData.LtiLaunchUrl)) { - await _tasksRepository.AddLtiUrlAsync(taskId, ltiLaunchData); + await _tasksRepository.AddOrUpdateLtiLaunchDataAsync(taskId, taskViewModel.LtiLaunchData.ToLtiLaunchData()!); } var deadlineDate = task.DeadlineDate ?? homework.DeadlineDate; @@ -80,8 +79,7 @@ public async Task DeleteTaskAsync(long taskId) public async Task UpdateTaskAsync( long taskId, PostTaskViewModel taskViewModel, - ActionOptions options, - LtiLaunchData? ltiLaunchData = null) + ActionOptions options) { var update = taskViewModel.ToHomeworkTask(); var task = await _tasksRepository.GetWithHomeworkAsync(taskId); @@ -107,14 +105,20 @@ public async Task UpdateTaskAsync( IsBonusExplicit = update.IsBonusExplicit, }, update.Criteria); - if (ltiLaunchData != null && !string.IsNullOrEmpty(ltiLaunchData.LtiLaunchUrl)) + if (taskViewModel.LtiLaunchData != null && !string.IsNullOrEmpty(taskViewModel.LtiLaunchData.LtiLaunchUrl)) { - await _tasksRepository.AddLtiUrlAsync(taskId, ltiLaunchData); + await _tasksRepository.AddOrUpdateLtiLaunchDataAsync(taskId, taskViewModel.LtiLaunchData.ToLtiLaunchData()!); } return await GetTaskAsync(taskId, true); } + public async Task FillTaskViewModelWithLtiLaunchDataAsync(HomeworkTaskViewModel taskViewModel, long taskId) + { + var ltiLaunchData = await this.GetTaskLtiDataAsync(taskId); + taskViewModel.LtiLaunchData = ltiLaunchData.ToLtiLaunchData(); + } + public async Task GetTaskLtiDataAsync(long taskId) { return await _tasksRepository.GetLtiDataAsync(taskId); diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index f5e5eb380..7860890ae 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -492,10 +492,10 @@ export interface CourseViewModel { isCompleted?: boolean; /** * - * @type {number} + * @type {string} * @memberof CourseViewModel */ - ltiToolId?: number; + ltiToolName?: string; /** * * @type {Array} @@ -565,10 +565,10 @@ export interface CreateCourseViewModel { baseCourseId?: number; /** * - * @type {number} + * @type {string} * @memberof CreateCourseViewModel */ - ltiToolId?: number; + ltiToolName?: string; } /** * @@ -1574,12 +1574,6 @@ export interface LtiTokenBody { * @interface LtiToolDto */ export interface LtiToolDto { - /** - * - * @type {number} - * @memberof LtiToolDto - */ - id?: number; /** * * @type {string} @@ -3049,10 +3043,10 @@ export interface UpdateCourseViewModel { isCompleted?: boolean; /** * - * @type {number} + * @type {string} * @memberof UpdateCourseViewModel */ - ltiToolId?: number; + ltiToolName?: string; } /** * @@ -3212,10 +3206,10 @@ export interface UserTaskSolutionsPageData { courseId?: number; /** * - * @type {number} + * @type {string} * @memberof UserTaskSolutionsPageData */ - ltiToolId?: number; + ltiToolName?: string; /** * * @type {Array} @@ -7953,7 +7947,7 @@ export const LtiAssignmentsGradesControllersApiFetchParamCreator = function (con if (taskId === null || taskId === undefined) { throw new RequiredError('taskId','Required parameter taskId was null or undefined when calling ltiAssignmentsGradesControllersUpdateTaskScore.'); } - const localVarPath = `/api/lti/lineItem/{taskId}` + const localVarPath = `/api/lti/lineItem/{taskId}/scores` .replace(`{${"taskId"}}`, encodeURIComponent(String(taskId))); const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'POST' }, options); @@ -7968,7 +7962,7 @@ export const LtiAssignmentsGradesControllersApiFetchParamCreator = function (con localVarHeaderParameter["Authorization"] = localVarApiKeyValue; } - localVarHeaderParameter['Content-Type'] = 'application/vnd.ims.lti-ags.v1.score+json'; + localVarHeaderParameter['Content-Type'] = 'application/json'; localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 @@ -8147,14 +8141,14 @@ export const LtiAuthApiFetchParamCreator = function (configuration?: Configurati * * @param {string} [resourceLinkId] * @param {string} [courseId] - * @param {string} [toolId] + * @param {string} [toolName] * @param {string} [ltiLaunchUrl] * @param {string} [ltiCustomParams] * @param {boolean} [isDeepLink] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options: any = {}): FetchArgs { + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolName?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options: any = {}): FetchArgs { const localVarPath = `/api/lti/start`; const localVarUrlObj = url.parse(localVarPath, true); const localVarRequestOptions = Object.assign({ method: 'GET' }, options); @@ -8177,8 +8171,8 @@ export const LtiAuthApiFetchParamCreator = function (configuration?: Configurati localVarQueryParameter['courseId'] = courseId; } - if (toolId !== undefined) { - localVarQueryParameter['toolId'] = toolId; + if (toolName !== undefined) { + localVarQueryParameter['toolName'] = toolName; } if (ltiLaunchUrl !== undefined) { @@ -8255,15 +8249,15 @@ export const LtiAuthApiFp = function(configuration?: Configuration) { * * @param {string} [resourceLinkId] * @param {string} [courseId] - * @param {string} [toolId] + * @param {string} [toolName] * @param {string} [ltiLaunchUrl] * @param {string} [ltiCustomParams] * @param {boolean} [isDeepLink] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, ltiLaunchUrl, ltiCustomParams, isDeepLink, options); + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolName?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiAuthApiFetchParamCreator(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolName, ltiLaunchUrl, ltiCustomParams, isDeepLink, 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) { @@ -8308,15 +8302,15 @@ export const LtiAuthApiFactory = function (configuration?: Configuration, fetch? * * @param {string} [resourceLinkId] * @param {string} [courseId] - * @param {string} [toolId] + * @param {string} [toolName] * @param {string} [ltiLaunchUrl] * @param {string} [ltiCustomParams] * @param {boolean} [isDeepLink] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any) { - return LtiAuthApiFp(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, ltiLaunchUrl, ltiCustomParams, isDeepLink, options)(fetch, basePath); + ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolName?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any) { + return LtiAuthApiFp(configuration).ltiAuthStartLti(resourceLinkId, courseId, toolName, ltiLaunchUrl, ltiCustomParams, isDeepLink, options)(fetch, basePath); }, }; }; @@ -8357,7 +8351,7 @@ export class LtiAuthApi extends BaseAPI { * * @param {string} [resourceLinkId] * @param {string} [courseId] - * @param {string} [toolId] + * @param {string} [toolName] * @param {string} [ltiLaunchUrl] * @param {string} [ltiCustomParams] * @param {boolean} [isDeepLink] @@ -8365,8 +8359,8 @@ export class LtiAuthApi extends BaseAPI { * @throws {RequiredError} * @memberof LtiAuthApi */ - public ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolId?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any) { - return LtiAuthApiFp(this.configuration).ltiAuthStartLti(resourceLinkId, courseId, toolId, ltiLaunchUrl, ltiCustomParams, isDeepLink, options)(this.fetch, this.basePath); + public ltiAuthStartLti(resourceLinkId?: string, courseId?: string, toolName?: string, ltiLaunchUrl?: string, ltiCustomParams?: string, isDeepLink?: boolean, options?: any) { + return LtiAuthApiFp(this.configuration).ltiAuthStartLti(resourceLinkId, courseId, toolName, ltiLaunchUrl, ltiCustomParams, isDeepLink, options)(this.fetch, this.basePath); } } @@ -8492,11 +8486,12 @@ export const LtiToolsApiFetchParamCreator = function (configuration?: Configurat return { /** * - * @param {number} id + * @param {string} id + * @param {string} [name] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiToolsGet(id: number, options: any = {}): FetchArgs { + ltiToolsGet(id: string, name?: string, options: any = {}): FetchArgs { // verify required parameter 'id' is not null or undefined if (id === null || id === undefined) { throw new RequiredError('id','Required parameter id was null or undefined when calling ltiToolsGet.'); @@ -8516,6 +8511,10 @@ export const LtiToolsApiFetchParamCreator = function (configuration?: Configurat localVarHeaderParameter["Authorization"] = localVarApiKeyValue; } + if (name !== undefined) { + localVarQueryParameter['name'] = name; + } + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 localVarUrlObj.search = null; @@ -8567,12 +8566,13 @@ export const LtiToolsApiFp = function(configuration?: Configuration) { return { /** * - * @param {number} id + * @param {string} id + * @param {string} [name] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiToolsGet(id: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = LtiToolsApiFetchParamCreator(configuration).ltiToolsGet(id, options); + ltiToolsGet(id: string, name?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { + const localVarFetchArgs = LtiToolsApiFetchParamCreator(configuration).ltiToolsGet(id, name, 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) { @@ -8611,12 +8611,13 @@ export const LtiToolsApiFactory = function (configuration?: Configuration, fetch return { /** * - * @param {number} id + * @param {string} id + * @param {string} [name] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - ltiToolsGet(id: number, options?: any) { - return LtiToolsApiFp(configuration).ltiToolsGet(id, options)(fetch, basePath); + ltiToolsGet(id: string, name?: string, options?: any) { + return LtiToolsApiFp(configuration).ltiToolsGet(id, name, options)(fetch, basePath); }, /** * @@ -8638,13 +8639,14 @@ export const LtiToolsApiFactory = function (configuration?: Configuration, fetch export class LtiToolsApi extends BaseAPI { /** * - * @param {number} id + * @param {string} id + * @param {string} [name] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof LtiToolsApi */ - public ltiToolsGet(id: number, options?: any) { - return LtiToolsApiFp(this.configuration).ltiToolsGet(id, options)(this.fetch, this.basePath); + public ltiToolsGet(id: string, name?: string, options?: any) { + return LtiToolsApiFp(this.configuration).ltiToolsGet(id, name, options)(this.fetch, this.basePath); } /** diff --git a/hwproj.front/src/components/Courses/AddCourseInfo.tsx b/hwproj.front/src/components/Courses/AddCourseInfo.tsx index 2ee44c720..5dade4ab8 100644 --- a/hwproj.front/src/components/Courses/AddCourseInfo.tsx +++ b/hwproj.front/src/components/Courses/AddCourseInfo.tsx @@ -115,17 +115,14 @@ const AddCourseInfo: FC = ({state, setState}) => { option.name || "Без названия"} - // Текущее значение. Ищем объект в массиве по ID. value={ - state.ltiToolId - ? state.ltiTools?.find(t => t.id === state.ltiToolId) || null - : null + state.ltiToolName + ? state.ltiTools?.find(t => t.name === state.ltiToolName) || undefined + : undefined } // Обработчик изменения @@ -133,7 +130,7 @@ const AddCourseInfo: FC = ({state, setState}) => { setState(prev => ({ ...prev, // Если выбрали (newValue не null), берем ID. Иначе undefined. - ltiToolId: newValue ? newValue.id : undefined + ltiToolName: newValue ? newValue.name : undefined })); }} diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index e7040c021..8eb763570 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -118,7 +118,6 @@ const Course: React.FC = () => { const setCurrentState = async () => { const course = await ApiSingleton.coursesApi.coursesGetCourseData(+courseId!) - console.log(course) // У пользователя изменилась роль (иначе он не может стать лектором в курсе), // однако он все ещё использует токен с прежней ролью @@ -329,7 +328,7 @@ const Course: React.FC = () => { {tabValue === "homeworks" && = (props) => { + Добавить задачу - {props.ltiToolId && ( + {props.ltiToolName && (
handleLtiImport(items, x)} />
diff --git a/hwproj.front/src/components/Courses/CreateCourse.tsx b/hwproj.front/src/components/Courses/CreateCourse.tsx index 1f1612156..82cd08916 100644 --- a/hwproj.front/src/components/Courses/CreateCourse.tsx +++ b/hwproj.front/src/components/Courses/CreateCourse.tsx @@ -57,7 +57,7 @@ export const CreateCourse: FC = () => { fetchingGroups: false, courseIsLoading: false, ltiTools: [], - ltiToolId: undefined, + ltiToolName: undefined, }) const {activeStep, completedSteps, baseCourses, selectedBaseCourse} = state @@ -136,7 +136,7 @@ export const CreateCourse: FC = () => { isOpen: true, baseCourseId: selectedBaseCourse?.id, fetchStudents: state.isGroupFromList ? state.fetchStudents : false, - ltiToolId: state.ltiToolId, + ltiToolName: state.ltiToolName, } try { setCourseIsLoading(true) diff --git a/hwproj.front/src/components/Courses/ICreateCourseState.tsx b/hwproj.front/src/components/Courses/ICreateCourseState.tsx index c9af4cc2a..dd9662692 100644 --- a/hwproj.front/src/components/Courses/ICreateCourseState.tsx +++ b/hwproj.front/src/components/Courses/ICreateCourseState.tsx @@ -35,7 +35,7 @@ export interface ICreateCourseState { courseIsLoading: boolean; ltiTools: LtiToolDto[]; - ltiToolId: number | undefined; + ltiToolName: string | undefined; } export interface IStepComponentProps { diff --git a/hwproj.front/src/components/Solutions/LtiLaunchButton.tsx b/hwproj.front/src/components/Solutions/LtiLaunchButton.tsx index 177378ac4..c7dbf6984 100644 --- a/hwproj.front/src/components/Solutions/LtiLaunchButton.tsx +++ b/hwproj.front/src/components/Solutions/LtiLaunchButton.tsx @@ -7,12 +7,12 @@ import {LtiLaunchData} from "@/api"; interface LtiLaunchButtonProps { courseId: number; - toolId: number; + toolName: string; taskId: number; ltiLaunchData: LtiLaunchData; } -export const LtiLaunchButton: FC = ({ courseId, toolId, taskId, ltiLaunchData }) => { +export const LtiLaunchButton: FC = ({ courseId, toolName, taskId, ltiLaunchData }) => { const [isLoading, setIsLoading] = useState(false); const [openDialog, setOpenDialog] = useState(false); @@ -46,7 +46,7 @@ export const LtiLaunchButton: FC = ({ courseId, toolId, ta const response = await ApiSingleton.ltiAuthApi.ltiAuthStartLti( String(taskId), String(courseId), - String(toolId), + toolName, ltiLaunchData.ltiLaunchUrl, ltiLaunchData.customParams, false diff --git a/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx b/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx index 283b73839..c33a80940 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionsPage.tsx @@ -25,7 +25,7 @@ interface ITaskSolutionsState { isLoaded: boolean addSolution: boolean courseId: number - ltiToolId: number + ltiToolName: string homeworkGroupedSolutions: HomeworksGroupUserTaskSolutions[] courseMates: AccountDataDto[] } @@ -52,7 +52,7 @@ const TaskSolutionsPage: FC = () => { const [taskSolutionPage, setTaskSolutionPage] = useState({ isLoaded: false, courseId: 0, - ltiToolId: 0, + ltiToolName: "", addSolution: false, homeworkGroupedSolutions: [], courseMates: [] @@ -101,13 +101,13 @@ const TaskSolutionsPage: FC = () => { isLoaded: true, addSolution: false, courseId: pageData.courseId!, - ltiToolId: pageData.ltiToolId!, + ltiToolName: pageData.ltiToolName!, homeworkGroupedSolutions: pageData.taskSolutions!, courseMates: pageData.courseMates!, }) } - const {homeworkGroupedSolutions, courseId, courseMates, ltiToolId} = taskSolutionPage + const {homeworkGroupedSolutions, courseId, courseMates, ltiToolName} = taskSolutionPage const student = courseMates.find(x => x.userId === userId)! useEffect(() => { @@ -181,6 +181,43 @@ const TaskSolutionsPage: FC = () => { })) } + const renderSolutionButton = () => { + if (task.ltiLaunchData) { + return ( + + ) + } + + if (task.canSendSolution) { + return ( + + ); + } + + return null + } + const renderRatingChip = (solutionsDescription: string, color: string, lastRatedSolution: Solution) => { return {solutionsDescription}}> @@ -233,34 +270,7 @@ const TaskSolutionsPage: FC = () => {
- {task.ltiLaunchData ? ( - - ) : ( - task.canSendSolution && ( - - ) - )} + {renderSolutionButton()}
diff --git a/hwproj.front/src/components/Tasks/LtiImportButton.tsx b/hwproj.front/src/components/Tasks/LtiImportButton.tsx index 784b5452e..98aba6725 100644 --- a/hwproj.front/src/components/Tasks/LtiImportButton.tsx +++ b/hwproj.front/src/components/Tasks/LtiImportButton.tsx @@ -13,11 +13,11 @@ export interface LtiItemDto { interface LtiImportButtonProps { courseId: number; - toolId: number; + toolName: string; onImport: (items: LtiItemDto[]) => void; } -export const LtiImportButton: FC = ({ courseId, toolId, onImport }) => { +export const LtiImportButton: FC = ({ courseId, toolName, onImport }) => { const submitLtiForm = (formData: any) => { const windowName = "lti_tab_" + new Date().getTime(); window.open('about:blank', windowName); @@ -45,7 +45,7 @@ export const LtiImportButton: FC = ({ courseId, toolId, on try { const response = await ApiSingleton.ltiAuthApi.ltiAuthStartLti( undefined, - String(courseId), String(toolId), + String(courseId), toolName, undefined, undefined, true From cb091b0cf91ff3138b2a72f0e19ffbe7c256b2cd Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Sun, 22 Mar 2026 09:57:23 +0300 Subject: [PATCH 24/26] refactor: implement ForeignKey relationship for HomeworkTaskLtiLaunchData --- .../20251230213439_Criteria.Designer.cs | 356 ---------------- .../Migrations/20251230213439_Criteria.cs | 44 -- .../20260104221447_InitialCreate.Designer.cs | 349 ---------------- ...106172258_AddLtiToolIdToCourse.Designer.cs | 351 ---------------- .../20260106172258_AddLtiToolIdToCourse.cs | 22 - ...eLtiToolIdToHomeworkTaskLtiUrl.Designer.cs | 349 ---------------- ...514_RemoveLtiToolIdToHomeworkTaskLtiUrl.cs | 23 -- ...260221230655_BonusTaskExplicit.Designer.cs | 358 ---------------- .../20260221230655_BonusTaskExplicit.cs | 23 -- ...301070634_AddCustomParamsToLti.Designer.cs | 351 ---------------- .../20260301070634_AddCustomParamsToLti.cs | 22 - ...80616_AddIsBonusExplicitColumn.Designer.cs | 382 ------------------ ...20260302080616_AddIsBonusExplicitColumn.cs | 63 --- ...0320150939_RenameAndChangeTypeLtiToolId.cs | 31 -- ... 20260322063832_InitialCreate.Designer.cs} | 14 +- ...ate.cs => 20260322063832_InitialCreate.cs} | 46 ++- .../Migrations/CourseContextModelSnapshot.cs | 10 +- .../Models/CourseContext.cs | 5 - .../Models/HomeworkTaskLtiLaunchData.cs | 5 +- .../Repositories/TasksRepository.cs | 6 +- .../Services/CoursesService.cs | 2 +- 21 files changed, 59 insertions(+), 2753 deletions(-) delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260302080616_AddIsBonusExplicitColumn.Designer.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260302080616_AddIsBonusExplicitColumn.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.cs rename HwProj.CoursesService/HwProj.CoursesService.API/Migrations/{20260320150939_RenameAndChangeTypeLtiToolId.Designer.cs => 20260322063832_InitialCreate.Designer.cs} (97%) rename HwProj.CoursesService/HwProj.CoursesService.API/Migrations/{20260104221447_InitialCreate.cs => 20260322063832_InitialCreate.cs} (89%) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.Designer.cs deleted file mode 100644 index 2e1d1382d..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.Designer.cs +++ /dev/null @@ -1,356 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20251230213439_Criteria")] - partial class Criteria - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("MaxPoints"); - - b.Property("Name"); - - b.Property("TaskId"); - - b.Property("Type"); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Criteria"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.Property("CourseMateId"); - - b.Property("Description"); - - b.Property("Tags"); - - b.HasKey("CourseMateId"); - - b.ToTable("StudentCharacteristics"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => - { - b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "Task") - .WithMany("Criteria") - .HasForeignKey("TaskId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseMate") - .WithOne("Characteristics") - .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.cs deleted file mode 100644 index 8ec08eac2..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20251230213439_Criteria.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class Criteria : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Criteria", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), - TaskId = table.Column(nullable: false), - Type = table.Column(nullable: false), - Name = table.Column(nullable: true), - MaxPoints = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Criteria", x => x.Id); - table.ForeignKey( - name: "FK_Criteria_Tasks_TaskId", - column: x => x.TaskId, - principalTable: "Tasks", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Criteria_TaskId", - table: "Criteria", - column: "TaskId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Criteria"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.Designer.cs deleted file mode 100644 index fe3c6fab2..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.Designer.cs +++ /dev/null @@ -1,349 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20260104221447_InitialCreate")] - partial class InitialCreate - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => - { - b.Property("TaskId"); - - b.Property("LtiLaunchUrl") - .IsRequired(); - - b.Property("ToolId"); - - b.HasKey("TaskId"); - - b.ToTable("TaskLtiUrls"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.Property("CourseMateId"); - - b.Property("Description"); - - b.Property("Tags"); - - b.HasKey("CourseMateId"); - - b.ToTable("StudentCharacteristics"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => - { - b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") - .WithOne() - .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", "TaskId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseMate") - .WithOne("Characteristics") - .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.Designer.cs deleted file mode 100644 index 0ff3ea68e..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.Designer.cs +++ /dev/null @@ -1,351 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20260106172258_AddLtiToolIdToCourse")] - partial class AddLtiToolIdToCourse - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("LtiToolId"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => - { - b.Property("TaskId"); - - b.Property("LtiLaunchUrl") - .IsRequired(); - - b.Property("ToolId"); - - b.HasKey("TaskId"); - - b.ToTable("TaskLtiUrls"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.Property("CourseMateId"); - - b.Property("Description"); - - b.Property("Tags"); - - b.HasKey("CourseMateId"); - - b.ToTable("StudentCharacteristics"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => - { - b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") - .WithOne() - .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", "TaskId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseMate") - .WithOne("Characteristics") - .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.cs deleted file mode 100644 index d5eff9f26..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260106172258_AddLtiToolIdToCourse.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class AddLtiToolIdToCourse : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LtiToolId", - table: "Courses", - nullable: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LtiToolId", - table: "Courses"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.Designer.cs deleted file mode 100644 index 233549e45..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.Designer.cs +++ /dev/null @@ -1,349 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl")] - partial class RemoveLtiToolIdToHomeworkTaskLtiUrl - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("LtiToolId"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => - { - b.Property("TaskId"); - - b.Property("LtiLaunchUrl") - .IsRequired(); - - b.HasKey("TaskId"); - - b.ToTable("TaskLtiUrls"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.Property("CourseMateId"); - - b.Property("Description"); - - b.Property("Tags"); - - b.HasKey("CourseMateId"); - - b.ToTable("StudentCharacteristics"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => - { - b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") - .WithOne() - .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", "TaskId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseMate") - .WithOne("Characteristics") - .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.cs deleted file mode 100644 index 585a1b6fb..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260107012514_RemoveLtiToolIdToHomeworkTaskLtiUrl.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class RemoveLtiToolIdToHomeworkTaskLtiUrl : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ToolId", - table: "TaskLtiUrls"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ToolId", - table: "TaskLtiUrls", - nullable: false, - defaultValue: 0); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.Designer.cs deleted file mode 100644 index 297f015d8..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.Designer.cs +++ /dev/null @@ -1,358 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20260221230655_BonusTaskExplicit")] - partial class BonusTaskExplicit - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("MaxPoints"); - - b.Property("Name"); - - b.Property("TaskId"); - - b.Property("Type"); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Criteria"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsBonusExplicit"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.Property("CourseMateId"); - - b.Property("Description"); - - b.Property("Tags"); - - b.HasKey("CourseMateId"); - - b.ToTable("StudentCharacteristics"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => - { - b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "Task") - .WithMany("Criteria") - .HasForeignKey("TaskId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseMate") - .WithOne("Characteristics") - .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.cs deleted file mode 100644 index ddc6b7d83..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260221230655_BonusTaskExplicit.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class BonusTaskExplicit : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsBonusExplicit", - table: "Tasks", - nullable: false, - defaultValue: false); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "IsBonusExplicit", - table: "Tasks"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.Designer.cs deleted file mode 100644 index c7d99fcde..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.Designer.cs +++ /dev/null @@ -1,351 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20260301070634_AddCustomParamsToLti")] - partial class AddCustomParamsToLti - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("LtiToolId"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => - { - b.Property("TaskId"); - - b.Property("CustomParams"); - - b.Property("LtiLaunchUrl") - .IsRequired(); - - b.HasKey("TaskId"); - - b.ToTable("TaskLtiUrls"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.Property("CourseMateId"); - - b.Property("Description"); - - b.Property("Tags"); - - b.HasKey("CourseMateId"); - - b.ToTable("StudentCharacteristics"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => - { - b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") - .WithOne() - .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", "TaskId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseMate") - .WithOne("Characteristics") - .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.cs deleted file mode 100644 index 923322f71..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260301070634_AddCustomParamsToLti.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class AddCustomParamsToLti : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "CustomParams", - table: "TaskLtiUrls", - nullable: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "CustomParams", - table: "TaskLtiUrls"); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260302080616_AddIsBonusExplicitColumn.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260302080616_AddIsBonusExplicitColumn.Designer.cs deleted file mode 100644 index 562dac219..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260302080616_AddIsBonusExplicitColumn.Designer.cs +++ /dev/null @@ -1,382 +0,0 @@ -// -using System; -using HwProj.CoursesService.API.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace HwProj.CoursesService.API.Migrations -{ - [DbContext(typeof(CourseContext))] - [Migration("20260302080616_AddIsBonusExplicitColumn")] - partial class AddIsBonusExplicitColumn - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("MentorId"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Assignments"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupName"); - - b.Property("InviteCode"); - - b.Property("IsCompleted"); - - b.Property("IsOpen"); - - b.Property("LtiToolId"); - - b.Property("MentorIds"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Courses"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("FilterJson"); - - b.HasKey("Id"); - - b.ToTable("CourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("IsAccepted"); - - b.Property("StudentId"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("CourseMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("MaxPoints"); - - b.Property("Name"); - - b.Property("TaskId"); - - b.Property("Type"); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Criteria"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("Name"); - - b.HasKey("Id"); - - b.ToTable("Groups"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("StudentId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasAlternateKey("GroupId", "StudentId"); - - b.ToTable("GroupMates"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("CourseId"); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("IsDeadlineStrict"); - - b.Property("PublicationDate"); - - b.Property("Tags"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("CourseId"); - - b.ToTable("Homeworks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("DeadlineDate"); - - b.Property("Description"); - - b.Property("HasDeadline"); - - b.Property("HomeworkId"); - - b.Property("IsBonusExplicit"); - - b.Property("IsDeadlineStrict"); - - b.Property("MaxRating"); - - b.Property("PublicationDate"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.HasIndex("HomeworkId"); - - b.ToTable("Tasks"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => - { - b.Property("TaskId"); - - b.Property("CustomParams"); - - b.Property("LtiLaunchUrl") - .IsRequired(); - - b.HasKey("TaskId"); - - b.ToTable("TaskLtiData"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.Property("CourseMateId"); - - b.Property("Description"); - - b.Property("Tags"); - - b.HasKey("CourseMateId"); - - b.ToTable("StudentCharacteristics"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("GroupId"); - - b.Property("TaskId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("TasksModels"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("Answer") - .HasMaxLength(1000); - - b.Property("IsPrivate"); - - b.Property("LecturerId"); - - b.Property("StudentId"); - - b.Property("TaskId"); - - b.Property("Text") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.HasIndex("TaskId"); - - b.ToTable("Questions"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.Property("CourseId"); - - b.Property("UserId"); - - b.Property("CourseFilterId"); - - b.HasKey("CourseId", "UserId"); - - b.HasIndex("CourseFilterId"); - - b.ToTable("UserToCourseFilters"); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Assignments") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("CourseMates") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => - { - b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "Task") - .WithMany("Criteria") - .HasForeignKey("TaskId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("GroupMates") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Course") - .WithMany("Homeworks") - .HasForeignKey("CourseId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") - .WithMany("Tasks") - .HasForeignKey("HomeworkId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", b => - { - b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") - .WithOne() - .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiUrl", "TaskId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseMate") - .WithOne("Characteristics") - .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => - { - b.HasOne("HwProj.CoursesService.API.Models.Group") - .WithMany("Tasks") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => - { - b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") - .WithMany() - .HasForeignKey("CourseFilterId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260302080616_AddIsBonusExplicitColumn.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260302080616_AddIsBonusExplicitColumn.cs deleted file mode 100644 index 62c5cbd74..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260302080616_AddIsBonusExplicitColumn.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class AddIsBonusExplicitColumn : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_TaskLtiUrls_Tasks_TaskId", - table: "TaskLtiUrls"); - - migrationBuilder.DropPrimaryKey( - name: "PK_TaskLtiUrls", - table: "TaskLtiUrls"); - - migrationBuilder.RenameTable( - name: "TaskLtiUrls", - newName: "TaskLtiData"); - - migrationBuilder.AddPrimaryKey( - name: "PK_TaskLtiData", - table: "TaskLtiData", - column: "TaskId"); - - migrationBuilder.AddForeignKey( - name: "FK_TaskLtiData_Tasks_TaskId", - table: "TaskLtiData", - column: "TaskId", - principalTable: "Tasks", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_TaskLtiData_Tasks_TaskId", - table: "TaskLtiData"); - - migrationBuilder.DropPrimaryKey( - name: "PK_TaskLtiData", - table: "TaskLtiData"); - - migrationBuilder.RenameTable( - name: "TaskLtiData", - newName: "TaskLtiUrls"); - - migrationBuilder.AddPrimaryKey( - name: "PK_TaskLtiUrls", - table: "TaskLtiUrls", - column: "TaskId"); - - migrationBuilder.AddForeignKey( - name: "FK_TaskLtiUrls_Tasks_TaskId", - table: "TaskLtiUrls", - column: "TaskId", - principalTable: "Tasks", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.cs deleted file mode 100644 index 85ae00f61..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace HwProj.CoursesService.API.Migrations -{ - public partial class RenameAndChangeTypeLtiToolId : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LtiToolId", - table: "Courses"); - - migrationBuilder.AddColumn( - name: "LtiToolName", - table: "Courses", - nullable: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LtiToolName", - table: "Courses"); - - migrationBuilder.AddColumn( - name: "LtiToolId", - table: "Courses", - nullable: true); - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.Designer.cs similarity index 97% rename from HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.Designer.cs rename to HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.Designer.cs index 044ee315c..a96a714f2 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260320150939_RenameAndChangeTypeLtiToolId.Designer.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.Designer.cs @@ -10,8 +10,8 @@ namespace HwProj.CoursesService.API.Migrations { [DbContext(typeof(CourseContext))] - [Migration("20260320150939_RenameAndChangeTypeLtiToolId")] - partial class RenameAndChangeTypeLtiToolId + [Migration("20260322063832_InitialCreate")] + partial class InitialCreate { protected override void BuildTargetModel(ModelBuilder modelBuilder) { @@ -213,14 +213,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => { - b.Property("TaskId"); + b.Property("HomeworkTaskId"); b.Property("CustomParams"); b.Property("LtiLaunchUrl") .IsRequired(); - b.HasKey("TaskId"); + b.HasKey("HomeworkTaskId"); b.ToTable("TaskLtiData"); }); @@ -347,9 +347,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => { - b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") - .WithOne() - .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", "TaskId") + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "HomeworkTask") + .WithMany() + .HasForeignKey("HomeworkTaskId") .OnDelete(DeleteBehavior.Cascade); }); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.cs similarity index 89% rename from HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.cs rename to HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.cs index 836a5a933..11a37c098 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260104221447_InitialCreate.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260322063832_InitialCreate.cs @@ -32,7 +32,8 @@ protected override void Up(MigrationBuilder migrationBuilder) IsOpen = table.Column(nullable: false), InviteCode = table.Column(nullable: true), IsCompleted = table.Column(nullable: false), - MentorIds = table.Column(nullable: true) + MentorIds = table.Column(nullable: true), + LtiToolName = table.Column(nullable: true) }, constraints: table => { @@ -231,6 +232,7 @@ protected override void Up(MigrationBuilder migrationBuilder) DeadlineDate = table.Column(nullable: true), IsDeadlineStrict = table.Column(nullable: true), PublicationDate = table.Column(nullable: true), + IsBonusExplicit = table.Column(nullable: false), HomeworkId = table.Column(nullable: false) }, constraints: table => @@ -245,24 +247,46 @@ protected override void Up(MigrationBuilder migrationBuilder) }); migrationBuilder.CreateTable( - name: "TaskLtiUrls", + name: "Criteria", columns: table => new { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), TaskId = table.Column(nullable: false), - LtiLaunchUrl = table.Column(nullable: false), - ToolId = table.Column(nullable: false) + Type = table.Column(nullable: false), + Name = table.Column(nullable: true), + MaxPoints = table.Column(nullable: false) }, constraints: table => { - table.PrimaryKey("PK_TaskLtiUrls", x => x.TaskId); + table.PrimaryKey("PK_Criteria", x => x.Id); table.ForeignKey( - name: "FK_TaskLtiUrls_Tasks_TaskId", + name: "FK_Criteria_Tasks_TaskId", column: x => x.TaskId, principalTable: "Tasks", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "TaskLtiData", + columns: table => new + { + HomeworkTaskId = table.Column(nullable: false), + LtiLaunchUrl = table.Column(nullable: false), + CustomParams = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TaskLtiData", x => x.HomeworkTaskId); + table.ForeignKey( + name: "FK_TaskLtiData_Tasks_HomeworkTaskId", + column: x => x.HomeworkTaskId, + principalTable: "Tasks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateIndex( name: "IX_Assignments_CourseId", table: "Assignments", @@ -273,6 +297,11 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "CourseMates", column: "CourseId"); + migrationBuilder.CreateIndex( + name: "IX_Criteria_TaskId", + table: "Criteria", + column: "TaskId"); + migrationBuilder.CreateIndex( name: "IX_Homeworks_CourseId", table: "Homeworks", @@ -304,6 +333,9 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "Assignments"); + migrationBuilder.DropTable( + name: "Criteria"); + migrationBuilder.DropTable( name: "GroupMates"); @@ -314,7 +346,7 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "StudentCharacteristics"); migrationBuilder.DropTable( - name: "TaskLtiUrls"); + name: "TaskLtiData"); migrationBuilder.DropTable( name: "TasksModels"); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs index ee14be8a0..fab15ce1a 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -211,14 +211,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => { - b.Property("TaskId"); + b.Property("HomeworkTaskId"); b.Property("CustomParams"); b.Property("LtiLaunchUrl") .IsRequired(); - b.HasKey("TaskId"); + b.HasKey("HomeworkTaskId"); b.ToTable("TaskLtiData"); }); @@ -345,9 +345,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", b => { - b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask") - .WithOne() - .HasForeignKey("HwProj.CoursesService.API.Models.HomeworkTaskLtiLaunchData", "TaskId") + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "HomeworkTask") + .WithMany() + .HasForeignKey("HomeworkTaskId") .OnDelete(DeleteBehavior.Cascade); }); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs index eb5a2df36..53cd7f80a 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs @@ -29,11 +29,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasIndex(a => a.CourseId); modelBuilder.Entity().HasKey(u => new { u.CourseId, u.UserId }); modelBuilder.Entity().HasIndex(t => t.TaskId); - modelBuilder.Entity() - .HasOne() - .WithOne() - .HasForeignKey(u => u.TaskId) - .OnDelete(DeleteBehavior.Cascade); } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiLaunchData.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiLaunchData.cs index 2edd3c907..a96104290 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiLaunchData.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/HomeworkTaskLtiLaunchData.cs @@ -7,12 +7,15 @@ public class HomeworkTaskLtiLaunchData { [Key] [DatabaseGenerated(DatabaseGeneratedOption.None)] - public long TaskId { get; set; } + public long HomeworkTaskId { get; set; } [Required] public string LtiLaunchUrl { get; set; } /// JSON public string? CustomParams { get; set; } + + [ForeignKey(nameof(HomeworkTaskId))] + public HomeworkTask HomeworkTask { get; set; } } } \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs index b2ea8a2e5..bbe30d149 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs @@ -68,7 +68,7 @@ public async Task AddOrUpdateLtiLaunchDataAsync(long taskId, LtiLaunchData ltiLa { var ltiRecord = new HomeworkTaskLtiLaunchData { - TaskId = taskId, + HomeworkTaskId = taskId, LtiLaunchUrl = ltiLaunchData.LtiLaunchUrl, CustomParams = ltiLaunchData.CustomParams }; @@ -105,8 +105,8 @@ public async Task AddRangeLtiLaunchDataAsync(IEnumerable> GetLtiDataForTasksAsync(IEnumerable taskIds) { return await Context.Set() - .Where(t => taskIds.Contains(t.TaskId)) - .ToDictionaryAsync(t => t.TaskId, t => new LtiLaunchData + .Where(t => taskIds.Contains(t.HomeworkTaskId)) + .ToDictionaryAsync(t => t.HomeworkTaskId, t => new LtiLaunchData { LtiLaunchUrl = t.LtiLaunchUrl, CustomParams = t.CustomParams diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs index 2f1b37036..f3ba087b1 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -203,7 +203,7 @@ private async Task AddFromTemplateAsync(CourseTemplate courseTemplate, L { ltiDataToSave.Add(new HomeworkTaskLtiLaunchData { - TaskId = pair.NewEntity.Id, + HomeworkTaskId = pair.NewEntity.Id, LtiLaunchUrl = pair.Template.LtiLaunchData.LtiLaunchUrl, CustomParams = pair.Template.LtiLaunchData.CustomParams }); From efe2b8fccaa24075e531a7f50085d203268a2d54 Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Sun, 22 Mar 2026 10:35:28 +0300 Subject: [PATCH 25/26] refactor: update appsettings.json --- .../HwProj.APIGateway.API/appsettings.json | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json index 1e2357a7e..ece890536 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json +++ b/HwProj.APIGateway/HwProj.APIGateway.API/appsettings.json @@ -31,14 +31,13 @@ }, "LtiTools": [ { - "id": 1, - "name": "", - "Issuer": "", - "clientId": "", - "JwksEndpoint": "", - "initiateLoginUri": "", - "launchUrl": "", - "deepLinking": "" + "name": "Local Mock Tool", + "Issuer": "Local Mock Tool", + "clientId": "mock-tool-client-id", + "JwksEndpoint": "http://localhost:5000/api/mocktool/jwks", + "initiateLoginUri": "http://localhost:5000/api/mocktool/login", + "launchUrl": "http://localhost:5000/api/mocktool/callback", + "deepLinking": "http://localhost:5000/api/mocktool/callback" } ] } From b9efaf094b166d36a6c3db1460287ac349865fbd Mon Sep 17 00:00:00 2001 From: kirillbenga Date: Sun, 22 Mar 2026 10:43:33 +0300 Subject: [PATCH 26/26] refactor: returned the appsettings.json --- HwProj.AuthService/HwProj.AuthService.API/appsettings.json | 4 ++-- .../HwProj.CoursesService.API/appsettings.json | 4 ++-- .../HwProj.NotificationsService.API/appsettings.json | 4 ++-- .../HwProj.SolutionsService.API/appsettings.json | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/HwProj.AuthService/HwProj.AuthService.API/appsettings.json b/HwProj.AuthService/HwProj.AuthService.API/appsettings.json index e95f4a1f7..c2777316b 100644 --- a/HwProj.AuthService/HwProj.AuthService.API/appsettings.json +++ b/HwProj.AuthService/HwProj.AuthService.API/appsettings.json @@ -29,8 +29,8 @@ }, "EventBus": { "EventBusHostName": "localhost", - "EventBusUserName": "user", - "EventBusPassword": "password", + "EventBusUserName": "guest", + "EventBusPassword": "guest", "EventBusVirtualHost": "/", "EventBusQueueName": "AuthService", "EventBusRetryCount": "5" diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json b/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json index e64da15b9..4f4452b1b 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json +++ b/HwProj.CoursesService/HwProj.CoursesService.API/appsettings.json @@ -11,8 +11,8 @@ "AllowedHosts": "*", "EventBus": { "EventBusHostName": "localhost", - "EventBusUserName": "user", - "EventBusPassword": "password", + "EventBusUserName": "guest", + "EventBusPassword": "guest", "EventBusVirtualHost": "/", "EventBusQueueName": "CoursesService", "EventBusRetryCount": "5" diff --git a/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json b/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json index 69b695e1b..f196fffa6 100644 --- a/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json +++ b/HwProj.NotificationsService/HwProj.NotificationsService.API/appsettings.json @@ -11,8 +11,8 @@ "AllowedHosts": "*", "EventBus": { "EventBusHostName": "localhost", - "EventBusUserName": "user", - "EventBusPassword": "password", + "EventBusUserName": "guest", + "EventBusPassword": "guest", "EventBusVirtualHost": "/", "EventBusQueueName": "NotificationService", "EventBusRetryCount": "5" diff --git a/HwProj.SolutionsService/HwProj.SolutionsService.API/appsettings.json b/HwProj.SolutionsService/HwProj.SolutionsService.API/appsettings.json index b2b0e5a1b..e00816874 100644 --- a/HwProj.SolutionsService/HwProj.SolutionsService.API/appsettings.json +++ b/HwProj.SolutionsService/HwProj.SolutionsService.API/appsettings.json @@ -11,8 +11,8 @@ "AllowedHosts": "*", "EventBus": { "EventBusHostName": "localhost", - "EventBusUserName": "user", - "EventBusPassword": "password", + "EventBusUserName": "guest", + "EventBusPassword": "guest", "EventBusVirtualHost": "/", "EventBusQueueName": "SolutionsService", "EventBusRetryCount": "5"