diff --git a/.gitignore b/.gitignore index 4d9a82f..00cc8ac 100644 --- a/.gitignore +++ b/.gitignore @@ -491,3 +491,5 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +PaydayBackend/ApiDocumentation.xml diff --git a/.idea/config/applicationhost.config b/.idea/config/applicationhost.config new file mode 100644 index 0000000..4b06913 --- /dev/null +++ b/.idea/config/applicationhost.config @@ -0,0 +1,997 @@ + + + + + + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PaydayBackend/Controllers/AdminController.cs b/PaydayBackend/Controllers/AdminController.cs new file mode 100644 index 0000000..c3c03c8 --- /dev/null +++ b/PaydayBackend/Controllers/AdminController.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Mvc; +using PaydayBackend.Models; +using PaydayBackend.Services; + +namespace PaydayBackend.Controllers; + +[Route("v1/admin")] +[ApiController] +public class AdminController : ControllerBase +{ + private readonly IAdminService _adminService; + + public AdminController(IAdminService adminService) + { + _adminService = adminService; + } + + // -------------------------------------| Банки |------------------------------------- + + /// + /// Добавление банка + /// + [HttpPost("banks")] + public async Task AddBank([FromBody] Bank bank) + { + await _adminService.AddBank(bank); + return Ok(); + } + + /// + /// Получение всех условий одного банка по id + /// + /// Банк не найден + [HttpGet("banks")] + public async Task GetAllLoanTermsByBankId(long bankId) + { + var result = await _adminService.GetAllLoanTermsByBankId(bankId); + if (result == null) + return BadRequest(); + + return Ok(result); + } + + // TODO: Переделать + /// + /// Добавление условия кредитования + /// + // [HttpPost("banks/{bank_id}/loanterms")] + // public async Task AddLoanTerm(long bank_id, [FromBody] LoanTerm loanTerm) + // { + // await _adminService.AddLoanTerm(loanTerm); + // return Ok(); + // } + + /// + /// Удаление ВСЕХ условий кредитования + /// + /// Банк не найден + [HttpGet("banks/{bank_id}/loanterms")] + public async Task AddLoanTerm(long bankId) + { + var result = await _adminService.RemoveAllLoanTermsByBankId(bankId); + return result == "OK" ? Ok() : BadRequest(result); + } + + // -------------------------------------| Университеты |------------------------------------- + + /// + /// Добавление университета + /// + [HttpPost("universities")] + public async Task AddUniversity([FromBody] University university) + { + await _adminService.AddUniversity(university); + return Ok(); + } + + // TODO: Переделать + /// + /// Добавление направления университета + /// + [HttpPost("universities/directions")] + public async Task AddUniversityDirection([FromBody] UniversityDirection universityDirection) + { + await _adminService.AddUniversityDirection(universityDirection); + return Ok(); + } + + +} \ No newline at end of file diff --git a/PaydayBackend/Controllers/PublicController.cs b/PaydayBackend/Controllers/PublicController.cs new file mode 100644 index 0000000..2fb14ea --- /dev/null +++ b/PaydayBackend/Controllers/PublicController.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Mvc; +using PaydayBackend.Models; +using PaydayBackend.Services; + +namespace PaydayBackend.Controllers; + +[Route("v1/public")] +[ApiController] +public class PublicController : ControllerBase +{ + private readonly IPublicService _publicService; + + public PublicController(IPublicService publicService) + { + _publicService = publicService; + } + + /// + /// Получение всех университетов + /// + [HttpGet("university")] + public async Task> GetAllUniversity() + { + return await _publicService.GetAllUniversity(); + } + + /// + /// Получение всех направлений университета + /// + /// Университет не найден + [HttpGet("university/{id}/direction")] + public async Task>> GetAllUniversityDirectionByUniversityId(long id) + { + var result = await _publicService.GetAllUniversityDirectionByUniversityId(id); + + if (result == null) + return BadRequest(); + + return Ok(result); + } +} \ No newline at end of file diff --git a/PaydayBackend/DatabaseContext.cs b/PaydayBackend/DatabaseContext.cs new file mode 100644 index 0000000..8099948 --- /dev/null +++ b/PaydayBackend/DatabaseContext.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; +using PaydayBackend.Models; + +namespace PaydayBackend; + +public class DatabaseContext : DbContext +{ + public DbSet Banks { get; set; } = null!; + public DbSet LoanTerms { get; set; } = null!; + public DbSet Universities { get; set; } = null!; + public DbSet UniversityDirections { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + + } + + public DatabaseContext(DbContextOptions options) : base(options) { } +} \ No newline at end of file diff --git a/PaydayBackend/Models/Bank.cs b/PaydayBackend/Models/Bank.cs new file mode 100644 index 0000000..7465bc4 --- /dev/null +++ b/PaydayBackend/Models/Bank.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace PaydayBackend.Models; + +public class Bank +{ + [Key] + public long Id { get; set; } + public string Name { get; set; } + public string ImageUrl { get; set; } +} \ No newline at end of file diff --git a/PaydayBackend/Models/LoanTerm.cs b/PaydayBackend/Models/LoanTerm.cs new file mode 100644 index 0000000..7c27c9d --- /dev/null +++ b/PaydayBackend/Models/LoanTerm.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace PaydayBackend.Models; + +public class LoanTerm +{ + [Key] + public long Id { get; set; } + public Bank Bank { get; set; } + [Range(0f, 360f)] + public float InterestRate { get; set; } + public int LoanTermInMonths { get; set; } + public string Documents { get; set; } + +} \ No newline at end of file diff --git a/PaydayBackend/Models/University.cs b/PaydayBackend/Models/University.cs new file mode 100644 index 0000000..c99ae1b --- /dev/null +++ b/PaydayBackend/Models/University.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace PaydayBackend.Models; + +public class University +{ + [Key] + public long Id { get; set; } + public string Name { get; set; } + public string FullName { get; set; } + public string ImageUrl { get; set; } +} \ No newline at end of file diff --git a/PaydayBackend/Models/UniversityDirection.cs b/PaydayBackend/Models/UniversityDirection.cs new file mode 100644 index 0000000..7137a26 --- /dev/null +++ b/PaydayBackend/Models/UniversityDirection.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace PaydayBackend.Models; + +public class UniversityDirection +{ + [Key] + public long UniversityId { get; set; } + public string Code { get; set; } + public string Name { get; set; } + public int BudgetPlaces { get; set; } +} \ No newline at end of file diff --git a/PaydayBackend/PaydayBackend.csproj b/PaydayBackend/PaydayBackend.csproj index 615a08c..1c6a0d5 100644 --- a/PaydayBackend/PaydayBackend.csproj +++ b/PaydayBackend/PaydayBackend.csproj @@ -7,8 +7,24 @@ Linux + + ApiDocumentation.xml + + + + ApiDocumentation.xml + 1701;1702;IL2121,1591 + + + + + + + + + @@ -18,8 +34,4 @@ - - - - diff --git a/PaydayBackend/Program.cs b/PaydayBackend/Program.cs index 8264bac..7f1e4ad 100644 --- a/PaydayBackend/Program.cs +++ b/PaydayBackend/Program.cs @@ -1,24 +1,75 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.OpenApi.Models; +using Minio; +using Minio.AspNetCore; +using Minio.AspNetCore.HealthChecks; +using PaydayBackend; +using PaydayBackend.Services; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. +string GetEnv(string envName, string settingsName = "") +{ + string? dbConString = builder.Configuration.GetConnectionString(settingsName) ?? Environment.GetEnvironmentVariable(envName); + if (!string.IsNullOrEmpty(dbConString)) + return dbConString; + + Console.WriteLine($"Environment variable {envName} not found."); + return String.Empty; +} builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "ApiDocumentation.xml")); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Backend", Version = "v1" }); +}); + +// Database +string? dbConString = GetEnv("CONNECTION_STRING", "DefaultConnection"); + +builder.Services.AddDbContext(options => + { options.UseNpgsql(dbConString); }); + +// HealthChecks +builder.Services.AddHealthChecks() + .AddNpgSql(dbConString) + .AddMinio(failureStatus: HealthStatus.Degraded, factory: sp => sp.GetRequiredService()); + +// Services +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Minio +builder.Services.AddMinio(options => +{ + options.Endpoint = GetEnv("S3_ENDPOINT", "S3Endpoint"); + options.AccessKey = GetEnv("S3_ACCESS_KEY", "S3AccessKey"); + options.SecretKey = GetEnv("S3_SECRET_KEY", "S3SecretKey"); + + options.ConfigureClient(client => + { + client.WithSSL(false); + }); +}); var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ +// if (app.Environment.IsDevelopment()) +// { app.UseSwagger(); app.UseSwaggerUI(); -} +// } -app.UseHttpsRedirection(); +app.MapHealthChecks("/health"); +// app.UseHttpsRedirection(); -app.UseAuthorization(); +// app.UseAuthorization(); app.MapControllers(); diff --git a/PaydayBackend/Services/AdminService.cs b/PaydayBackend/Services/AdminService.cs new file mode 100644 index 0000000..37c4909 --- /dev/null +++ b/PaydayBackend/Services/AdminService.cs @@ -0,0 +1,85 @@ +using Microsoft.EntityFrameworkCore; +using PaydayBackend.Models; + +namespace PaydayBackend.Services; + +public interface IAdminService +{ + public Task AddBank(Bank bank); + public Task> GetAllBanks(); + public Task AddLoanTerm(LoanTerm loanTerm); + public Task RemoveAllLoanTermsByBankId(long bankId); + public Task?> GetAllLoanTermsByBankId(long bankId); + public Task AddUniversity(University university); + public Task AddUniversityDirection(UniversityDirection universityDirection); +} + +public class AdminService : IAdminService +{ + private DatabaseContext _databaseContext; + + private async Task BankIsExist(long bankId) + { + if (await _databaseContext.Banks.Where(x => x.Id == bankId).FirstOrDefaultAsync() == null) + return false; + + return true; + } + + public AdminService(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + } + + // -------------------------------------| Банки |------------------------------------- + + public async Task AddBank(Bank bank) + { + await _databaseContext.Banks.AddAsync(bank); + await _databaseContext.SaveChangesAsync(); + } + + public async Task> GetAllBanks() + { + return await _databaseContext.Banks.ToListAsync(); + } + + public async Task AddLoanTerm(LoanTerm loanTerm) + { + await _databaseContext.LoanTerms.AddAsync(loanTerm); + await _databaseContext.SaveChangesAsync(); + } + + public async Task RemoveAllLoanTermsByBankId(long bankId) + { + if (await BankIsExist(bankId)) + return "Bank not found"; + + var result = await _databaseContext.LoanTerms.Where(x => x.Bank.Id == bankId).ToListAsync(); + _databaseContext.LoanTerms.RemoveRange(result); + + return "OK"; + } + + public async Task?> GetAllLoanTermsByBankId(long bankId) + { + if (await BankIsExist(bankId)) + return null; + + return await _databaseContext.LoanTerms.Where(x => x.Bank.Id == bankId).ToListAsync(); + } + + // -------------------------------------| Университеты |------------------------------------- + + public async Task AddUniversity(University university) + { + await _databaseContext.Universities.AddAsync(university); + await _databaseContext.SaveChangesAsync(); + } + + public async Task AddUniversityDirection(UniversityDirection universityDirection) + { + await _databaseContext.UniversityDirections.AddAsync(universityDirection); + await _databaseContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/PaydayBackend/Services/IStorageService.cs b/PaydayBackend/Services/IStorageService.cs new file mode 100644 index 0000000..241f285 --- /dev/null +++ b/PaydayBackend/Services/IStorageService.cs @@ -0,0 +1,124 @@ +using Minio; +using Minio.AspNetCore; +using Minio.Exceptions; +using PaydayBackend.Utils; + +namespace PaydayBackend.Services; + +public interface IStorageService +{ + public Task UploadFile(MemoryStream fileStream, string bucket, string fileName); + public Task GetFile(string bucket, string fileName); + public Task RemoveFile(string bucket, string fileName); + public Task IsFileExist(string bucket, string fileName); +} + +public class StorageService : IStorageService +{ + private MinioClient _minio; + + public StorageService(IMinioClientFactory minioClientFactory) + { + _minio = minioClientFactory.CreateClient(); + } + + public async Task GetFile(string bucket, string fileName) + { + if (!await IsFileExist(bucket, fileName)) + return null; + + try + { + var outStream = new MemoryStream(); + GetObjectArgs getObjectArgs = new GetObjectArgs() + .WithBucket(bucket) + .WithObject(fileName) + .WithCallbackStream((stream) => + { + stream.CopyTo(outStream); + outStream.Position = 0; + stream.Close(); + }); + await _minio.GetObjectAsync(getObjectArgs); + + return outStream; + } + catch (MinioException e) + { + Console.WriteLine("File upload error: {0}", e.Message); + return null; + } + } + + public async Task RemoveFile(string bucket, string fileName) + { + try + { + RemoveObjectArgs rmArgs = new RemoveObjectArgs() + .WithBucket(bucket) + .WithObject(fileName); + await _minio.RemoveObjectAsync(rmArgs); + Console.WriteLine($"File \"{fileName}\" removed successfully"); + return true; + } + catch (MinioException e) + { + Console.WriteLine($"File \"{fileName}\" remove error: " + e); + return false; + } + } + + public async Task IsFileExist(string bucket, string fileName) + { + try + { + // Проверка, существует ли объект + // Если объект не найден, statObjectArgs генерирует исключение + StatObjectArgs statObjectArgs = new StatObjectArgs() + .WithBucket(bucket) + .WithObject(fileName); + await _minio.StatObjectAsync(statObjectArgs); + return true; + } + catch (MinioException e) + { + Console.WriteLine($"File \"{fileName}\" not exist: " + e); + return false; + } + } + + public async Task UploadFile(MemoryStream fileStream, string bucket, string fileName) + { + fileStream.Position = 0; + + try + { + // Создаём ведро если его нет + var beArgs = new BucketExistsArgs().WithBucket(bucket); + bool found = await _minio.BucketExistsAsync(beArgs).ConfigureAwait(false); + if (!found) + { + var mbArgs = new MakeBucketArgs().WithBucket(bucket); + await _minio.MakeBucketAsync(mbArgs).ConfigureAwait(false); + } + + // Загружаем файл + var putObjectArgs = new PutObjectArgs() + .WithBucket(bucket) + .WithObject(fileName) + .WithStreamData(fileStream) + .WithObjectSize(fileStream.Length) + .WithContentType(FileHelper.GetMimeType(fileName)); + await _minio.PutObjectAsync(putObjectArgs).ConfigureAwait(false); + Console.WriteLine("File uploaded successfully: " + fileName); + fileStream.Close(); + return true; + } + catch (MinioException e) + { + Console.WriteLine("File upload error: {0}", e.Message); + fileStream.Close(); + return false; + } + } +} \ No newline at end of file diff --git a/PaydayBackend/Services/PublicService.cs b/PaydayBackend/Services/PublicService.cs new file mode 100644 index 0000000..cfc8e99 --- /dev/null +++ b/PaydayBackend/Services/PublicService.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using PaydayBackend.Models; + +namespace PaydayBackend.Services; + +public interface IPublicService +{ + public Task> GetAllUniversity(); + public Task?> GetAllUniversityDirectionByUniversityId(long universityId); + // public Task GetAllLoansByDirectionCost(); +} + +public class PublicService : IPublicService +{ + private DatabaseContext _databaseContext; + + public PublicService(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + } + + public async Task> GetAllUniversity() + { + return await _databaseContext.Universities.ToListAsync(); + } + + public async Task?> GetAllUniversityDirectionByUniversityId(long universityId) + { + if (await _databaseContext.Universities.Where(x => x.Id == universityId).FirstOrDefaultAsync() == null) + return null; + + return await _databaseContext.UniversityDirections.Where(x => x.UniversityId == universityId).ToListAsync(); + } + +} \ No newline at end of file diff --git a/PaydayBackend/Utils/FileHelper.cs b/PaydayBackend/Utils/FileHelper.cs new file mode 100644 index 0000000..c2fdb4f --- /dev/null +++ b/PaydayBackend/Utils/FileHelper.cs @@ -0,0 +1,90 @@ +using SixLabors.ImageSharp.Formats.Jpeg; + +namespace PaydayBackend.Utils; + +public static class FileHelper +{ + public static string GetMimeType(string filename) + { + var extension = filename.Split(".").Last(); + switch (extension) + { + case "jpg": + case "jpeg": + return "image/jpeg"; + case "png": + return "image/png"; + case "csv": + return "text/csv"; + case "pdf": + return "application/pdf"; + case "html": + return "text/html"; + default: + throw new ArgumentException($"Unsupported file type, file: {filename}"); + } + } + + // Функция валидации размера файла + public static string ValidateMaxFileSize(IFormFile file, int maxSizeMb, string[] allowedExtensions) + { + if (file.Length > 0) + { + if (file.Length > maxSizeMb * 1024 * 1024) + return $"File need to be less than {maxSizeMb}MB."; + if (!allowedExtensions.Contains(Path.GetExtension(file.FileName).ToLower())) + return "Invalid file extension."; + + return "OK"; + } + + return "Invalid file."; + } + + // Функция валидации изображения + public static async Task ValidateImage(MemoryStream fileStream, string[]? allowedExtensions, int minWidth = 0, + int minHeight = 0, int maxWidth = 0, int maxHeight = 0) + { + if (allowedExtensions == null) + allowedExtensions = new[] {"JPG", "JPEG"}; + + try + { + var imageInfo = await Image.DetectFormatAsync(fileStream); + if (!allowedExtensions.Contains(imageInfo.Name)) + return "Image type is not" + allowedExtensions.Aggregate("", + (current, next) => current + " " + next) + "."; + + using var image = await Image.LoadAsync(fileStream); + if (image.Width < minWidth || image.Height < minHeight) + return $"Image size must be at least {minWidth}x{minHeight}."; + + if (maxHeight != 0 && maxWidth != 0) + if (image.Width > maxWidth || image.Height > maxHeight) + return $"Image size must be less than {maxWidth}x{maxHeight}."; + + return "OK"; + } + catch (ImageFormatException e) + { + Console.WriteLine(e.Message); + return "Invalid image type."; + } + } + + // Кропает изображение по центру, в зависимости от того, какая сторона меньше + public static async Task CropImage(MemoryStream fileStream) + { + fileStream.Position = 0; + using var image = await Image.LoadAsync(fileStream); + Console.WriteLine(image.Height/2 - image.Width/2 + " " + image.Width + " " + image.Height); + if (image.Height > image.Width) + image.Mutate(i => i.Crop(new Rectangle(0, image.Height/2 - image.Width/2, image.Width, image.Width))); + else + image.Mutate(i => i.Crop(new Rectangle(image.Width/2 - image.Height/2, 0, image.Height, image.Height))); + + await fileStream.FlushAsync(); + fileStream.Position = 0; + await image.SaveAsync(fileStream, new JpegEncoder()); + } +} \ No newline at end of file