Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
803d1d2
add: file uploader element to add solution page
semrosin Sep 29, 2025
ae19141
add: files count validation
semrosin Sep 30, 2025
9a52552
add: file type validation
semrosin Sep 30, 2025
7390848
fix: files count validation
semrosin Oct 2, 2025
47f6123
fix: default lastSolution id
semrosin Oct 6, 2025
ee10140
feat: files processing in taskSolutionPage
semrosin Oct 6, 2025
b949a2e
feat: files processing in studentSolutionPage
semrosin Oct 6, 2025
403a459
feat: make course files state universal
semrosin Oct 6, 2025
e3a9190
feat: add props in task solutions component
semrosin Oct 6, 2025
b62c1d5
feat: get files info for solutions in converter
semrosin Oct 6, 2025
b595000
feat: files preview in solution component
semrosin Oct 6, 2025
987cc2a
feat: processing files after adding solution
semrosin Oct 6, 2025
d7e052a
fix: add solution imports
semrosin Oct 6, 2025
d301e61
feat: make edit files intarface exporting
semrosin Oct 15, 2025
3119729
fix: start processing after adding solution
semrosin Oct 19, 2025
4f753eb
feat: add solution privacy attribute
semrosin Oct 19, 2025
af46907
feat: add privacy attribute for processing
semrosin Oct 19, 2025
75a5dab
feat: add attributes to startup
semrosin Oct 19, 2025
ba113ae
feat: add lecturer or student role
semrosin Oct 19, 2025
e5f1c9c
feat: change processing validation (back)
semrosin Oct 19, 2025
611f208
feat: change get statuses validation (back)
semrosin Oct 19, 2025
d708bd0
feat: change download link validation (back)
semrosin Oct 19, 2025
e55d743
feat: add scope dto with file id
semrosin Oct 19, 2025
daa0e63
feat: change download link api call (front)
semrosin Oct 19, 2025
1278615
fix: files preview without comment
semrosin Oct 19, 2025
990027c
feat: file type validation (back)
semrosin Oct 20, 2025
ae5b58b
fix: front file type validation
semrosin Oct 20, 2025
364127e
feat: add files access for groups
semrosin Oct 20, 2025
b4ceae3
refactor: make studentIds HashSet
semrosin Oct 23, 2025
4b52b46
feat: process files for groupmates
semrosin Oct 23, 2025
1e5bd31
fix: dispose stream in back type validation
semrosin Oct 25, 2025
eb2d272
feat: separate access files functionality
semrosin Oct 25, 2025
4adb6e0
fix: show solution files uploading status for students only
semrosin Oct 25, 2025
18b170b
refactor: delete unused function in files accessor
semrosin Oct 25, 2025
510b12e
fix: intervalRef usage in files accessor
semrosin Oct 25, 2025
8a14f89
fix: subscribe updating course files on course id
semrosin Oct 25, 2025
6716f0c
feat: update solutions components for files accessor
semrosin Oct 25, 2025
81f6806
fix: padding after solution files
semrosin Oct 26, 2025
fb9e646
fix: return alien code
semrosin Oct 26, 2025
a916c7b
refactor: deleteunused variables, await with async calls
semrosin Oct 28, 2025
1f04d4b
feat: [back] add class for privacy validation
semrosin Nov 19, 2025
d7f0add
feat [back]: method to get file scope
semrosin Nov 19, 2025
ad3578a
fix: delete attribute validation
semrosin Nov 19, 2025
a8a991d
fix: delete unused role
semrosin Nov 19, 2025
8c4e93e
feat [back]: method to get files scope in info service
semrosin Nov 19, 2025
aeefc3b
refactor [back]: return dto from files controller download link
semrosin Nov 19, 2025
b21bf75
refactor [back]: return dto from content client download link
semrosin Nov 19, 2025
a11ad47
feat [back]: add privacy filter to start up
semrosin Nov 19, 2025
0177412
feat [back]: file link dto
semrosin Nov 19, 2025
3f1d84f
fix: download link request
semrosin Nov 19, 2025
03886d7
feat [back]: privacy validation
semrosin Nov 19, 2025
26681c0
refactor: delete unused validation attributes
semrosin Nov 19, 2025
851a912
refactor [front]: rename files upload waiter
semrosin Nov 19, 2025
1aa83bf
feat [front]: variability for max files count
semrosin Nov 19, 2025
0f4a812
refactor [front]: course files access to upload waiter
semrosin Nov 19, 2025
4b8d797
fix [front]: rename usage of upload waiter
semrosin Nov 19, 2025
26dbe90
fix [front]: rename usages of download link getter
semrosin Nov 19, 2025
45ad3fa
refactor [front]: unify unit files info getter
semrosin Nov 19, 2025
13388c9
feat [front]: delete files saved status text
semrosin Nov 19, 2025
e862b60
refactor [front]: delete unused files info array
semrosin Nov 19, 2025
bc0292c
refactor [front]: separate files handle logic
semrosin Nov 19, 2025
65364ee
refactor [back]: type validation by foreign library
semrosin Nov 20, 2025
096da5e
refactor [back]: scope usage in privacy filter
semrosin Nov 20, 2025
0804463
fix [back]: return with privacy error
semrosin Nov 21, 2025
ffe54a2
feat [back]: add max files count filter
semrosin Nov 21, 2025
c29116a
fix [front]: max files count showing
semrosin Nov 22, 2025
0a25ae8
fix [back]: add files count limit to start up
semrosin Nov 22, 2025
da30fe3
feat [back]: showing max files count on limit exceeding
semrosin Nov 22, 2025
e58e637
refactor: separate methods in privacy filter
semrosin Dec 15, 2025
405cb92
refactor: create courseUnitType constans
semrosin Dec 15, 2025
72081bc
fix: privacy filter
semrosin Dec 17, 2025
39a7e04
wip
DedSec256 Jan 10, 2026
8df7cc5
wip
DedSec256 Jan 10, 2026
150da6c
fix
DedSec256 Jan 10, 2026
57f0498
fix
DedSec256 Jan 10, 2026
c5113b7
wip
DedSec256 Jan 11, 2026
0cb74b3
wip
DedSec256 Jan 11, 2026
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 @@ -4,7 +4,7 @@
using HwProj.AuthService.Client;
using HwProj.ContentService.Client;
using HwProj.Models.ContentService.DTO;
using HwProj.Models.Roles;
using HwProj.Models.CourseUnitType;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -13,72 +13,79 @@ namespace HwProj.APIGateway.API.Controllers;
[Route("api/[controller]")]
[Authorize]
[ApiController]
public class FilesController : AggregationController
public class FilesController(
IAuthServiceClient authServiceClient,
IContentServiceClient contentServiceClient,
FilesPrivacyFilter privacyFilter,
FilesCountLimiter filesCountLimiter)
: AggregationController(authServiceClient)
{
private readonly IContentServiceClient _contentServiceClient;

public FilesController(IAuthServiceClient authServiceClient,
IContentServiceClient contentServiceClient) : base(authServiceClient)
{
_contentServiceClient = contentServiceClient;
}

[HttpPost("process")]
[Authorize(Roles = Roles.LecturerRole)]
[ServiceFilter(typeof(CourseMentorOnlyAttribute))]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> Process([FromForm] ProcessFilesDTO processFilesDto)
{
var result = await _contentServiceClient.ProcessFilesAsync(processFilesDto);
var checkRights = await privacyFilter.CheckUploadRights(UserId, processFilesDto.FilesScope);
if (!checkRights) return Forbid("Недостаточно прав для загрузки файлов");

var checkCountLimit = await filesCountLimiter.CheckCountLimit(processFilesDto);
if (!checkCountLimit)
return Forbid("Слишком много файлов в решении." +
$"Максимальное количество файлов - ${FilesCountLimiter.MaxSolutionFiles}");

var result = await contentServiceClient.ProcessFilesAsync(processFilesDto);
return result.Succeeded
? Ok()
: StatusCode((int)HttpStatusCode.ServiceUnavailable, result.Errors);
: BadRequest(result.Errors);
}

[HttpPost("statuses")]
[Authorize(Roles = Roles.LecturerRole)]
[ServiceFilter(typeof(CourseMentorOnlyAttribute))]
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
[ProducesResponseType(typeof(FileInfoDTO[]), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> GetStatuses(ScopeDTO filesScope)
{
var filesStatusesResult = await _contentServiceClient.GetFilesStatuses(filesScope);
return filesStatusesResult.Succeeded
? Ok(filesStatusesResult.Value) as IActionResult
: StatusCode((int)HttpStatusCode.ServiceUnavailable, filesStatusesResult.Errors);
var checkRights = await privacyFilter.CheckUploadRights(UserId, filesScope);
if (!checkRights) return Forbid("Недостаточно прав для получения информации о файлах");

var result = await contentServiceClient.GetFilesStatuses(filesScope);
return result.Succeeded
? Ok(result.Value)
: BadRequest(result.Errors);
}

[HttpGet("downloadLink")]
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.NotFound)]
public async Task<IActionResult> GetDownloadLink([FromQuery] long fileId)
{
var result = await _contentServiceClient.GetDownloadLinkAsync(fileId);
return result.Succeeded
? Ok(result.Value)
: NotFound(result.Errors);
}
var linkDto = await contentServiceClient.GetDownloadLinkAsync(fileId);
if (linkDto.Succeeded) return BadRequest(linkDto.Errors);

[HttpGet("info/course/{courseId}")]
[ProducesResponseType(typeof(FileInfoDTO[]), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
public async Task<IActionResult> GetFilesInfo(long courseId)
{
var filesInfoResult = await _contentServiceClient.GetFilesInfo(courseId);
return filesInfoResult.Succeeded
? Ok(filesInfoResult.Value) as IActionResult
: StatusCode((int)HttpStatusCode.ServiceUnavailable, filesInfoResult.Errors);
var result = linkDto.Value;
var userId = UserId;

foreach (var scope in result.FileScopes)
{
if (await privacyFilter.CheckDownloadRights(userId, scope))
return Ok(result.DownloadUrl);
}

return Forbid("Недостаточно прав для получения ссылки на файл");
}

[HttpGet("info/course/{courseId}/uploaded")]
[HttpGet("info/course/{courseId}")]
[ProducesResponseType(typeof(FileInfoDTO[]), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.ServiceUnavailable)]
public async Task<IActionResult> GetUploadedFilesInfo(long courseId)
[ProducesResponseType(typeof(string[]), (int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> GetFilesInfo(long courseId,
[FromQuery] bool uploadedOnly = true,
[FromQuery] string courseUnitType = CourseUnitType.Homework)
{
var filesInfoResult = await _contentServiceClient.GetUploadedFilesInfo(courseId);
var filesInfoResult = await contentServiceClient.GetFilesInfo(courseId, uploadedOnly, courseUnitType);
return filesInfoResult.Succeeded
? Ok(filesInfoResult.Value) as IActionResult
: StatusCode((int)HttpStatusCode.ServiceUnavailable, filesInfoResult.Errors);
? Ok(filesInfoResult.Value)
: BadRequest(filesInfoResult.Errors);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Linq;
using System.Threading.Tasks;
using HwProj.ContentService.Client;
using HwProj.Models.ContentService.DTO;
using HwProj.Models.CourseUnitType;

namespace HwProj.APIGateway.API.Filters;

public class FilesCountLimiter(IContentServiceClient contentServiceClient)
{
public const long MaxSolutionFiles = 5;

public async Task<bool> CheckCountLimit(ProcessFilesDTO processFilesDto)
{
if (processFilesDto.FilesScope.CourseUnitType == CourseUnitType.Homework) return true;

var existingStatuses = await contentServiceClient.GetFilesStatuses(processFilesDto.FilesScope);
if (!existingStatuses.Succeeded) return false;

var existingIds = existingStatuses.Value.Select(f => f.Id).ToList();
if (processFilesDto.DeletingFileIds.Any(id => !existingIds.Contains(id)))
return false;

return existingIds.Count + processFilesDto.NewFiles.Count - processFilesDto.DeletingFileIds.Count <=
MaxSolutionFiles;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using HwProj.CoursesService.Client;
using HwProj.Models.ContentService.DTO;
using HwProj.Models.CourseUnitType;
using HwProj.SolutionsService.Client;

namespace HwProj.APIGateway.API.Filters;

public class FilesPrivacyFilter(
ICoursesServiceClient coursesServiceClient,
ISolutionsServiceClient solutionsServiceClient)
{
private async Task<HashSet<string>> GetSolutionStudentIds(long solutionId)
{
var studentIds = new HashSet<string>();
var solution = await solutionsServiceClient.GetSolutionById(solutionId);
studentIds.Add(solution.StudentId);

if (solution.GroupId is { } groupId)
{
var groups = await coursesServiceClient.GetGroupsById(groupId);
if (groups is [var group]) studentIds.UnionWith(group.StudentsIds.ToHashSet());
}

return studentIds;
}

public async Task<bool> CheckDownloadRights(string? userId, ScopeDTO fileScope)
{
if (userId == null) return false;

switch (fileScope.CourseUnitType)
{
case CourseUnitType.Homework:
return true;
case CourseUnitType.Solution:
{
var studentIds = await GetSolutionStudentIds(fileScope.CourseUnitId);
if (studentIds.Contains(userId)) return true;

var mentorIds = await coursesServiceClient.GetCourseLecturersIds(fileScope.CourseId);
return mentorIds.Contains(userId);
}
default:
return false;
}
}

public async Task<bool> CheckUploadRights(string? userId, ScopeDTO fileScope)
{
if (userId == null) return false;

switch (fileScope.CourseUnitType)
{
case CourseUnitType.Homework:
{
var mentorIds = await coursesServiceClient.GetCourseLecturersIds(fileScope.CourseId);
return mentorIds.Contains(userId);
}
case CourseUnitType.Solution:
{
var studentIds = await GetSolutionStudentIds(fileScope.CourseUnitId);
return studentIds.Contains(userId);
}
default:
return false;
}
}
}
2 changes: 2 additions & 0 deletions HwProj.APIGateway/HwProj.APIGateway.API/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddContentServiceClient();

services.AddScoped<CourseMentorOnlyAttribute>();
services.AddScoped<FilesPrivacyFilter>();
services.AddScoped<FilesCountLimiter>();
}

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

namespace HwProj.Models.ContentService.Attributes
{
[AttributeUsage(AttributeTargets.Property)]
public class CorrectFileTypeAttribute : FileValidationAttribute
{
private static readonly HashSet<FileType> ForbiddenFileTypes = new HashSet<FileType>
{
new MachO(), new Executable(), new ExecutableAndLinkableFormat()
};

protected override ValidationResult Validate(IFormFile file)
{
try
{
using var fileContent = file.OpenReadStream();
//FileTypeValidator.RegisterCustomTypes(typeof(MachO).Assembly);
if ( //!FileTypeValidator.IsTypeRecognizable(fileContent) ||
ForbiddenFileTypes.Any(type => type.DoesMatchWith(fileContent)))
{
return new ValidationResult(
$"Файл `{file.FileName}` имеет недопустимый тип ${file.ContentType}");
}
}
catch
{
return new ValidationResult(
$"Невозможно прочитать файл `{file.FileName}`");
}

return ValidationResult.Success;
}

private class MachO : FileType
{
private const string TypeName = "MacOS executable";
private const string TypeExtension = "macho";

private static readonly byte[][] MagicBytes =
{
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
};

public MachO() : base(TypeName, TypeExtension, MagicBytes)
{
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNetCore.Http;

namespace HwProj.Models.ContentService.Attributes
{
public abstract class FileValidationAttribute : ValidationAttribute
{
protected abstract ValidationResult Validate(IFormFile file);

protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) =>
value switch
{
IFormFile singleFile => Validate(singleFile),
IEnumerable<IFormFile> files => files
.Select(Validate)
.FirstOrDefault(x => x != ValidationResult.Success) ?? ValidationResult.Success,
_ => null
};
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;

namespace HwProj.Models.ContentService.Attributes
{
[AttributeUsage(AttributeTargets.Property)]
public class MaxFileSizeAttribute : ValidationAttribute
public class MaxFileSizeAttribute : FileValidationAttribute
{
private readonly long _maxFileSizeInBytes;

public MaxFileSizeAttribute(long maxFileSizeInBytes)
=>_maxFileSizeInBytes = maxFileSizeInBytes;
=> _maxFileSizeInBytes = maxFileSizeInBytes;

protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
protected override ValidationResult Validate(IFormFile file)
{
var files = value switch
{
IFormFile singleFile => new[] { singleFile },
IEnumerable<IFormFile> filesCollection => filesCollection,
_ => null
};
if (file.Length > _maxFileSizeInBytes)
return new ValidationResult(
$"Файл `{file.FileName}` превышает лимит в {_maxFileSizeInBytes / 1024 / 1024} MB");

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

foreach (var file in files)
if (file.Length > _maxFileSizeInBytes)
return new ValidationResult(
$"Файл `{file.FileName}` превышает лимит в {_maxFileSizeInBytes / 1024 / 1024} MB");

return ValidationResult.Success;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace HwProj.Models.CourseUnitType
{
public static class CourseUnitType
{
public const string Homework = "Homework";
public const string Solution = "Solution";
public const string Task = "Task";
};
};
11 changes: 11 additions & 0 deletions HwProj.Common/HwProj.Models/ContentService/DTO/FileLinkDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;

namespace HwProj.Models.ContentService.DTO
{

public class FileLinkDTO
{
public string DownloadUrl { get; set; }
public List<ScopeDTO> FileScopes { get; set; }
}
}
Loading
Loading