Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
a34425d
add: file uploader element to add solution page
semrosin Sep 29, 2025
d873449
add: files count validation
semrosin Sep 30, 2025
814d06d
add: file type validation
semrosin Sep 30, 2025
7b17225
fix: files count validation
semrosin Oct 2, 2025
338b35d
fix: default lastSolution id
semrosin Oct 6, 2025
fd2d09d
feat: files processing in taskSolutionPage
semrosin Oct 6, 2025
11681f4
feat: files processing in studentSolutionPage
semrosin Oct 6, 2025
f58d21f
feat: make course files state universal
semrosin Oct 6, 2025
426901b
feat: add props in task solutions component
semrosin Oct 6, 2025
e4d9c17
feat: get files info for solutions in converter
semrosin Oct 6, 2025
8c410e8
feat: files preview in solution component
semrosin Oct 6, 2025
0fb8aa4
feat: processing files after adding solution
semrosin Oct 6, 2025
554fc90
fix: add solution imports
semrosin Oct 6, 2025
063735b
feat: make edit files intarface exporting
semrosin Oct 15, 2025
42a8659
fix: start processing after adding solution
semrosin Oct 19, 2025
3864735
feat: add solution privacy attribute
semrosin Oct 19, 2025
9136289
feat: add privacy attribute for processing
semrosin Oct 19, 2025
322de21
feat: add attributes to startup
semrosin Oct 19, 2025
bcff142
feat: add lecturer or student role
semrosin Oct 19, 2025
40bb94a
feat: change processing validation (back)
semrosin Oct 19, 2025
243eefa
feat: change get statuses validation (back)
semrosin Oct 19, 2025
b91a3c3
feat: change download link validation (back)
semrosin Oct 19, 2025
0628bb6
feat: add scope dto with file id
semrosin Oct 19, 2025
a2e3b80
feat: change download link api call (front)
semrosin Oct 19, 2025
a958fce
fix: files preview without comment
semrosin Oct 19, 2025
5af661e
feat: file type validation (back)
semrosin Oct 20, 2025
d55f007
fix: front file type validation
semrosin Oct 20, 2025
cc2efea
feat: add files access for groups
semrosin Oct 20, 2025
7150a1a
refactor: make studentIds HashSet
semrosin Oct 23, 2025
a40c42a
feat: process files for groupmates
semrosin Oct 23, 2025
8eff112
fix: dispose stream in back type validation
semrosin Oct 25, 2025
9341495
feat: separate access files functionality
semrosin Oct 25, 2025
8f8c57d
fix: show solution files uploading status for students only
semrosin Oct 25, 2025
a64510d
refactor: delete unused function in files accessor
semrosin Oct 25, 2025
ea1ffa5
fix: intervalRef usage in files accessor
semrosin Oct 25, 2025
365a2a6
fix: subscribe updating course files on course id
semrosin Oct 25, 2025
9ac14b1
feat: update solutions components for files accessor
semrosin Oct 25, 2025
767213c
fix: padding after solution files
semrosin Oct 26, 2025
972d106
fix: return alien code
semrosin Oct 26, 2025
ddf6936
README: базовое описание сервисов
DedSec256 Oct 26, 2025
4c6fa0c
Update project title to include emoji
DedSec256 Oct 26, 2025
d32b88f
Add update news link and interactive presentation
DedSec256 Oct 26, 2025
d586b94
Update README.md
YuriUfimtsev Oct 26, 2025
354712a
fix README.md
YuriUfimtsev Oct 26, 2025
1bf82dd
refactor: deleteunused variables, await with async calls
semrosin Oct 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public FilesController(IAuthServiceClient authServiceClient,
}

[HttpPost("process")]
[Authorize(Roles = Roles.LecturerRole)]
[ServiceFilter(typeof(CourseMentorOnlyAttribute))]
[Authorize(Roles = Roles.LecturerOrStudentRole)]
[ServiceFilter(typeof(CourseMentorOrSolutionStudentAttribute))]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
public async Task<IActionResult> Process([FromForm] ProcessFilesDTO processFilesDto)
Expand All @@ -37,8 +37,8 @@ public async Task<IActionResult> Process([FromForm] ProcessFilesDTO processFiles
}

[HttpPost("statuses")]
[Authorize(Roles = Roles.LecturerRole)]
[ServiceFilter(typeof(CourseMentorOnlyAttribute))]
[Authorize(Roles = Roles.LecturerOrStudentRole)]
[ServiceFilter(typeof(SolutionPrivacyAttribute))]
[ProducesResponseType(typeof(FileInfoDTO[]), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
public async Task<IActionResult> GetStatuses(ScopeDTO filesScope)
Expand All @@ -49,12 +49,13 @@ public async Task<IActionResult> GetStatuses(ScopeDTO filesScope)
: StatusCode((int)HttpStatusCode.ServiceUnavailable, filesStatusesResult.Errors);
}

[HttpGet("downloadLink")]
[HttpPost("downloadLink")]
[ServiceFilter(typeof(SolutionPrivacyAttribute))]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.NotFound)]
public async Task<IActionResult> GetDownloadLink([FromQuery] long fileId)
public async Task<IActionResult> GetDownloadLink([FromForm] FileScopeDTO fileScope)
{
var result = await _contentServiceClient.GetDownloadLinkAsync(fileId);
var result = await _contentServiceClient.GetDownloadLinkAsync(fileScope.FileId);
return result.Succeeded
? Ok(result.Value)
: NotFound(result.Errors);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
namespace HwProj.APIGateway.API.Filters;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using CoursesService.Client;
using SolutionsService.Client;
using HwProj.Models.ContentService.DTO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public class CourseMentorOrSolutionStudentAttribute : ActionFilterAttribute
{
private readonly ICoursesServiceClient _coursesServiceClient;
private readonly ISolutionsServiceClient _solutionsServiceClient;

public CourseMentorOrSolutionStudentAttribute(ICoursesServiceClient coursesServiceClient, ISolutionsServiceClient solutionsServiceClient)
{
_coursesServiceClient = coursesServiceClient;
_solutionsServiceClient = solutionsServiceClient;
}

public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var userId = context.HttpContext.User.Claims
.FirstOrDefault(claim => claim.Type.ToString() == "_id")?.Value;
if (userId == null)
{
context.Result = new ContentResult
{
StatusCode = StatusCodes.Status403Forbidden,
Content = "В запросе не передан идентификатор пользователя",
ContentType = "application/json"
};
return;
}

long courseId = -1;
var courseUnitType = "";
long courseUnitId = -1;

if (context.ActionArguments.TryGetValue("processFilesDto", out var processFilesDto) &&
processFilesDto is ProcessFilesDTO dto)
{
courseId = dto.FilesScope.CourseId;
courseUnitType = dto.FilesScope.CourseUnitType;
courseUnitId = dto.FilesScope.CourseUnitId;
}

if (courseUnitType == "Solution")
{
var studentIds = new HashSet<string>();
if (courseId != -1)
{
var solution = await _solutionsServiceClient.GetSolutionById(courseUnitId);
studentIds.Add(solution.StudentId);
var group = await _coursesServiceClient.GetGroupsById(solution.GroupId ?? 0);
studentIds.UnionWith(group.FirstOrDefault()?.StudentsIds.ToHashSet() ?? new());
}

if (!studentIds.Contains(userId))
{
context.Result = new ContentResult
{
StatusCode = StatusCodes.Status403Forbidden,
Content = "Недостаточно прав для работы с файлами: Вы не являетесь студентом, отправляющим задание",
ContentType = "application/json"
};
return;
}
} else if (courseUnitType == "Homework")
{
string[]? mentorIds = null;

if (courseId != -1)
mentorIds = await _coursesServiceClient.GetCourseLecturersIds(courseId);
if (mentorIds == null || !mentorIds.Contains(userId))
{
context.Result = new ContentResult
{
StatusCode = StatusCodes.Status403Forbidden,
Content = "Недостаточно прав для работы с файлами: Вы не являетесь ментором на курсе",
ContentType = "application/json"
};
return;
}
}

await next.Invoke();
}

private static string? GetValueFromRequest(HttpRequest request, string key)
{
if (request.Query.TryGetValue(key, out var queryValue))
return queryValue.ToString();

if (request.HasFormContentType && request.Form.TryGetValue(key, out var formValue))
return formValue.ToString();

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
namespace HwProj.APIGateway.API.Filters;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using HwProj.CoursesService.Client;
using HwProj.SolutionsService.Client;
using HwProj.Models.ContentService.DTO;
using HwProj.Models.Roles;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public class SolutionPrivacyAttribute : ActionFilterAttribute
{
private readonly ICoursesServiceClient _coursesServiceClient;
private readonly ISolutionsServiceClient _solutionsServiceClient;

public SolutionPrivacyAttribute(ICoursesServiceClient coursesServiceClient, ISolutionsServiceClient solutionsServiceClient)
{
_coursesServiceClient = coursesServiceClient;
_solutionsServiceClient = solutionsServiceClient;
}

public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var userId = context.HttpContext.User.Claims
.FirstOrDefault(claim => claim.Type.ToString() == "_id")?.Value;
var userRole = context.HttpContext.User.Claims
.FirstOrDefault(claim => claim.Type.ToString().EndsWith("role"))?.Value;

if (userId == null || userRole == null)
{
context.Result = new ContentResult
{
StatusCode = StatusCodes.Status403Forbidden,
Content = "В запросе не передан идентификатор пользователя",
ContentType = "application/json"
};
return;
}


long courseId = -1;
var courseUnitType = "";
long courseUnitId = -1;

// Для метода GetStatuses (параметр: filesScope)
if (context.ActionArguments.TryGetValue("filesScope", out var filesScope) &&
filesScope is ScopeDTO scopeDto)
{
courseId = scopeDto.CourseId;
courseUnitType = scopeDto.CourseUnitType;
courseUnitId = scopeDto.CourseUnitId;
}
// Для метода GetDownloadLink (параметр: fileScope)
else if (context.ActionArguments.TryGetValue("fileScope", out var fileScope) &&
fileScope is FileScopeDTO fileScopeDto)
{
courseId = fileScopeDto.CourseId;
courseUnitType = fileScopeDto.CourseUnitType;
courseUnitId = fileScopeDto.CourseUnitId;
}

if (courseUnitType == "Homework") await next.Invoke();

if (userRole == Roles.StudentRole)
{
var studentIds = new HashSet<string>();
if (courseId != -1)
{
var solution = await _solutionsServiceClient.GetSolutionById(courseUnitId);
studentIds.Add(solution.StudentId);
var group = await _coursesServiceClient.GetGroupsById(solution.GroupId ?? 0);
studentIds.UnionWith(group.FirstOrDefault()?.StudentsIds.ToHashSet() ?? new());
}

if (!studentIds.Contains(userId))
{
context.Result = new ContentResult
{
StatusCode = StatusCodes.Status403Forbidden,
Content = "Недостаточно прав для работы с файлами: Вы не являетесь студентом, отправляющим задание",
ContentType = "application/json"
};
return;
}
} else if (userRole == Roles.LecturerRole)
{
string[]? mentorIds = null;

if (courseId != -1)
mentorIds = await _coursesServiceClient.GetCourseLecturersIds(courseId);
if (mentorIds == null || !mentorIds.Contains(userId))
{
context.Result = new ContentResult
{
StatusCode = StatusCodes.Status403Forbidden,
Content = "Недостаточно прав для работы с файлами: Вы не являетесь ментором на курсе",
ContentType = "application/json"
};
return;
}
}

await next.Invoke();
}

private static string? GetValueFromRequest(HttpRequest request, string key)
{
if (request.Query.TryGetValue(key, out var queryValue))
return queryValue.ToString();

if (request.HasFormContentType && request.Form.TryGetValue(key, out var formValue))
return formValue.ToString();

return null;
}
}
4 changes: 3 additions & 1 deletion HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ public void ConfigureServices(IServiceCollection services)
services.AddNotificationsServiceClient();
services.AddContentServiceClient();

services.AddScoped<CourseMentorOnlyAttribute>();
services.AddScoped<CourseMentorOnlyAttribute>(); //TODO : delete?
services.AddScoped<SolutionPrivacyAttribute>();
services.AddScoped<CourseMentorOrSolutionStudentAttribute>();
}

public void Configure(IApplicationBuilder app, IHostEnvironment env)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNetCore.Http;

namespace HwProj.Models.ContentService.Attributes
{
[AttributeUsage(AttributeTargets.Property)]
public class CorrectFileTypeAttribute : ValidationAttribute
{
private static HashSet<byte[]> forbiddenFileSignatures = new HashSet<byte[]>{

new byte[] { 0x4d, 0x5a }, // MZ (exe BE)
new byte[] { 0x5a, 0x4d }, // ZM (exe LE)

new byte[] { 0x7F, 0x45, 0x4C, 0x46 }, // ELF

new byte[] { 0xfe, 0xed, 0xfa, 0xce }, // Mach-O BE 32-bit
new byte[] { 0xfe, 0xed, 0xfa, 0xcf }, // Mach-O BE 64-bit
new byte[] { 0xce, 0xfa, 0xed, 0xfe }, // Mach-O LE 32-bit
new byte[] { 0xcf, 0xfa, 0xed, 0xfe }, // Mach-O LE 64-bit

};

protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
var files = value switch
{
IFormFile singleFile => new[] { singleFile },
IEnumerable<IFormFile> filesCollection => filesCollection,
_ => null
};

if (files == null) return ValidationResult.Success;

foreach (var file in files)
{
try
{
// Первые байты для проверки сигнатуры
var buffer = new byte[4];
using (var stream = file.OpenReadStream())
{
var bytesRead = stream.Read(buffer, 0, buffer.Length);

if (bytesRead < 2)
return ValidationResult.Success; // Слишком короткий файл, не исполняемый

foreach (var signature in forbiddenFileSignatures)
{
if (signature.SequenceEqual(buffer.Take(signature.Length)))
{
return new ValidationResult(
$"Файл `{file.FileName}` имеет недопустимый тип ${file.ContentType}");
}
}
}
}
catch
{
return new ValidationResult(
$"Невозможно прочитать файл `{file.FileName}`");
}
}

return ValidationResult.Success;
}
}
}
10 changes: 10 additions & 0 deletions HwProj.Common/HwProj.Models/ContentService/DTO/FileScopeDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace HwProj.Models.ContentService.DTO
{
public class FileScopeDTO
{
public long FileId { get; set; }
public long CourseId { get; set; }
public string CourseUnitType { get; set; }
public long CourseUnitId { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class ProcessFilesDTO

public List<long> DeletingFileIds { get; set; } = new List<long>();

[CorrectFileType]
[MaxFileSize(100 * 1024 * 1024)]
public List<IFormFile> NewFiles { get; set; } = new List<IFormFile>();
}
Expand Down
1 change: 1 addition & 0 deletions HwProj.Common/HwProj.Models/Roles/Roles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public static class Roles
public const string StudentRole = "Student";
public const string ExpertRole = "Expert";
public const string LecturerOrExpertRole = "Lecturer, Expert";
public const string LecturerOrStudentRole = "Lecturer, Student";
}
}
Loading
Loading