Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
058e469
add: file uploader element to add solution page
semrosin Sep 29, 2025
71a99a3
add: files count validation
semrosin Sep 30, 2025
1db78b9
add: file type validation
semrosin Sep 30, 2025
8b6bd02
fix: files count validation
semrosin Oct 2, 2025
008c1c7
fix: default lastSolution id
semrosin Oct 6, 2025
a52cff2
feat: files processing in taskSolutionPage
semrosin Oct 6, 2025
381d83f
feat: files processing in studentSolutionPage
semrosin Oct 6, 2025
9b0f97c
feat: make course files state universal
semrosin Oct 6, 2025
5cb127c
feat: add props in task solutions component
semrosin Oct 6, 2025
d6b38db
feat: get files info for solutions in converter
semrosin Oct 6, 2025
7b83a51
feat: files preview in solution component
semrosin Oct 6, 2025
c3b3fa0
feat: processing files after adding solution
semrosin Oct 6, 2025
d472ea5
fix: add solution imports
semrosin Oct 6, 2025
99200c1
feat: make edit files intarface exporting
semrosin Oct 15, 2025
beb168b
fix: start processing after adding solution
semrosin Oct 19, 2025
d982f1a
feat: add solution privacy attribute
semrosin Oct 19, 2025
924686a
feat: add privacy attribute for processing
semrosin Oct 19, 2025
2beb4a3
feat: add attributes to startup
semrosin Oct 19, 2025
dff6ced
feat: add lecturer or student role
semrosin Oct 19, 2025
eb96d7b
feat: change processing validation (back)
semrosin Oct 19, 2025
e8ebd03
feat: change get statuses validation (back)
semrosin Oct 19, 2025
1b2822e
feat: change download link validation (back)
semrosin Oct 19, 2025
e080e83
feat: add scope dto with file id
semrosin Oct 19, 2025
c2c010a
feat: change download link api call (front)
semrosin Oct 19, 2025
638111a
fix: files preview without comment
semrosin Oct 19, 2025
30b9a9f
feat: file type validation (back)
semrosin Oct 20, 2025
cc7d57d
fix: front file type validation
semrosin Oct 20, 2025
52a5540
feat: add files access for groups
semrosin Oct 20, 2025
9558d32
refactor: make studentIds HashSet
semrosin Oct 23, 2025
57e0367
feat: process files for groupmates
semrosin Oct 23, 2025
cf74b78
fix: dispose stream in back type validation
semrosin Oct 25, 2025
14fff3b
feat: separate access files functionality
semrosin Oct 25, 2025
0138af5
fix: show solution files uploading status for students only
semrosin Oct 25, 2025
9d7a40e
refactor: delete unused function in files accessor
semrosin Oct 25, 2025
2e3361c
fix: intervalRef usage in files accessor
semrosin Oct 25, 2025
771028d
fix: subscribe updating course files on course id
semrosin Oct 25, 2025
874d9c4
feat: update solutions components for files accessor
semrosin Oct 25, 2025
c26917f
fix: padding after solution files
semrosin Oct 26, 2025
8a596bc
fix: return alien code
semrosin Oct 26, 2025
e47995b
refactor: deleteunused variables, await with async calls
semrosin Oct 28, 2025
e682bf8
feat: [back] add class for privacy validation
semrosin Nov 19, 2025
04a517a
feat [back]: method to get file scope
semrosin Nov 19, 2025
5d28775
fix: delete attribute validation
semrosin Nov 19, 2025
f407073
fix: delete unused role
semrosin Nov 19, 2025
665de62
feat [back]: method to get files scope in info service
semrosin Nov 19, 2025
4afe55b
refactor [back]: return dto from files controller download link
semrosin Nov 19, 2025
0c3937e
refactor [back]: return dto from content client download link
semrosin Nov 19, 2025
c289e53
feat [back]: add privacy filter to start up
semrosin Nov 19, 2025
bb6ca08
feat [back]: file link dto
semrosin Nov 19, 2025
a420e3b
fix: download link request
semrosin Nov 19, 2025
d663e79
feat [back]: privacy validation
semrosin Nov 19, 2025
c208f59
refactor: delete unused validation attributes
semrosin Nov 19, 2025
79d5765
refactor [front]: rename files upload waiter
semrosin Nov 19, 2025
74a7d06
feat [front]: variability for max files count
semrosin Nov 19, 2025
f0d3c7c
refactor [front]: course files access to upload waiter
semrosin Nov 19, 2025
c8dfd59
fix [front]: rename usage of upload waiter
semrosin Nov 19, 2025
d848b5e
fix [front]: rename usages of download link getter
semrosin Nov 19, 2025
f9080b4
refactor [front]: unify unit files info getter
semrosin Nov 19, 2025
2e40268
feat [front]: delete files saved status text
semrosin Nov 19, 2025
cb8788d
refactor [front]: delete unused files info array
semrosin Nov 19, 2025
6600ffc
refactor [front]: separate files handle logic
semrosin Nov 19, 2025
0885997
refactor [back]: type validation by foreign library
semrosin Nov 20, 2025
556eb10
refactor [back]: scope usage in privacy filter
semrosin Nov 20, 2025
4169e4c
fix [back]: return with privacy error
semrosin Nov 21, 2025
8a38d56
feat [back]: add max files count filter
semrosin Nov 21, 2025
0b0fffb
fix [front]: max files count showing
semrosin Nov 22, 2025
4c1e950
fix [back]: add files count limit to start up
semrosin Nov 22, 2025
bd83fa9
feat [back]: showing max files count on limit exceeding
semrosin Nov 22, 2025
bef638b
refactor: separate methods in privacy filter
semrosin Dec 15, 2025
5c0188b
refactor: create courseUnitType constans
semrosin Dec 15, 2025
c13018f
fix: privacy filter
semrosin Dec 17, 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
@@ -1,10 +1,10 @@
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using HwProj.APIGateway.API.Filters;
using HwProj.AuthService.Client;
using HwProj.ContentService.Client;
using HwProj.Models.ContentService.DTO;
using HwProj.Models.Roles;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -16,48 +16,79 @@ namespace HwProj.APIGateway.API.Controllers;
public class FilesController : AggregationController
{
private readonly IContentServiceClient _contentServiceClient;
private readonly FilesPrivacyFilter _privacyFilter;
private readonly FilesCountLimit _countFilter;

public FilesController(IAuthServiceClient authServiceClient,
IContentServiceClient contentServiceClient) : base(authServiceClient)
IContentServiceClient contentServiceClient,
FilesPrivacyFilter privacyFilter, FilesCountLimit countFilter) : base(authServiceClient)
{
_contentServiceClient = contentServiceClient;
_privacyFilter = privacyFilter;
_countFilter = countFilter;
}

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

var checkCountLimit = await _countFilter.CheckCountLimit(processFilesDto);
if (!checkCountLimit) return StatusCode((int)HttpStatusCode.Forbidden, "Слишком много файлов в решении."
+ $"Максимальное количество файлов - ${_countFilter.maxSolutionFiles}");

var result = await _contentServiceClient.ProcessFilesAsync(processFilesDto);
return result.Succeeded
? Ok()
: StatusCode((int)HttpStatusCode.ServiceUnavailable, 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)]
public async Task<IActionResult> GetStatuses(ScopeDTO filesScope)
{
var checkRights = await _privacyFilter.CheckUploadRights(
User.Claims.FirstOrDefault(claim => claim.Type.ToString() == "_id")?.Value,
filesScope);
if (!checkRights) return Forbid("Недостаточно прав для получения информации о файлах");

var filesStatusesResult = await _contentServiceClient.GetFilesStatuses(filesScope);
return filesStatusesResult.Succeeded
? Ok(filesStatusesResult.Value) as IActionResult
: StatusCode((int)HttpStatusCode.ServiceUnavailable, filesStatusesResult.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 StatusCode((int)HttpStatusCode.ServiceUnavailable, linkDto.Errors);

var userId = User.Claims.FirstOrDefault(claim => claim.Type.ToString() == "_id")?.Value;
var hasRights = false;
foreach (var scope in linkDto.Value.fileScopes)
{
if (await _privacyFilter.CheckDownloadRights(userId, scope))
{
hasRights = true;
break;
}
}

return hasRights
? Ok(linkDto.Value.DownloadUrl)
: Forbid("Недостаточно прав для получения ссылки на файл");
}

[HttpGet("info/course/{courseId}")]
Expand Down
39 changes: 39 additions & 0 deletions HwProj.APIGateway/HwProj.APIGateway.API/Filters/FilesCountLimit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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 FilesCountLimit
{
private readonly IContentServiceClient _contentServiceClient;
public readonly long maxSolutionFiles = 5;

public FilesCountLimit(IContentServiceClient contentServiceClient)
{
_contentServiceClient = contentServiceClient;
}

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);
if (processFilesDto.DeletingFileIds.Any(id => !existingIds.Contains(id)))
{
return false;
}

if (existingIds.Count() + processFilesDto.NewFiles.Count - processFilesDto.DeletingFileIds.Count > maxSolutionFiles)
{
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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
{
private readonly ICoursesServiceClient _coursesServiceClient;
private readonly ISolutionsServiceClient _solutionsServiceClient;

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

public async Task<bool> CheckDownloadRights(string? userId, ScopeDTO fileScope)
{
if (fileScope.CourseUnitType == CourseUnitType.Homework) return true;
if (fileScope.CourseUnitType == CourseUnitType.Solution)
{
if (userId == null) return false;
var studentIds = new HashSet<string>();
var solution = await _solutionsServiceClient.GetSolutionById(fileScope.CourseUnitId);
studentIds.Add(solution.StudentId);
var groupIds = await _coursesServiceClient.GetGroupsById(solution.GroupId ?? 0);
studentIds.UnionWith(groupIds.FirstOrDefault()?.StudentsIds.ToHashSet() ?? new());

var mentorIds = await _coursesServiceClient.GetCourseLecturersIds(fileScope.CourseId);

if (!studentIds.Contains(userId) && !mentorIds.Contains(userId)) return false;

return true;
}

return false;
}

public async Task<bool> CheckUploadRights(string? userId, ScopeDTO fileScope)
{
if (userId == null) return false;
if (fileScope.CourseUnitType == CourseUnitType.Homework)
{
var mentorIds = await _coursesServiceClient.GetCourseLecturersIds(fileScope.CourseId);
if (!mentorIds.Contains(userId)) return false;
return true;
}
if (fileScope.CourseUnitType == CourseUnitType.Solution)
{
var studentIds = new HashSet<string>();
var solution = await _solutionsServiceClient.GetSolutionById(fileScope.CourseUnitId);
studentIds.Add(solution.StudentId);
var group = await _coursesServiceClient.GetGroupsById(solution.GroupId ?? 0);
studentIds.UnionWith(group.FirstOrDefault()?.StudentsIds.ToHashSet() ?? new());

if (!studentIds.Contains(userId)) return false;

return true;
}

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<FilesCountLimit>();
}

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

namespace HwProj.Models.ContentService.Attributes
{

public class MachO : FileType
{
public const string TypeName = "MacOS executable";
public 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)
{
}
}

[AttributeUsage(AttributeTargets.Property)]
public class CorrectFileTypeAttribute : ValidationAttribute
{
private static readonly HashSet<FileType> forbiddenFileTypes = new HashSet<FileType>{
new MachO(), new Executable(), new ExecutableAndLinkableFormat()
};

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;

FileTypeValidator.RegisterCustomTypes(typeof(MachO).Assembly);
foreach (var file in files)
{
try
{
using (var fileContent = file.OpenReadStream())
{
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;
}
}
}
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; }
}
}
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/HwProj.Models.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="File.TypeChecker" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,25 @@ public async Task<IActionResult> GetStatuses(ScopeDTO scopeDto)
}

[HttpGet("downloadLink")]
[ProducesResponseType(typeof(Result<string>), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(FileLinkDTO[]), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetDownloadLink([FromQuery] long fileId)
{
var externalKey = await _filesInfoService.GetFileExternalKeyAsync(fileId);
if (externalKey is null) return Ok(Result<string>.Failed("Файл не найден"));
if (externalKey is null) return Ok(Result<FileLinkDTO>.Failed("Файл не найден"));

var downloadUrlResult = await _s3FilesService.GetDownloadUrl(externalKey);
return Ok(downloadUrlResult);
var fileScopes = await _filesInfoService.GetFileScopesAsync(fileId);
if (fileScopes is null) return Ok(Result<FileLinkDTO>.Failed("Файл не найден"));

var downloadUrl = await _s3FilesService.GetDownloadUrl(externalKey);
if (!downloadUrl.Succeeded) return Ok(Result<FileLinkDTO>.Failed(downloadUrl.Errors));

var result = new FileLinkDTO
{
DownloadUrl = downloadUrl.Value,
fileScopes = fileScopes.Select(fs => fs.ToScopeDTO()).ToList()
};

return Ok(result);
}

[HttpGet("info/course/{courseId}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ public async Task UpdateStatusAsync(List<long> fileRecordIds, FileStatus newStat
=> await _contentContext.FileRecords
.AsNoTracking()
.SingleOrDefaultAsync(fr => fr.Id == fileRecordId);

public async Task<List<Scope>?> GetScopesAsync(long fileRecordId)
=> await _contentContext.FileToCourseUnits
.AsNoTracking()
.Where(fr => fr.FileRecordId == fileRecordId)
.Select(fc => fc.ToScope())
.ToListAsync();

public async Task<List<FileRecord>> GetByScopeAsync(Scope scope)
=> await _contentContext.FileToCourseUnits
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public interface IFileRecordRepository
public Task UpdateAsync(long id,
Expression<Func<SetPropertyCalls<FileRecord>, SetPropertyCalls<FileRecord>>> setPropertyCalls);
public Task<FileRecord?> GetFileRecordByIdAsync(long fileRecordId);
public Task<List<Scope>?> GetScopesAsync(long fileRecordId);
public Task<List<FileRecord>> GetByScopeAsync(Scope scope);
public Task<List<FileToCourseUnit>> GetByCourseIdAsync(long courseId);
public Task<List<FileToCourseUnit>> GetByCourseIdAndStatusAsync(long courseId, FileStatus filesStatus);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ public async Task<List<FileInfoDTO>> GetFilesStatusesAsync(Scope filesScope)
var fileRecord = await _fileRecordRepository.GetFileRecordByIdAsync(fileId);
return fileRecord?.ExternalKey;
}

public async Task<List<Scope>?> GetFileScopesAsync(long fileId)
{
var fileToCourseUnit = await _fileRecordRepository.GetScopesAsync(fileId);
return fileToCourseUnit;
}

public async Task<List<FileInfoDTO>> GetFilesInfoAsync(long courseId)
{
Expand Down
Loading
Loading