diff --git a/.env b/.env
new file mode 100644
index 0000000..f06ff63
--- /dev/null
+++ b/.env
@@ -0,0 +1,101 @@
+# The .env configuration file
+# Please DO NOT share this file, it contains confidential data.
+
+# All variables are specified according to this rule:
+# DESCRIPTION - information about what the variable is responsible for
+# TYPE - the type of the variable (string, boolean, etc.)
+# Any additional information
+# SOME_ENV_CODE=data - default data. If specified, then the variable is optional
+
+# General
+
+# The path to save the data.
+# string
+# (optional)
+# Saving logs (if the full path is not specified),
+# databases (if Sqlite) and other data that should be saved in a place other than the place where the program is launched.
+# REQUIRED if the application is inside the container
+# If you want to change this value, you need to change the values in Settings.json and move the file itself to the desired location.
+PATH_TO_SAVE=
+
+# Security
+
+# JWT signature token
+# string (UTF8)
+# This token will be used to create and verify the signature of JWT tokens.
+# The token must be equal to 64 characters
+SECURITY_SIGNING_TOKEN=
+
+# Token for JWT encryption
+# string (UTF8)
+# This token will be used to encrypt and decrypt JWT tokens.
+# The token must be equal to 32 characters
+SECURITY_ENCRYPTION_TOKEN=
+
+# Time in minutes, which indicates after which time the Refresh Token will become invalid
+# integer
+# The token indicates how long after the user is inactive, he will need to log in again
+SECURITY_LIFE_TIME_RT=1440
+
+# The time in a minute, which indicates that this is exactly what it takes to become a non-state
+# integer
+# Do not specify a time that is too long or too short. Optimally 5 > x > 60
+SECURITY_LIFE_TIME_JWT=15
+
+# Time in minutes, which indicates after which time the token of the first factor will become invalid
+# integer
+# Do not specify a short time. The user must be able to log in using the second factor
+SECURITY_LIFE_TIME_1_FA=15
+
+# An identifier that points to the server that created the token
+# string
+SECURITY_JWT_ISSUER=
+
+# ID of the audience for which the token is intended
+# string
+SECURITY_JWT_AUDIENCE=
+
+### Hashing
+
+# In order to set up hashing correctly, you need to start from the security requirements
+# You can use the settings that were used in https://github.com/P-H-C/phc-winner-argon2
+# These parameters have a STRONG impact on performance
+# When testing the system, these values were used:
+# 10 <= SECURITY_HASH_ITERATION <= 25 iterations
+# 16384 <= SECURITY_HASH_MEMORY <= 32768 KB
+# 4 <= SECURITY_HASH_PARALLELISM <= 8 lines
+# If we take all the large values, it will take a little more than 1 second to get the hash. If this time is critical, reduce the parameters
+
+# The number of iterations used to hash passwords in the Argon2 algorithm
+# integer
+# This parameter determines the number of iterations that the Argon2 algorithm goes through when hashing passwords.
+# Increasing this value can improve security by increasing the time it takes to calculate the password hash.
+# The average number of iterations to increase the security level should be set to at least 10.
+SECURITY_HASH_ITERATION=
+
+# The amount of memory used to hash passwords in the Argon2 algorithm
+# integer
+# 65536
+# This parameter determines the number of kilobytes of memory that will be used for the password hashing process.
+# Increasing this value may increase security, but it may also require more system resources.
+SECURITY_HASH_MEMORY=
+
+# Parallelism determines how many of the memory fragments divided into strips will be used to generate a hash
+# integer
+# This value affects the hash itself, but can be changed to achieve an ideal execution time, taking into account the processor and the number of cores.
+SECURITY_HASH_PARALLELISM=
+
+# The size of the output hash generated by the password hashing algorithm
+# integer
+SECURITY_HASH_SIZE=32
+
+# Additional protection for Argon2
+# string (BASE64)
+# (optional)
+# We recommend installing a token so that even if the data is compromised, an attacker cannot brute force a password without a token
+SECURITY_HASH_TOKEN=
+
+# The size of the salt used to hash passwords
+# integer
+# The salt is a random value added to the password before hashing to prevent the use of rainbow hash tables and other attacks.
+SECURITY_SALT_SIZE=16
\ No newline at end of file
diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml
index 49484d4..9d784b5 100644
--- a/.gitea/workflows/test.yaml
+++ b/.gitea/workflows/test.yaml
@@ -2,17 +2,19 @@ name: .NET Test Pipeline
on:
pull_request:
- branches: [master, 'release/*']
+ push:
+ branches:
+ [master, 'release/*']
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up .NET Core
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
diff --git a/ApiDto/Common/PairPeriodTime.cs b/ApiDto/Common/PairPeriodTime.cs
new file mode 100644
index 0000000..89b0732
--- /dev/null
+++ b/ApiDto/Common/PairPeriodTime.cs
@@ -0,0 +1,22 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Mirea.Api.Dto.Common;
+
+///
+/// Represents a pair of time periods.
+///
+public class PairPeriodTime
+{
+ ///
+ /// Gets or sets the start time of the period.
+ ///
+ [Required]
+ public TimeOnly Start { get; set; }
+
+ ///
+ /// Gets or sets the end time of the period.
+ ///
+ [Required]
+ public TimeOnly End { get; set; }
+}
\ No newline at end of file
diff --git a/ApiDto/Requests/Configuration/CacheRequest.cs b/ApiDto/Requests/Configuration/CacheRequest.cs
new file mode 100644
index 0000000..cafc1c6
--- /dev/null
+++ b/ApiDto/Requests/Configuration/CacheRequest.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Mirea.Api.Dto.Requests.Configuration;
+
+///
+/// Represents a request to configure cache settings.
+///
+public class CacheRequest
+{
+ ///
+ /// Gets or sets the server address.
+ ///
+ [Required]
+ public required string Server { get; set; }
+
+ ///
+ /// Gets or sets the port number.
+ ///
+ [Required]
+ public int Port { get; set; }
+
+ ///
+ /// Gets or sets the password.
+ ///
+ public string? Password { get; set; }
+}
\ No newline at end of file
diff --git a/ApiDto/Requests/Configuration/DatabaseRequest.cs b/ApiDto/Requests/Configuration/DatabaseRequest.cs
new file mode 100644
index 0000000..5da58e3
--- /dev/null
+++ b/ApiDto/Requests/Configuration/DatabaseRequest.cs
@@ -0,0 +1,44 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Mirea.Api.Dto.Requests.Configuration;
+
+///
+/// Represents a request to configure the database connection settings.
+///
+public class DatabaseRequest
+{
+ ///
+ /// Gets or sets the server address.
+ ///
+ [Required]
+ public required string Server { get; set; }
+
+ ///
+ /// Gets or sets the port number.
+ ///
+ [Required]
+ public int Port { get; set; }
+
+ ///
+ /// Gets or sets the database name.
+ ///
+ [Required]
+ public required string Database { get; set; }
+
+ ///
+ /// Gets or sets the username.
+ ///
+ [Required]
+ public required string User { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether SSL is enabled.
+ ///
+ [Required]
+ public bool Ssl { get; set; }
+
+ ///
+ /// Gets or sets the password.
+ ///
+ public string? Password { get; set; }
+}
\ No newline at end of file
diff --git a/ApiDto/Requests/Configuration/EmailRequest.cs b/ApiDto/Requests/Configuration/EmailRequest.cs
new file mode 100644
index 0000000..7770c6e
--- /dev/null
+++ b/ApiDto/Requests/Configuration/EmailRequest.cs
@@ -0,0 +1,45 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Mirea.Api.Dto.Requests.Configuration;
+
+///
+/// Represents a request to configure email settings.
+///
+public class EmailRequest
+{
+ ///
+ /// Gets or sets the server address.
+ ///
+ [Required]
+ public required string Server { get; set; }
+
+ ///
+ /// Gets or sets the email address from which emails will be sent.
+ ///
+ [Required]
+ public required string From { get; set; }
+
+ ///
+ /// Gets or sets the password for the email account.
+ ///
+ [Required]
+ public required string Password { get; set; }
+
+ ///
+ /// Gets or sets the port number.
+ ///
+ [Required]
+ public int Port { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether SSL is enabled.
+ ///
+ [Required]
+ public bool Ssl { get; set; }
+
+ ///
+ /// Gets or sets the username.
+ ///
+ [Required]
+ public required string User { get; set; }
+}
\ No newline at end of file
diff --git a/ApiDto/Requests/Configuration/LoggingRequest.cs b/ApiDto/Requests/Configuration/LoggingRequest.cs
new file mode 100644
index 0000000..eba6428
--- /dev/null
+++ b/ApiDto/Requests/Configuration/LoggingRequest.cs
@@ -0,0 +1,25 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Mirea.Api.Dto.Requests.Configuration;
+
+///
+/// Represents a request to configure logging settings.
+///
+public class LoggingRequest
+{
+ ///
+ /// Gets or sets a value indicating whether logging to file is enabled.
+ ///
+ [Required]
+ public bool EnableLogToFile { get; set; }
+
+ ///
+ /// Gets or sets the log file name.
+ ///
+ public string? LogFileName { get; set; }
+
+ ///
+ /// Gets or sets the log file path.
+ ///
+ public string? LogFilePath { get; set; }
+}
\ No newline at end of file
diff --git a/ApiDto/Requests/Configuration/ScheduleConfigurationRequest.cs b/ApiDto/Requests/Configuration/ScheduleConfigurationRequest.cs
new file mode 100644
index 0000000..ca6e950
--- /dev/null
+++ b/ApiDto/Requests/Configuration/ScheduleConfigurationRequest.cs
@@ -0,0 +1,29 @@
+using Mirea.Api.Dto.Common;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Mirea.Api.Dto.Requests.Configuration;
+
+///
+/// Represents a request to configure the schedule settings.
+///
+public class ScheduleConfigurationRequest
+{
+ ///
+ /// Gets or sets the cron expression for updating the schedule.
+ ///
+ public string? CronUpdateSchedule { get; set; }
+
+ ///
+ /// Gets or sets the start date of the term.
+ ///
+ [Required]
+ public DateOnly StartTerm { get; set; }
+
+ ///
+ /// Gets or sets the pair period times, keyed by pair number.
+ ///
+ [Required]
+ public required IDictionary PairPeriod { get; set; }
+}
\ No newline at end of file
diff --git a/ApiDto/Requests/CreateUserRequest.cs b/ApiDto/Requests/CreateUserRequest.cs
new file mode 100644
index 0000000..3bcf70c
--- /dev/null
+++ b/ApiDto/Requests/CreateUserRequest.cs
@@ -0,0 +1,36 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Mirea.Api.Dto.Requests;
+
+///
+/// Request model for creating a user.
+///
+public class CreateUserRequest
+{
+ ///
+ /// Gets or sets the email address of the user.
+ ///
+ ///
+ /// The email address is a required field.
+ ///
+ [Required]
+ public required string Email { get; set; }
+
+ ///
+ /// Gets or sets the username of the user.
+ ///
+ ///
+ /// The username is a required field.
+ ///
+ [Required]
+ public required string Username { get; set; }
+
+ ///
+ /// Gets or sets the password of the user.
+ ///
+ ///
+ /// The password is a required field.
+ ///
+ [Required]
+ public required string Password { get; set; }
+}
diff --git a/Application/Common/Mappings/AssemblyMappingProfile.cs b/Application/Common/Mappings/AssemblyMappingProfile.cs
deleted file mode 100644
index 68a9e8e..0000000
--- a/Application/Common/Mappings/AssemblyMappingProfile.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using AutoMapper;
-using System;
-using System.Linq;
-using System.Reflection;
-
-namespace Mirea.Api.DataAccess.Application.Common.Mappings;
-
-public class AssemblyMappingProfile : Profile
-{
- public AssemblyMappingProfile(Assembly assembly) =>
- ApplyMappingsFromAssembly(assembly);
-
- private void ApplyMappingsFromAssembly(Assembly assembly)
- {
- var types = assembly.GetExportedTypes()
- .Where(type => type.GetInterfaces()
- .Any(i => i.IsGenericType &&
- i.GetGenericTypeDefinition() == typeof(IMapWith<>)))
- .ToList();
-
- foreach (var type in types)
- {
- var instance = Activator.CreateInstance(type);
- var methodInfo = type.GetMethod("Mapping");
- methodInfo?.Invoke(instance, new[] { this });
- }
- }
-}
\ No newline at end of file
diff --git a/Application/Common/Mappings/IMapWith.cs b/Application/Common/Mappings/IMapWith.cs
deleted file mode 100644
index 390e4e0..0000000
--- a/Application/Common/Mappings/IMapWith.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using AutoMapper;
-
-namespace Mirea.Api.DataAccess.Application.Common.Mappings;
-
-public interface IMapWith
-{
- void Mapping(Profile profile) =>
- profile.CreateMap(typeof(T), GetType());
-}
\ No newline at end of file
diff --git a/Backend.sln b/Backend.sln
index 383bd42..81a8dbe 100644
--- a/Backend.sln
+++ b/Backend.sln
@@ -3,28 +3,47 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "Domain\Domain.csproj", "{C27FB5CD-6A70-4FB2-847A-847B34806902}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Endpoint", "Endpoint\Endpoint.csproj", "{F3A1D12E-F5B2-4339-9966-DBF869E78357}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Elements of the solution", "Elements of the solution", "{3E087889-A4A0-4A55-A07D-7D149A5BC928}"
ProjectSection(SolutionItems) = preProject
.dockerignore = .dockerignore
+ .env = .env
.gitattributes = .gitattributes
.gitignore = .gitignore
Dockerfile = Dockerfile
LICENSE.txt = LICENSE.txt
README.md = README.md
+ .gitea\workflows\test.yaml = .gitea\workflows\test.yaml
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "Application\Application.csproj", "{E7F0A4D4-B032-4BB9-9526-1AF688F341A4}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiDto", "ApiDto\ApiDto.csproj", "{0335FA36-E137-453F-853B-916674C168FE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Security", "Security\Security.csproj", "{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SqlData", "SqlData", "{7E7A63CD-547B-4FB4-A383-EB75298020A1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "SqlData\Domain\Domain.csproj", "{3BFD6180-7CA7-4E85-A379-225B872439A1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "SqlData\Application\Application.csproj", "{0B1F3656-E5B3-440C-961F-A7D004FBE9A8}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence", "SqlData\Persistence\Persistence.csproj", "{48C9998C-ECE2-407F-835F-1A7255A5C99E}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Migrations", "Migrations", "{79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqliteMigrations", "SqlData\Migrations\SqliteMigrations\SqliteMigrations.csproj", "{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}"
ProjectSection(ProjectDependencies) = postProject
- {C27FB5CD-6A70-4FB2-847A-847B34806902} = {C27FB5CD-6A70-4FB2-847A-847B34806902}
+ {48C9998C-ECE2-407F-835F-1A7255A5C99E} = {48C9998C-ECE2-407F-835F-1A7255A5C99E}
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence", "Persistence\Persistence.csproj", "{4C1E558F-633F-438E-AC3A-61CDDED917C5}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MysqlMigrations", "SqlData\Migrations\MysqlMigrations\MysqlMigrations.csproj", "{5861915B-9574-4D5D-872F-D54A09651697}"
ProjectSection(ProjectDependencies) = postProject
- {E7F0A4D4-B032-4BB9-9526-1AF688F341A4} = {E7F0A4D4-B032-4BB9-9526-1AF688F341A4}
+ {48C9998C-ECE2-407F-835F-1A7255A5C99E} = {48C9998C-ECE2-407F-835F-1A7255A5C99E}
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsqlMigrations", "SqlData\Migrations\PsqlMigrations\PsqlMigrations.csproj", "{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}"
+ ProjectSection(ProjectDependencies) = postProject
+ {48C9998C-ECE2-407F-835F-1A7255A5C99E} = {48C9998C-ECE2-407F-835F-1A7255A5C99E}
EndProjectSection
EndProject
Global
@@ -33,26 +52,55 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {C27FB5CD-6A70-4FB2-847A-847B34806902}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C27FB5CD-6A70-4FB2-847A-847B34806902}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C27FB5CD-6A70-4FB2-847A-847B34806902}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C27FB5CD-6A70-4FB2-847A-847B34806902}.Release|Any CPU.Build.0 = Release|Any CPU
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Release|Any CPU.Build.0 = Release|Any CPU
- {E7F0A4D4-B032-4BB9-9526-1AF688F341A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E7F0A4D4-B032-4BB9-9526-1AF688F341A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E7F0A4D4-B032-4BB9-9526-1AF688F341A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E7F0A4D4-B032-4BB9-9526-1AF688F341A4}.Release|Any CPU.Build.0 = Release|Any CPU
- {4C1E558F-633F-438E-AC3A-61CDDED917C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4C1E558F-633F-438E-AC3A-61CDDED917C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4C1E558F-633F-438E-AC3A-61CDDED917C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4C1E558F-633F-438E-AC3A-61CDDED917C5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0335FA36-E137-453F-853B-916674C168FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0335FA36-E137-453F-853B-916674C168FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3BFD6180-7CA7-4E85-A379-225B872439A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3BFD6180-7CA7-4E85-A379-225B872439A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3BFD6180-7CA7-4E85-A379-225B872439A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3BFD6180-7CA7-4E85-A379-225B872439A1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {48C9998C-ECE2-407F-835F-1A7255A5C99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {48C9998C-ECE2-407F-835F-1A7255A5C99E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {48C9998C-ECE2-407F-835F-1A7255A5C99E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {48C9998C-ECE2-407F-835F-1A7255A5C99E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5861915B-9574-4D5D-872F-D54A09651697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5861915B-9574-4D5D-872F-D54A09651697}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5861915B-9574-4D5D-872F-D54A09651697}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5861915B-9574-4D5D-872F-D54A09651697}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {3BFD6180-7CA7-4E85-A379-225B872439A1} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
+ {0B1F3656-E5B3-440C-961F-A7D004FBE9A8} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
+ {48C9998C-ECE2-407F-835F-1A7255A5C99E} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
+ {79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
+ {EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7} = {79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5}
+ {5861915B-9574-4D5D-872F-D54A09651697} = {79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5}
+ {E9E238CD-6DD8-4B29-8C36-C61F1168FCCD} = {79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E80A1224-87F5-4FEB-82AE-89006BE98B12}
EndGlobalSection
diff --git a/Endpoint/Common/Attributes/LocalhostAttribute.cs b/Endpoint/Common/Attributes/LocalhostAttribute.cs
new file mode 100644
index 0000000..734e0bf
--- /dev/null
+++ b/Endpoint/Common/Attributes/LocalhostAttribute.cs
@@ -0,0 +1,19 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using System.Net;
+
+namespace Mirea.Api.Endpoint.Common.Attributes;
+
+public class LocalhostAttribute : ActionFilterAttribute
+{
+ public override void OnActionExecuting(ActionExecutingContext context)
+ {
+ var ip = context.HttpContext.Connection.RemoteIpAddress;
+ if (ip == null || !IPAddress.IsLoopback(ip))
+ {
+ context.Result = new UnauthorizedResult();
+ return;
+ }
+ base.OnActionExecuting(context);
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Common/Attributes/MaintenanceModeIgnoreAttribute.cs b/Endpoint/Common/Attributes/MaintenanceModeIgnoreAttribute.cs
new file mode 100644
index 0000000..6f69b7a
--- /dev/null
+++ b/Endpoint/Common/Attributes/MaintenanceModeIgnoreAttribute.cs
@@ -0,0 +1,6 @@
+using System;
+
+namespace Mirea.Api.Endpoint.Common.Attributes;
+
+[AttributeUsage(AttributeTargets.Class, Inherited = false)]
+public class MaintenanceModeIgnoreAttribute : Attribute;
\ No newline at end of file
diff --git a/Endpoint/Common/Attributes/TokenAuthenticationAttribute.cs b/Endpoint/Common/Attributes/TokenAuthenticationAttribute.cs
new file mode 100644
index 0000000..81812bf
--- /dev/null
+++ b/Endpoint/Common/Attributes/TokenAuthenticationAttribute.cs
@@ -0,0 +1,27 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.DependencyInjection;
+using Mirea.Api.Endpoint.Common.Interfaces;
+using System;
+
+namespace Mirea.Api.Endpoint.Common.Attributes;
+
+[AttributeUsage(AttributeTargets.Method)]
+public class TokenAuthenticationAttribute : Attribute, IActionFilter
+{
+ public void OnActionExecuting(ActionExecutingContext context)
+ {
+ var setupToken = context.HttpContext.RequestServices.GetRequiredService();
+ if (!context.HttpContext.Request.Cookies.TryGetValue("AuthToken", out string? tokenFromCookie))
+ {
+ context.Result = new UnauthorizedResult();
+ return;
+ }
+
+ if (setupToken.MatchToken(Convert.FromBase64String(tokenFromCookie))) return;
+
+ context.Result = new UnauthorizedResult();
+ }
+
+ public void OnActionExecuted(ActionExecutedContext context) { }
+}
\ No newline at end of file
diff --git a/Endpoint/Common/Exceptions/ControllerArgumentException.cs b/Endpoint/Common/Exceptions/ControllerArgumentException.cs
new file mode 100644
index 0000000..36e6b26
--- /dev/null
+++ b/Endpoint/Common/Exceptions/ControllerArgumentException.cs
@@ -0,0 +1,5 @@
+using System;
+
+namespace Mirea.Api.Endpoint.Common.Exceptions;
+
+public class ControllerArgumentException(string message) : Exception(message);
\ No newline at end of file
diff --git a/Endpoint/Common/Interfaces/IMaintenanceModeNotConfigureService.cs b/Endpoint/Common/Interfaces/IMaintenanceModeNotConfigureService.cs
new file mode 100644
index 0000000..e2e6a5d
--- /dev/null
+++ b/Endpoint/Common/Interfaces/IMaintenanceModeNotConfigureService.cs
@@ -0,0 +1,8 @@
+namespace Mirea.Api.Endpoint.Common.Interfaces;
+
+public interface IMaintenanceModeNotConfigureService
+{
+ bool IsMaintenanceMode { get; }
+
+ void DisableMaintenanceMode();
+}
\ No newline at end of file
diff --git a/Endpoint/Common/Interfaces/IMaintenanceModeService.cs b/Endpoint/Common/Interfaces/IMaintenanceModeService.cs
new file mode 100644
index 0000000..7f2d7cb
--- /dev/null
+++ b/Endpoint/Common/Interfaces/IMaintenanceModeService.cs
@@ -0,0 +1,10 @@
+namespace Mirea.Api.Endpoint.Common.Interfaces;
+
+public interface IMaintenanceModeService
+{
+ bool IsMaintenanceMode { get; }
+
+ void EnableMaintenanceMode();
+
+ void DisableMaintenanceMode();
+}
\ No newline at end of file
diff --git a/Endpoint/Common/Interfaces/ISetupToken.cs b/Endpoint/Common/Interfaces/ISetupToken.cs
new file mode 100644
index 0000000..462b759
--- /dev/null
+++ b/Endpoint/Common/Interfaces/ISetupToken.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Mirea.Api.Endpoint.Common.Interfaces;
+
+public interface ISetupToken
+{
+ bool MatchToken(ReadOnlySpan token);
+ void SetToken(ReadOnlySpan token);
+}
\ No newline at end of file
diff --git a/Endpoint/Common/Services/MaintenanceModeNotConfigureService.cs b/Endpoint/Common/Services/MaintenanceModeNotConfigureService.cs
new file mode 100644
index 0000000..b11516b
--- /dev/null
+++ b/Endpoint/Common/Services/MaintenanceModeNotConfigureService.cs
@@ -0,0 +1,11 @@
+using Mirea.Api.Endpoint.Common.Interfaces;
+
+namespace Mirea.Api.Endpoint.Common.Services;
+
+public class MaintenanceModeNotConfigureService : IMaintenanceModeNotConfigureService
+{
+ public bool IsMaintenanceMode { get; private set; } = true;
+
+ public void DisableMaintenanceMode() =>
+ IsMaintenanceMode = false;
+}
\ No newline at end of file
diff --git a/Endpoint/Common/Services/MaintenanceModeService.cs b/Endpoint/Common/Services/MaintenanceModeService.cs
new file mode 100644
index 0000000..ef57365
--- /dev/null
+++ b/Endpoint/Common/Services/MaintenanceModeService.cs
@@ -0,0 +1,14 @@
+using Mirea.Api.Endpoint.Common.Interfaces;
+
+namespace Mirea.Api.Endpoint.Common.Services;
+
+public class MaintenanceModeService : IMaintenanceModeService
+{
+ public bool IsMaintenanceMode { get; private set; }
+
+ public void EnableMaintenanceMode() =>
+ IsMaintenanceMode = true;
+
+ public void DisableMaintenanceMode() =>
+ IsMaintenanceMode = false;
+}
\ No newline at end of file
diff --git a/Endpoint/Common/Services/PairPeriodTimeConverter.cs b/Endpoint/Common/Services/PairPeriodTimeConverter.cs
new file mode 100644
index 0000000..ad46295
--- /dev/null
+++ b/Endpoint/Common/Services/PairPeriodTimeConverter.cs
@@ -0,0 +1,13 @@
+using Mirea.Api.Endpoint.Configuration.General.Settings;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Mirea.Api.Endpoint.Common.Services;
+
+public static class PairPeriodTimeConverter
+{
+ public static Dictionary ConvertToDto(this IDictionary pairPeriod) =>
+ pairPeriod.ToDictionary(kvp => kvp.Key, kvp => new Dto.Common.PairPeriodTime { Start = kvp.Value.Start, End = kvp.Value.End });
+
+ public static Dictionary ConvertFromDto(this IDictionary pairPeriod) => pairPeriod.ToDictionary(kvp => kvp.Key, kvp => new ScheduleSettings.PairPeriodTime(kvp.Value.Start, kvp.Value.End));
+}
diff --git a/Endpoint/Common/Services/PathBuilder.cs b/Endpoint/Common/Services/PathBuilder.cs
new file mode 100644
index 0000000..799f582
--- /dev/null
+++ b/Endpoint/Common/Services/PathBuilder.cs
@@ -0,0 +1,12 @@
+using System;
+using System.IO;
+using System.Linq;
+
+namespace Mirea.Api.Endpoint.Common.Services;
+
+public static class PathBuilder
+{
+ public static bool IsDefaultPath => Environment.GetEnvironmentVariable("PATH_TO_SAVE") == null;
+ public static string PathToSave => Environment.GetEnvironmentVariable("PATH_TO_SAVE") ?? Directory.GetCurrentDirectory();
+ public static string Combine(params string[] paths) => Path.Combine([.. paths.Prepend(PathToSave)]);
+}
\ No newline at end of file
diff --git a/Endpoint/Common/Services/Security/DistributedCacheService.cs b/Endpoint/Common/Services/Security/DistributedCacheService.cs
new file mode 100644
index 0000000..bf3dc39
--- /dev/null
+++ b/Endpoint/Common/Services/Security/DistributedCacheService.cs
@@ -0,0 +1,32 @@
+using Microsoft.Extensions.Caching.Distributed;
+using Mirea.Api.Security.Common.Interfaces;
+using System;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Mirea.Api.Endpoint.Common.Services.Security;
+
+public class DistributedCacheService(IDistributedCache cache) : ICacheService
+{
+ public async Task SetAsync(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null, CancellationToken cancellationToken = default)
+ {
+ var options = new DistributedCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow,
+ SlidingExpiration = slidingExpiration
+ };
+
+ var serializedValue = JsonSerializer.SerializeToUtf8Bytes(value);
+ await cache.SetAsync(key, serializedValue, options, cancellationToken);
+ }
+
+ public async Task GetAsync(string key, CancellationToken cancellationToken = default)
+ {
+ var cachedValue = await cache.GetAsync(key, cancellationToken);
+ return cachedValue == null ? default : JsonSerializer.Deserialize(cachedValue);
+ }
+
+ public Task RemoveAsync(string key, CancellationToken cancellationToken = default) =>
+ cache.RemoveAsync(key, cancellationToken);
+}
\ No newline at end of file
diff --git a/Endpoint/Common/Services/Security/JwtTokenService.cs b/Endpoint/Common/Services/Security/JwtTokenService.cs
new file mode 100644
index 0000000..7c3225f
--- /dev/null
+++ b/Endpoint/Common/Services/Security/JwtTokenService.cs
@@ -0,0 +1,82 @@
+using Microsoft.IdentityModel.Tokens;
+using Mirea.Api.Security.Common.Interfaces;
+using System;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Security.Claims;
+
+namespace Mirea.Api.Endpoint.Common.Services.Security;
+
+public class JwtTokenService : IAccessToken
+{
+ public required string Issuer { private get; init; }
+ public required string Audience { private get; init; }
+ public TimeSpan Lifetime { private get; init; }
+
+ public ReadOnlyMemory EncryptionKey { get; init; }
+ public ReadOnlyMemory SigningKey { private get; init; }
+
+ public (string Token, DateTime ExpireIn) GenerateToken(string userId)
+ {
+ var tokenHandler = new JwtSecurityTokenHandler();
+ var signingKey = new SymmetricSecurityKey(SigningKey.ToArray());
+ var encryptionKey = new SymmetricSecurityKey(EncryptionKey.ToArray());
+ var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha512);
+
+ var expires = DateTime.UtcNow.Add(Lifetime);
+
+ var tokenDescriptor = new SecurityTokenDescriptor
+ {
+ Issuer = Issuer,
+ Audience = Audience,
+ Expires = expires,
+ SigningCredentials = signingCredentials,
+ Subject = new ClaimsIdentity(
+ [
+ new Claim(ClaimTypes.Name, userId),
+ // todo: get role by userId
+ new Claim(ClaimTypes.Role, "")
+ ]),
+ EncryptingCredentials = new EncryptingCredentials(encryptionKey, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512)
+ };
+
+ var token = tokenHandler.CreateToken(tokenDescriptor);
+
+ return (tokenHandler.WriteToken(token), expires);
+ }
+
+ public DateTimeOffset GetExpireDateTime(string token)
+ {
+ var tokenHandler = new JwtSecurityTokenHandler();
+ var signingKey = new SymmetricSecurityKey(SigningKey.ToArray());
+ var encryptionKey = new SymmetricSecurityKey(EncryptionKey.ToArray());
+
+ var tokenValidationParameters = new TokenValidationParameters
+ {
+ ValidIssuer = Issuer,
+ ValidAudience = Audience,
+ IssuerSigningKey = signingKey,
+ TokenDecryptionKey = encryptionKey,
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateIssuerSigningKey = true,
+ ValidateLifetime = false
+ };
+
+ try
+ {
+ var claimsPrincipal = tokenHandler.ValidateToken(token, tokenValidationParameters, out _);
+
+ var expClaim = claimsPrincipal.Claims.FirstOrDefault(c => c.Type == "exp");
+
+ if (expClaim != null && long.TryParse(expClaim.Value, out var expUnix))
+ return DateTimeOffset.FromUnixTimeSeconds(expUnix);
+ }
+ catch (SecurityTokenException)
+ {
+ return DateTimeOffset.MinValue;
+ }
+
+ return DateTimeOffset.MinValue;
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Common/Services/Security/MemoryCacheService.cs b/Endpoint/Common/Services/Security/MemoryCacheService.cs
new file mode 100644
index 0000000..a428034
--- /dev/null
+++ b/Endpoint/Common/Services/Security/MemoryCacheService.cs
@@ -0,0 +1,34 @@
+using Microsoft.Extensions.Caching.Memory;
+using Mirea.Api.Security.Common.Interfaces;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Mirea.Api.Endpoint.Common.Services.Security;
+
+public class MemoryCacheService(IMemoryCache cache) : ICacheService
+{
+ public Task SetAsync(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null, CancellationToken cancellationToken = default)
+ {
+ var options = new MemoryCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow,
+ SlidingExpiration = slidingExpiration
+ };
+
+ cache.Set(key, value, options);
+ return Task.CompletedTask;
+ }
+
+ public Task GetAsync(string key, CancellationToken cancellationToken = default)
+ {
+ cache.TryGetValue(key, out T? value);
+ return Task.FromResult(value);
+ }
+
+ public Task RemoveAsync(string key, CancellationToken cancellationToken = default)
+ {
+ cache.Remove(key);
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Common/Services/Security/MemoryRevokedTokenService.cs b/Endpoint/Common/Services/Security/MemoryRevokedTokenService.cs
new file mode 100644
index 0000000..94c2f75
--- /dev/null
+++ b/Endpoint/Common/Services/Security/MemoryRevokedTokenService.cs
@@ -0,0 +1,17 @@
+using Microsoft.Extensions.Caching.Memory;
+using Mirea.Api.Security.Common.Interfaces;
+using System;
+using System.Threading.Tasks;
+
+namespace Mirea.Api.Endpoint.Common.Services.Security;
+
+public class MemoryRevokedTokenService(IMemoryCache cache) : IRevokedToken
+{
+ public Task AddTokenToRevokedAsync(string token, DateTimeOffset expiresIn)
+ {
+ cache.Set(token, true, expiresIn);
+ return Task.CompletedTask;
+ }
+
+ public Task IsTokenRevokedAsync(string token) => Task.FromResult(cache.TryGetValue(token, out _));
+}
\ No newline at end of file
diff --git a/Endpoint/Configuration/EnvironmentManager.cs b/Endpoint/Configuration/EnvironmentManager.cs
index cc2b201..c21d59e 100644
--- a/Endpoint/Configuration/EnvironmentManager.cs
+++ b/Endpoint/Configuration/EnvironmentManager.cs
@@ -5,20 +5,32 @@ namespace Mirea.Api.Endpoint.Configuration;
internal static class EnvironmentManager
{
- public static void LoadEnvironment(string filePath)
+ public static void LoadEnvironment(string envFile)
{
- if (!File.Exists(filePath)) return;
+ if (!File.Exists(envFile)) return;
- foreach (var line in File.ReadAllLines(filePath))
+ foreach (var line in File.ReadAllLines(envFile))
{
- var parts = line.Split(
+ if (string.IsNullOrEmpty(line)) continue;
+
+ var commentIndex = line.IndexOf('#', StringComparison.Ordinal);
+
+ string arg = line;
+
+ if (commentIndex != -1)
+ arg = arg.Remove(commentIndex, arg.Length - commentIndex);
+
+ var parts = arg.Split(
'=',
StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length > 2)
+ parts = [parts[0], string.Join("=", parts[1..])];
+
if (parts.Length != 2)
continue;
- Environment.SetEnvironmentVariable(parts[0].Trim(), parts[1][..(parts[1].Contains('#') ? parts[1].IndexOf('#') : parts[1].Length)].Trim());
+ Environment.SetEnvironmentVariable(parts[0].Trim(), parts[1].Trim());
}
}
}
\ No newline at end of file
diff --git a/Endpoint/Configuration/General/Attributes/RequiredSettingsAttribute.cs b/Endpoint/Configuration/General/Attributes/RequiredSettingsAttribute.cs
new file mode 100644
index 0000000..dcb13ac
--- /dev/null
+++ b/Endpoint/Configuration/General/Attributes/RequiredSettingsAttribute.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace Mirea.Api.Endpoint.Configuration.General.Attributes;
+
+[AttributeUsage(AttributeTargets.Class, Inherited = false)]
+public class RequiredSettingsAttribute : Attribute;
+
+// todo: only with IIsConfigured. If possible add Roslyn Analyzer later
\ No newline at end of file
diff --git a/Endpoint/Configuration/General/GeneralConfig.cs b/Endpoint/Configuration/General/GeneralConfig.cs
new file mode 100644
index 0000000..60f98a1
--- /dev/null
+++ b/Endpoint/Configuration/General/GeneralConfig.cs
@@ -0,0 +1,14 @@
+using Mirea.Api.Endpoint.Configuration.General.Settings;
+
+namespace Mirea.Api.Endpoint.Configuration.General;
+
+public class GeneralConfig
+{
+ public const string FilePath = "Settings.json";
+
+ public DbSettings? DbSettings { get; set; }
+ public CacheSettings? CacheSettings { get; set; }
+ public ScheduleSettings? ScheduleSettings { get; set; }
+ public EmailSettings? EmailSettings { get; set; }
+ public LogSettings? LogSettings { get; set; }
+}
\ No newline at end of file
diff --git a/Endpoint/Configuration/General/Interfaces/IIsConfigured.cs b/Endpoint/Configuration/General/Interfaces/IIsConfigured.cs
new file mode 100644
index 0000000..60c09e0
--- /dev/null
+++ b/Endpoint/Configuration/General/Interfaces/IIsConfigured.cs
@@ -0,0 +1,6 @@
+namespace Mirea.Api.Endpoint.Configuration.General.Interfaces;
+
+public interface IIsConfigured
+{
+ bool IsConfigured();
+}
\ No newline at end of file
diff --git a/Endpoint/Configuration/General/Settings/CacheSettings.cs b/Endpoint/Configuration/General/Settings/CacheSettings.cs
new file mode 100644
index 0000000..a0a7802
--- /dev/null
+++ b/Endpoint/Configuration/General/Settings/CacheSettings.cs
@@ -0,0 +1,23 @@
+using Mirea.Api.Endpoint.Configuration.General.Attributes;
+using Mirea.Api.Endpoint.Configuration.General.Interfaces;
+
+namespace Mirea.Api.Endpoint.Configuration.General.Settings;
+
+[RequiredSettings]
+public class CacheSettings : IIsConfigured
+{
+ public enum CacheEnum
+ {
+ Memcached,
+ Redis
+ }
+
+ public CacheEnum TypeDatabase { get; set; }
+ public string? ConnectionString { get; set; }
+
+ public bool IsConfigured()
+ {
+ return TypeDatabase == CacheEnum.Memcached ||
+ !string.IsNullOrEmpty(ConnectionString);
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Configuration/General/Settings/DbSettings.cs b/Endpoint/Configuration/General/Settings/DbSettings.cs
new file mode 100644
index 0000000..2fa5ff9
--- /dev/null
+++ b/Endpoint/Configuration/General/Settings/DbSettings.cs
@@ -0,0 +1,31 @@
+using System;
+using Mirea.Api.DataAccess.Persistence.Common;
+using Mirea.Api.Endpoint.Configuration.General.Attributes;
+using Mirea.Api.Endpoint.Configuration.General.Interfaces;
+
+namespace Mirea.Api.Endpoint.Configuration.General.Settings;
+
+[RequiredSettings]
+public class DbSettings : IIsConfigured
+{
+ public enum DatabaseEnum
+ {
+ Mysql,
+ Sqlite,
+ PostgresSql
+ }
+ public DatabaseEnum TypeDatabase { get; set; }
+ public required string ConnectionStringSql { get; set; }
+
+ public DatabaseProvider DatabaseProvider =>
+ TypeDatabase switch
+ {
+ DatabaseEnum.PostgresSql => DatabaseProvider.Postgresql,
+ DatabaseEnum.Mysql => DatabaseProvider.Mysql,
+ DatabaseEnum.Sqlite => DatabaseProvider.Sqlite,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ public bool IsConfigured() =>
+ !string.IsNullOrEmpty(ConnectionStringSql);
+}
\ No newline at end of file
diff --git a/Endpoint/Configuration/General/Settings/EmailSettings.cs b/Endpoint/Configuration/General/Settings/EmailSettings.cs
new file mode 100644
index 0000000..bdd5179
--- /dev/null
+++ b/Endpoint/Configuration/General/Settings/EmailSettings.cs
@@ -0,0 +1,23 @@
+using Mirea.Api.Endpoint.Configuration.General.Interfaces;
+
+namespace Mirea.Api.Endpoint.Configuration.General.Settings;
+
+public class EmailSettings : IIsConfigured
+{
+ public string? Server { get; set; }
+ public string? User { get; set; }
+ public string? Password { get; set; }
+ public string? From { get; set; }
+ public int? Port { get; set; }
+ public bool? Ssl { get; set; }
+
+ public bool IsConfigured()
+ {
+ return !string.IsNullOrEmpty(Server) &&
+ !string.IsNullOrEmpty(User) &&
+ !string.IsNullOrEmpty(Password) &&
+ !string.IsNullOrEmpty(From) &&
+ Port.HasValue &&
+ Ssl.HasValue;
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Configuration/General/Settings/LogSettings.cs b/Endpoint/Configuration/General/Settings/LogSettings.cs
new file mode 100644
index 0000000..4a42d28
--- /dev/null
+++ b/Endpoint/Configuration/General/Settings/LogSettings.cs
@@ -0,0 +1,19 @@
+using Mirea.Api.Endpoint.Configuration.General.Attributes;
+using Mirea.Api.Endpoint.Configuration.General.Interfaces;
+
+namespace Mirea.Api.Endpoint.Configuration.General.Settings;
+
+[RequiredSettings]
+public class LogSettings : IIsConfigured
+{
+ public bool EnableLogToFile { get; set; }
+ public string? LogFilePath { get; set; }
+ public string? LogFileName { get; set; }
+
+ public bool IsConfigured()
+ {
+ return !EnableLogToFile ||
+ !string.IsNullOrEmpty(LogFilePath) &&
+ !string.IsNullOrEmpty(LogFileName);
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Configuration/General/Settings/ScheduleSettings.cs b/Endpoint/Configuration/General/Settings/ScheduleSettings.cs
new file mode 100644
index 0000000..a45b41f
--- /dev/null
+++ b/Endpoint/Configuration/General/Settings/ScheduleSettings.cs
@@ -0,0 +1,45 @@
+using Mirea.Api.Endpoint.Configuration.General.Attributes;
+using Mirea.Api.Endpoint.Configuration.General.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Mirea.Api.Endpoint.Configuration.General.Settings;
+
+[RequiredSettings]
+public class ScheduleSettings : IIsConfigured
+{
+ public struct PairPeriodTime
+ {
+ public TimeOnly Start { get; set; }
+ public TimeOnly End { get; set; }
+
+ public PairPeriodTime(TimeOnly t1, TimeOnly t2)
+ {
+ if (t1 > t2)
+ {
+ Start = t2;
+ End = t1;
+ }
+ else
+ {
+ Start = t1;
+ End = t2;
+ }
+ }
+
+ public PairPeriodTime(Dto.Common.PairPeriodTime time) : this(time.Start, time.End) { }
+ }
+
+ public required string CronUpdateSchedule { get; set; }
+ public DateOnly StartTerm { get; set; }
+ public required IDictionary PairPeriod { get; set; }
+
+ public bool IsConfigured()
+ {
+ return !string.IsNullOrEmpty(CronUpdateSchedule) &&
+ StartTerm != default &&
+ PairPeriod.Count != 0 &&
+ PairPeriod.Any();
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Configuration/General/SetupTokenService.cs b/Endpoint/Configuration/General/SetupTokenService.cs
new file mode 100644
index 0000000..4cc1216
--- /dev/null
+++ b/Endpoint/Configuration/General/SetupTokenService.cs
@@ -0,0 +1,28 @@
+using Mirea.Api.Endpoint.Common.Interfaces;
+using System;
+
+namespace Mirea.Api.Endpoint.Configuration.General;
+
+public class SetupTokenService : ISetupToken
+{
+ public ReadOnlyMemory? Token { get; private set; }
+
+ public bool MatchToken(ReadOnlySpan token)
+ {
+ if (Token == null || token.Length != Token.Value.Length)
+ return false;
+
+ var token2 = Token.Value.Span;
+
+ int result = 0;
+ for (int i = 0; i < Token.Value.Length; i++)
+ result |= token2[i] ^ token[i];
+
+ return result == 0;
+ }
+
+ public void SetToken(ReadOnlySpan token)
+ {
+ Token = token.ToArray();
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Configuration/General/Validators/SettingsRequiredValidator.cs b/Endpoint/Configuration/General/Validators/SettingsRequiredValidator.cs
new file mode 100644
index 0000000..a1e8fbc
--- /dev/null
+++ b/Endpoint/Configuration/General/Validators/SettingsRequiredValidator.cs
@@ -0,0 +1,38 @@
+using Microsoft.Extensions.Options;
+using Mirea.Api.Endpoint.Configuration.General.Attributes;
+using Mirea.Api.Endpoint.Configuration.General.Interfaces;
+using System;
+using System.Reflection;
+
+namespace Mirea.Api.Endpoint.Configuration.General.Validators;
+
+public class SettingsRequiredValidator
+{
+ private readonly GeneralConfig _generalConfig;
+
+ public SettingsRequiredValidator(IOptionsSnapshot configuration) =>
+ _generalConfig = configuration.Value;
+
+ public SettingsRequiredValidator(GeneralConfig configuration) =>
+ _generalConfig = configuration;
+
+ public bool AreSettingsValid()
+ {
+ foreach (var property in _generalConfig
+ .GetType()
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance))
+ {
+ if (!Attribute.IsDefined(property.PropertyType, typeof(RequiredSettingsAttribute))) continue;
+
+ var value = property.GetValue(_generalConfig);
+ if (value == null)
+ return false;
+
+ var isConfigured = value as IIsConfigured;
+ if (!isConfigured!.IsConfigured())
+ return false;
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Configuration/ConfigureSwaggerOptions.cs b/Endpoint/Configuration/Swagger/ConfigureSwaggerOptions.cs
similarity index 96%
rename from Endpoint/Configuration/ConfigureSwaggerOptions.cs
rename to Endpoint/Configuration/Swagger/ConfigureSwaggerOptions.cs
index 39280bb..bf47325 100644
--- a/Endpoint/Configuration/ConfigureSwaggerOptions.cs
+++ b/Endpoint/Configuration/Swagger/ConfigureSwaggerOptions.cs
@@ -5,7 +5,7 @@ using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
-namespace Mirea.Api.Endpoint.Configuration;
+namespace Mirea.Api.Endpoint.Configuration.Swagger;
public class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions
{
diff --git a/Endpoint/Configuration/SwaggerDefaultValues.cs b/Endpoint/Configuration/Swagger/SwaggerDefaultValues.cs
similarity index 97%
rename from Endpoint/Configuration/SwaggerDefaultValues.cs
rename to Endpoint/Configuration/Swagger/SwaggerDefaultValues.cs
index cefb307..581393e 100644
--- a/Endpoint/Configuration/SwaggerDefaultValues.cs
+++ b/Endpoint/Configuration/Swagger/SwaggerDefaultValues.cs
@@ -6,7 +6,7 @@ using System;
using System.Linq;
using System.Text.Json;
-namespace Mirea.Api.Endpoint.Configuration;
+namespace Mirea.Api.Endpoint.Configuration.Swagger;
public class SwaggerDefaultValues : IOperationFilter
{
diff --git a/Endpoint/Controllers/BaseController.cs b/Endpoint/Controllers/BaseController.cs
index 82c4c7a..53f02a1 100644
--- a/Endpoint/Controllers/BaseController.cs
+++ b/Endpoint/Controllers/BaseController.cs
@@ -2,6 +2,7 @@
namespace Mirea.Api.Endpoint.Controllers;
+[Produces("application/json")]
+[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
-[Route("api/[controller]/[action]")]
public class BaseController : ControllerBase;
\ No newline at end of file
diff --git a/Endpoint/Controllers/Configuration/SetupController.cs b/Endpoint/Controllers/Configuration/SetupController.cs
new file mode 100644
index 0000000..1c9d275
--- /dev/null
+++ b/Endpoint/Controllers/Configuration/SetupController.cs
@@ -0,0 +1,312 @@
+using Cronos;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Data.Sqlite;
+using Microsoft.Extensions.Caching.Memory;
+using Mirea.Api.Dto.Requests;
+using Mirea.Api.Dto.Requests.Configuration;
+using Mirea.Api.Endpoint.Common.Attributes;
+using Mirea.Api.Endpoint.Common.Exceptions;
+using Mirea.Api.Endpoint.Common.Interfaces;
+using Mirea.Api.Endpoint.Common.Services;
+using Mirea.Api.Endpoint.Configuration.General;
+using Mirea.Api.Endpoint.Configuration.General.Settings;
+using Mirea.Api.Endpoint.Configuration.General.Validators;
+using MySqlConnector;
+using Npgsql;
+using StackExchange.Redis;
+using System;
+using System.Data;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text.Json;
+
+namespace Mirea.Api.Endpoint.Controllers.Configuration;
+
+[ApiVersion("1.0")]
+[ApiController]
+[MaintenanceModeIgnore]
+[ApiExplorerSettings(IgnoreApi = true)]
+public class SetupController(ISetupToken setupToken, IMaintenanceModeNotConfigureService notConfigureService, IMemoryCache cache) : BaseController
+{
+ private const string CacheGeneralKey = "config_general";
+ private const string CacheAdminKey = "config_admin";
+
+ private GeneralConfig GeneralConfig
+ {
+ get => cache.Get(CacheGeneralKey) ?? new GeneralConfig();
+ set => cache.Set(CacheGeneralKey, value);
+ }
+
+ [HttpGet("GenerateToken")]
+ [Localhost]
+ public ActionResult GenerateToken()
+ {
+ if (!notConfigureService.IsMaintenanceMode)
+ throw new ControllerArgumentException(
+ "The token cannot be generated because the server has been configured. " +
+ $"If you need to restart the configuration, then delete the \"{PathBuilder.Combine(GeneralConfig.FilePath)}\" file and restart the application.");
+
+ var token = new byte[32];
+ RandomNumberGenerator.Create().GetBytes(token);
+ setupToken.SetToken(token);
+
+ return Ok(Convert.ToBase64String(token));
+ }
+
+ [HttpGet("CheckToken")]
+ public ActionResult CheckToken([FromQuery] string token)
+ {
+ if (!setupToken.MatchToken(Convert.FromBase64String(token))) return Unauthorized("The token is not valid");
+
+ Response.Cookies.Append("AuthToken", token, new CookieOptions
+ {
+ HttpOnly = false,
+ Secure = false,
+ Path = "/"
+ });
+
+ return Ok(true);
+ }
+
+ private ActionResult SetDatabase(string connectionString, DbSettings.DatabaseEnum databaseType)
+ where TConnection : class, IDbConnection, new()
+ where TException : Exception
+ {
+ try
+ {
+ using var connection = new TConnection();
+ connection.ConnectionString = connectionString;
+ connection.Open();
+ connection.Close();
+
+ var general = GeneralConfig;
+ general.DbSettings = new DbSettings
+ {
+ ConnectionStringSql = connectionString,
+ TypeDatabase = databaseType
+ };
+ GeneralConfig = general;
+
+ return Ok(true);
+ }
+ catch (TException ex)
+ {
+ throw new ControllerArgumentException($"Error when connecting: {ex.Message}");
+ }
+ }
+
+ [HttpPost("SetPsql")]
+ [TokenAuthentication]
+ [BadRequestResponse]
+ public ActionResult SetPsql([FromBody] DatabaseRequest request)
+ {
+ string connectionString = $"Host={request.Server}:{request.Port};Username={request.User};Database={request.Database}";
+ if (request.Password != null)
+ connectionString += $";Password={request.Password}";
+ if (request.Ssl)
+ connectionString += ";SSL Mode=Require;";
+
+ return SetDatabase(connectionString, DbSettings.DatabaseEnum.PostgresSql);
+ }
+
+ [HttpPost("SetMysql")]
+ [TokenAuthentication]
+ [BadRequestResponse]
+ public ActionResult SetMysql([FromBody] DatabaseRequest request)
+ {
+ string connectionString = $"Server={request.Server}:{request.Port};Uid={request.User};Database={request.Database};";
+ if (request.Password != null)
+ connectionString += $"Pwd={request.Password};";
+ if (request.Ssl)
+ connectionString += "SslMode=Require;";
+
+ return SetDatabase(connectionString, DbSettings.DatabaseEnum.Mysql);
+ }
+
+ [HttpPost("SetSqlite")]
+ [TokenAuthentication]
+ public ActionResult SetSqlite([FromQuery] string? path)
+ {
+ if (string.IsNullOrEmpty(path)) path = "database";
+
+ path = PathBuilder.Combine(path);
+
+ if (!Directory.Exists(path))
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ Directory.CreateDirectory(path);
+ else
+ Directory.CreateDirectory(path, UnixFileMode.UserRead | UnixFileMode.UserWrite);
+ }
+ else
+ throw new ControllerArgumentException("Such a folder exists. Enter a different name");
+
+ string connectionString = $"Data Source={PathBuilder.Combine(path, "database.db3")}";
+
+ return SetDatabase(connectionString, DbSettings.DatabaseEnum.Sqlite);
+ }
+
+ [HttpPost("SetRedis")]
+ [TokenAuthentication]
+ [BadRequestResponse]
+ public ActionResult SetRedis([FromBody] CacheRequest request)
+ {
+ string connectionString = $"{request.Server}:{request.Port},ssl=false";
+ if (request.Password != null)
+ connectionString += $",password={request.Password}";
+
+ try
+ {
+ var redis = ConnectionMultiplexer.Connect(connectionString);
+ redis.Close();
+
+ var general = GeneralConfig;
+ general.CacheSettings = new CacheSettings
+ {
+ ConnectionString = connectionString,
+ TypeDatabase = CacheSettings.CacheEnum.Redis
+ };
+ GeneralConfig = general;
+
+ return Ok(true);
+ }
+ catch (Exception ex)
+ {
+ throw new ControllerArgumentException("Error when connecting to Redis: " + ex.Message);
+ }
+ }
+
+ [HttpPost("SetMemcached")]
+ [TokenAuthentication]
+ [BadRequestResponse]
+ public ActionResult SetMemcached()
+ {
+ var general = GeneralConfig;
+ general.CacheSettings = new CacheSettings
+ {
+ ConnectionString = null,
+ TypeDatabase = CacheSettings.CacheEnum.Memcached
+ };
+ GeneralConfig = general;
+
+ return Ok(true);
+ }
+
+ [HttpPost("CreateAdmin")]
+ [TokenAuthentication]
+ [BadRequestResponse]
+ public ActionResult CreateAdmin([FromBody] CreateUserRequest user)
+ {
+ // todo: change CreateUserRequest to Domain entity
+ cache.Set(CacheAdminKey, user);
+ return Ok(true);
+ }
+
+ [HttpPost("SetLogging")]
+ [TokenAuthentication]
+ [BadRequestResponse]
+ public ActionResult SetLogging([FromBody] LoggingRequest? request = null)
+ {
+ var settings = (request == null) switch
+ {
+ true => new LogSettings
+ {
+ EnableLogToFile = true,
+ LogFileName = "log-",
+ LogFilePath = OperatingSystem.IsWindows() || PathBuilder.IsDefaultPath ?
+ PathBuilder.Combine("logs") :
+ "/var/log/mirea"
+ },
+ false => new LogSettings
+ {
+ EnableLogToFile = request.EnableLogToFile,
+ LogFileName = request.LogFileName,
+ LogFilePath = request.LogFilePath
+ }
+ };
+
+ var general = GeneralConfig;
+ general.LogSettings = settings;
+ GeneralConfig = general;
+
+ return true;
+ }
+
+ [HttpPost("SetEmail")]
+ [TokenAuthentication]
+ [BadRequestResponse]
+ public ActionResult SetEmail([FromBody] EmailRequest? request = null)
+ {
+ var settings = (request == null) switch
+ {
+ true => new EmailSettings(),
+ false => new EmailSettings
+ {
+ Server = request.Server,
+ From = request.From,
+ Password = request.Password,
+ Port = request.Port,
+ Ssl = request.Ssl,
+ User = request.User
+ }
+ };
+
+ var general = GeneralConfig;
+ general.EmailSettings = settings;
+ GeneralConfig = general;
+
+ return true;
+ }
+
+ [HttpPost("SetSchedule")]
+ [TokenAuthentication]
+ [BadRequestResponse]
+ public ActionResult SetSchedule([FromBody] ScheduleConfigurationRequest request)
+ {
+ var general = GeneralConfig;
+ general.ScheduleSettings = new ScheduleSettings
+ {
+ // every 6 hours
+ CronUpdateSchedule = request.CronUpdateSchedule ?? "0 */6 * * *",
+ StartTerm = request.StartTerm,
+ PairPeriod = request.PairPeriod.ConvertFromDto()
+ };
+
+ if (!CronExpression.TryParse(general.ScheduleSettings.CronUpdateSchedule, CronFormat.Standard, out _))
+ throw new ControllerArgumentException("The Cron task could not be parsed. Check the format of the entered data.");
+
+ GeneralConfig = general;
+
+ return true;
+ }
+
+ [HttpPost("Submit")]
+ [TokenAuthentication]
+ [BadRequestResponse]
+ public ActionResult Submit()
+ {
+ if (!new SettingsRequiredValidator(GeneralConfig).AreSettingsValid())
+ throw new ControllerArgumentException("The necessary data has not been configured.");
+
+ // todo: change CreateUserRequest to Domain entity
+ if (!cache.TryGetValue(CacheAdminKey, out CreateUserRequest? user) || user == null)
+ throw new ControllerArgumentException("The administrator's data was not set.");
+
+ if (System.IO.File.Exists(PathBuilder.Combine(GeneralConfig.FilePath)))
+ System.IO.File.Delete(PathBuilder.Combine(GeneralConfig.FilePath));
+
+ System.IO.File.WriteAllText(PathBuilder.Combine("admin.json"), JsonSerializer.Serialize(user));
+
+ System.IO.File.WriteAllText(
+ PathBuilder.Combine(GeneralConfig.FilePath),
+ JsonSerializer.Serialize(GeneralConfig, new JsonSerializerOptions
+ {
+ WriteIndented = true
+ })
+ );
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Controllers/V1/BaseControllerV1.cs b/Endpoint/Controllers/V1/BaseControllerV1.cs
deleted file mode 100644
index 6ebe1f3..0000000
--- a/Endpoint/Controllers/V1/BaseControllerV1.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-
-namespace Mirea.Api.Endpoint.Controllers.V1;
-
-[ApiVersion("1.0")]
-[Produces("application/json")]
-[Route("api/v{version:apiVersion}/[controller]/[action]")]
-public class BaseControllerV1 : BaseController;
\ No newline at end of file
diff --git a/Endpoint/Controllers/V1/CampusController.cs b/Endpoint/Controllers/V1/CampusController.cs
index ae83834..4f4f101 100644
--- a/Endpoint/Controllers/V1/CampusController.cs
+++ b/Endpoint/Controllers/V1/CampusController.cs
@@ -9,53 +9,53 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-namespace Mirea.Api.Endpoint.Controllers.V1
+namespace Mirea.Api.Endpoint.Controllers.V1;
+
+[ApiVersion("1.0")]
+public class CampusController(IMediator mediator) : BaseController
{
- public class CampusController(IMediator mediator) : BaseControllerV1
+ ///
+ /// Gets basic information about campuses.
+ ///
+ /// Basic information about campuses.
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> Get()
{
- ///
- /// Gets basic information about campuses.
- ///
- /// Basic information about campuses.
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task>> Get()
- {
- var result = await mediator.Send(new GetCampusBasicInfoListQuery());
+ var result = await mediator.Send(new GetCampusBasicInfoListQuery());
- return Ok(result.Campuses
- .Select(c => new CampusBasicInfoResponse()
- {
- Id = c.Id,
- CodeName = c.CodeName,
- FullName = c.FullName
- })
- );
- }
-
- ///
- /// Gets details of a specific campus by ID.
- ///
- /// Campus ID.
- /// Details of the specified campus.
- [HttpGet("{id:int}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [BadRequestResponse]
- [NotFoundResponse]
- public async Task> GetDetails(int id)
- {
- var result = await mediator.Send(new GetCampusDetailsQuery()
+ return Ok(result.Campuses
+ .Select(c => new CampusBasicInfoResponse()
{
- Id = id
- });
-
- return Ok(new CampusDetailsResponse()
- {
- Id = result.Id,
- CodeName = result.CodeName,
- FullName = result.FullName,
- Address = result.Address
- });
- }
+ Id = c.Id,
+ CodeName = c.CodeName,
+ FullName = c.FullName
+ })
+ );
}
-}
+
+ ///
+ /// Gets details of a specific campus by ID.
+ ///
+ /// Campus ID.
+ /// Details of the specified campus.
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [BadRequestResponse]
+ [NotFoundResponse]
+ public async Task> GetDetails(int id)
+ {
+ var result = await mediator.Send(new GetCampusDetailsQuery()
+ {
+ Id = id
+ });
+
+ return Ok(new CampusDetailsResponse()
+ {
+ Id = result.Id,
+ CodeName = result.CodeName,
+ FullName = result.FullName,
+ Address = result.Address
+ });
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Controllers/V1/DisciplineController.cs b/Endpoint/Controllers/V1/DisciplineController.cs
index 056b208..1de1136 100644
--- a/Endpoint/Controllers/V1/DisciplineController.cs
+++ b/Endpoint/Controllers/V1/DisciplineController.cs
@@ -9,57 +9,57 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-namespace Mirea.Api.Endpoint.Controllers.V1
+namespace Mirea.Api.Endpoint.Controllers.V1;
+
+[ApiVersion("1.0")]
+public class DisciplineController(IMediator mediator) : BaseController
{
- public class DisciplineController(IMediator mediator) : BaseControllerV1
+ ///
+ /// Gets a paginated list of disciplines.
+ ///
+ /// Page number. Start from 0.
+ /// Number of items per page.
+ /// Paginated list of disciplines.
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [BadRequestResponse]
+ public async Task>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
{
- ///
- /// Gets a paginated list of disciplines.
- ///
- /// Page number. Start from 0.
- /// Number of items per page.
- /// Paginated list of disciplines.
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [BadRequestResponse]
- public async Task>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
+ var result = await mediator.Send(new GetDisciplineListQuery()
{
- var result = await mediator.Send(new GetDisciplineListQuery()
- {
- Page = page,
- PageSize = pageSize
- });
+ Page = page,
+ PageSize = pageSize
+ });
- return Ok(result.Disciplines
- .Select(d => new DisciplineResponse()
- {
- Id = d.Id,
- Name = d.Name
- })
- );
- }
-
- ///
- /// Gets details of a specific discipline by ID.
- ///
- /// Discipline ID.
- /// Details of the specified discipline.
- [HttpGet("{id:int}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [BadRequestResponse]
- [NotFoundResponse]
- public async Task> GetDetails(int id)
- {
- var result = await mediator.Send(new GetDisciplineInfoQuery()
+ return Ok(result.Disciplines
+ .Select(d => new DisciplineResponse()
{
- Id = id
- });
-
- return Ok(new DisciplineResponse()
- {
- Id = result.Id,
- Name = result.Name
- });
- }
+ Id = d.Id,
+ Name = d.Name
+ })
+ );
}
-}
+
+ ///
+ /// Gets details of a specific discipline by ID.
+ ///
+ /// Discipline ID.
+ /// Details of the specified discipline.
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [BadRequestResponse]
+ [NotFoundResponse]
+ public async Task> GetDetails(int id)
+ {
+ var result = await mediator.Send(new GetDisciplineInfoQuery()
+ {
+ Id = id
+ });
+
+ return Ok(new DisciplineResponse()
+ {
+ Id = result.Id,
+ Name = result.Name
+ });
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Controllers/V1/FacultyController.cs b/Endpoint/Controllers/V1/FacultyController.cs
index 3b4cd61..9652551 100644
--- a/Endpoint/Controllers/V1/FacultyController.cs
+++ b/Endpoint/Controllers/V1/FacultyController.cs
@@ -9,61 +9,61 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-namespace Mirea.Api.Endpoint.Controllers.V1
+namespace Mirea.Api.Endpoint.Controllers.V1;
+
+[ApiVersion("1.0")]
+public class FacultyController(IMediator mediator) : BaseController
{
- public class FacultyController(IMediator mediator) : BaseControllerV1
+ ///
+ /// Gets a paginated list of faculties.
+ ///
+ /// Page number. Start from 0.
+ /// Number of items per page.
+ /// Paginated list of faculties.
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [BadRequestResponse]
+ public async Task>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
{
- ///
- /// Gets a paginated list of faculties.
- ///
- /// Page number. Start from 0.
- /// Number of items per page.
- /// Paginated list of faculties.
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [BadRequestResponse]
- public async Task>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
+ var result = await mediator.Send(new GetFacultyListQuery()
{
- var result = await mediator.Send(new GetFacultyListQuery()
- {
- Page = page,
- PageSize = pageSize
- });
+ Page = page,
+ PageSize = pageSize
+ });
- return Ok(result.Faculties
- .Select(f => new FacultyResponse()
- {
- Id = f.Id,
- Name = f.Name,
- CampusId = f.CampusId
- })
- );
- }
-
- ///
- /// Gets details of a specific faculty by ID.
- ///
- /// Faculty ID.
- /// Details of the specified faculty.
- [HttpGet("{id:int}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [BadRequestResponse]
- [NotFoundResponse]
- public async Task> GetDetails(int id)
- {
- var result = await mediator.Send(new GetFacultyInfoQuery()
+ return Ok(result.Faculties
+ .Select(f => new FacultyResponse()
{
- Id = id
- });
-
- return Ok(new FacultyDetailsResponse()
- {
- Id = result.Id,
- Name = result.Name,
- CampusId = result.CampusId,
- CampusCode = result.CampusCode,
- CampusName = result.CampusName
- });
- }
+ Id = f.Id,
+ Name = f.Name,
+ CampusId = f.CampusId
+ })
+ );
}
-}
+
+ ///
+ /// Gets details of a specific faculty by ID.
+ ///
+ /// Faculty ID.
+ /// Details of the specified faculty.
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [BadRequestResponse]
+ [NotFoundResponse]
+ public async Task> GetDetails(int id)
+ {
+ var result = await mediator.Send(new GetFacultyInfoQuery()
+ {
+ Id = id
+ });
+
+ return Ok(new FacultyDetailsResponse()
+ {
+ Id = result.Id,
+ Name = result.Name,
+ CampusId = result.CampusId,
+ CampusCode = result.CampusCode,
+ CampusName = result.CampusName
+ });
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Controllers/V1/GroupController.cs b/Endpoint/Controllers/V1/GroupController.cs
index cead029..c12f3da 100644
--- a/Endpoint/Controllers/V1/GroupController.cs
+++ b/Endpoint/Controllers/V1/GroupController.cs
@@ -10,99 +10,99 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-namespace Mirea.Api.Endpoint.Controllers.V1
+namespace Mirea.Api.Endpoint.Controllers.V1;
+
+[ApiVersion("1.0")]
+public class GroupController(IMediator mediator) : BaseController
{
- public class GroupController(IMediator mediator) : BaseControllerV1
+ private static int GetCourseNumber(string groupName)
{
- private static int GetCourseNumber(string groupName)
- {
- var current = DateTime.Now;
- if (!int.TryParse(groupName[2..], out var yearOfGroup)
- && !int.TryParse(groupName.Split('-')[^1][..2], out yearOfGroup))
- return -1;
+ var current = DateTime.Now;
+ if (!int.TryParse(groupName[2..], out var yearOfGroup)
+ && !int.TryParse(groupName.Split('-')[^1][..2], out yearOfGroup))
+ return -1;
- // Convert a two-digit year to a four-digit one
- yearOfGroup += current.Year / 100 * 100;
+ // Convert a two-digit year to a four-digit one
+ yearOfGroup += current.Year / 100 * 100;
- return current.Year - yearOfGroup + (current.Month < 9 ? 0 : 1);
- }
-
- ///
- /// Retrieves a list of groups.
- ///
- /// The page number for pagination (optional).
- /// The page size for pagination (optional).
- /// A list of groups.
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [BadRequestResponse]
- public async Task>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
- {
- var result = await mediator.Send(new GetGroupListQuery()
- {
- Page = page,
- PageSize = pageSize
- });
-
- return Ok(result.Groups
- .Select(g => new GroupResponse()
- {
- Id = g.Id,
- Name = g.Name,
- FacultyId = g.FacultyId,
- CourseNumber = GetCourseNumber(g.Name)
- })
- );
- }
-
- ///
- /// Retrieves detailed information about a specific group.
- ///
- /// The ID of the group to retrieve.
- /// Detailed information about the group.
- [HttpGet("{id:int}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [BadRequestResponse]
- [NotFoundResponse]
- public async Task> GetDetails(int id)
- {
- var result = await mediator.Send(new GetGroupInfoQuery()
- {
- Id = id
- });
-
- return Ok(new GroupDetailsResponse()
- {
- Id = result.Id,
- Name = result.Name,
- FacultyId = result.FacultyId,
- FacultyName = result.Faculty,
- CourseNumber = GetCourseNumber(result.Name)
- });
- }
-
- ///
- /// Retrieves a list of groups by faculty ID.
- ///
- /// The ID of the faculty.
- /// A list of groups belonging to the specified faculty.
- [HttpGet("{id:int}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [BadRequestResponse]
- [NotFoundResponse]
- public async Task>> GetByFaculty(int id)
- {
- var result = await mediator.Send(new GetGroupListQuery());
-
- return Ok(result.Groups
- .Where(g => g.FacultyId == id)
- .Select(g => new GroupResponse()
- {
- Id = g.Id,
- Name = g.Name,
- CourseNumber = GetCourseNumber(g.Name),
- FacultyId = g.FacultyId
- }));
- }
+ return current.Year - yearOfGroup + (current.Month < 9 ? 0 : 1);
}
-}
+
+ ///
+ /// Retrieves a list of groups.
+ ///
+ /// The page number for pagination (optional).
+ /// The page size for pagination (optional).
+ /// A list of groups.
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [BadRequestResponse]
+ public async Task>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
+ {
+ var result = await mediator.Send(new GetGroupListQuery()
+ {
+ Page = page,
+ PageSize = pageSize
+ });
+
+ return Ok(result.Groups
+ .Select(g => new GroupResponse()
+ {
+ Id = g.Id,
+ Name = g.Name,
+ FacultyId = g.FacultyId,
+ CourseNumber = GetCourseNumber(g.Name)
+ })
+ );
+ }
+
+ ///
+ /// Retrieves detailed information about a specific group.
+ ///
+ /// The ID of the group to retrieve.
+ /// Detailed information about the group.
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [BadRequestResponse]
+ [NotFoundResponse]
+ public async Task> GetDetails(int id)
+ {
+ var result = await mediator.Send(new GetGroupInfoQuery()
+ {
+ Id = id
+ });
+
+ return Ok(new GroupDetailsResponse()
+ {
+ Id = result.Id,
+ Name = result.Name,
+ FacultyId = result.FacultyId,
+ FacultyName = result.Faculty,
+ CourseNumber = GetCourseNumber(result.Name)
+ });
+ }
+
+ ///
+ /// Retrieves a list of groups by faculty ID.
+ ///
+ /// The ID of the faculty.
+ /// A list of groups belonging to the specified faculty.
+ [HttpGet("GetByFaculty/{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [BadRequestResponse]
+ [NotFoundResponse]
+ public async Task>> GetByFaculty(int id)
+ {
+ var result = await mediator.Send(new GetGroupListQuery());
+
+ return Ok(result.Groups
+ .Where(g => g.FacultyId == id)
+ .Select(g => new GroupResponse()
+ {
+ Id = g.Id,
+ Name = g.Name,
+ CourseNumber = GetCourseNumber(g.Name),
+ FacultyId = g.FacultyId
+ }));
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Controllers/V1/LectureHallController.cs b/Endpoint/Controllers/V1/LectureHallController.cs
index de2f21f..c6e67b8 100644
--- a/Endpoint/Controllers/V1/LectureHallController.cs
+++ b/Endpoint/Controllers/V1/LectureHallController.cs
@@ -9,76 +9,76 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-namespace Mirea.Api.Endpoint.Controllers.V1
+namespace Mirea.Api.Endpoint.Controllers.V1;
+
+[ApiVersion("1.0")]
+public class LectureHallController(IMediator mediator) : BaseController
{
- public class LectureHallController(IMediator mediator) : BaseControllerV1
+ ///
+ /// Retrieves a list of all lecture halls.
+ ///
+ /// A list of lecture halls.
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> Get()
{
- ///
- /// Retrieves a list of all lecture halls.
- ///
- /// A list of lecture halls.
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task>> Get()
- {
- var result = await mediator.Send(new GetLectureHallListQuery());
+ var result = await mediator.Send(new GetLectureHallListQuery());
- return Ok(result.LectureHalls
- .Select(l => new LectureHallResponse()
- {
- Id = l.Id,
- Name = l.Name,
- CampusId = l.CampusId
- })
- );
- }
-
- ///
- /// Retrieves details of a specific lecture hall by its ID.
- ///
- /// The ID of the lecture hall to retrieve.
- /// The details of the specified lecture hall.
- [HttpGet("{id:int}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [BadRequestResponse]
- [NotFoundResponse]
- public async Task> GetDetails(int id)
- {
- var result = await mediator.Send(new GetLectureHallInfoQuery()
+ return Ok(result.LectureHalls
+ .Select(l => new LectureHallResponse()
{
- Id = id
- });
-
- return Ok(new LectureHallDetailsResponse()
- {
- Id = result.Id,
- Name = result.Name,
- CampusId = result.CampusId,
- CampusCode = result.CampusCode,
- CampusName = result.CampusName
- });
- }
-
- ///
- /// Retrieves a list of lecture halls by campus ID.
- ///
- /// The ID of the campus.
- /// A list of lecture halls in the specified campus.
- [HttpGet("{id:int}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [BadRequestResponse]
- [NotFoundResponse]
- public async Task>> GetByCampus(int id)
- {
- var result = await mediator.Send(new GetLectureHallListQuery());
-
- return Ok(result.LectureHalls.Where(l => l.CampusId == id)
- .Select(l => new LectureHallResponse()
- {
- Id = l.Id,
- Name = l.Name,
- CampusId = l.CampusId
- }));
- }
+ Id = l.Id,
+ Name = l.Name,
+ CampusId = l.CampusId
+ })
+ );
}
-}
+
+ ///
+ /// Retrieves details of a specific lecture hall by its ID.
+ ///
+ /// The ID of the lecture hall to retrieve.
+ /// The details of the specified lecture hall.
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [BadRequestResponse]
+ [NotFoundResponse]
+ public async Task> GetDetails(int id)
+ {
+ var result = await mediator.Send(new GetLectureHallInfoQuery()
+ {
+ Id = id
+ });
+
+ return Ok(new LectureHallDetailsResponse()
+ {
+ Id = result.Id,
+ Name = result.Name,
+ CampusId = result.CampusId,
+ CampusCode = result.CampusCode,
+ CampusName = result.CampusName
+ });
+ }
+
+ ///
+ /// Retrieves a list of lecture halls by campus ID.
+ ///
+ /// The ID of the campus.
+ /// A list of lecture halls in the specified campus.
+ [HttpGet("GetByCampus/{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [BadRequestResponse]
+ [NotFoundResponse]
+ public async Task>> GetByCampus(int id)
+ {
+ var result = await mediator.Send(new GetLectureHallListQuery());
+
+ return Ok(result.LectureHalls.Where(l => l.CampusId == id)
+ .Select(l => new LectureHallResponse()
+ {
+ Id = l.Id,
+ Name = l.Name,
+ CampusId = l.CampusId
+ }));
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Controllers/V1/ProfessorController.cs b/Endpoint/Controllers/V1/ProfessorController.cs
index 5a9053a..3093a38 100644
--- a/Endpoint/Controllers/V1/ProfessorController.cs
+++ b/Endpoint/Controllers/V1/ProfessorController.cs
@@ -11,7 +11,8 @@ using System.Threading.Tasks;
namespace Mirea.Api.Endpoint.Controllers.V1;
-public class ProfessorController(IMediator mediator) : BaseControllerV1
+[ApiVersion("1.0")]
+public class ProfessorController(IMediator mediator) : BaseController
{
///
/// Retrieves a list of professors.
diff --git a/Endpoint/Controllers/V1/ScheduleController.cs b/Endpoint/Controllers/V1/ScheduleController.cs
index 1500efa..904e3d2 100644
--- a/Endpoint/Controllers/V1/ScheduleController.cs
+++ b/Endpoint/Controllers/V1/ScheduleController.cs
@@ -1,19 +1,31 @@
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
using Mirea.Api.DataAccess.Application.Cqrs.Schedule.Queries.GetScheduleList;
+using Mirea.Api.Dto.Common;
using Mirea.Api.Dto.Requests;
using Mirea.Api.Dto.Responses;
using Mirea.Api.Dto.Responses.Schedule;
using Mirea.Api.Endpoint.Common.Attributes;
+using Mirea.Api.Endpoint.Common.Services;
+using Mirea.Api.Endpoint.Configuration.General;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Mirea.Api.Endpoint.Controllers.V1;
-public class ScheduleController(IMediator mediator) : BaseControllerV1
+[ApiVersion("1.0")]
+public class ScheduleController(IMediator mediator, IOptionsSnapshot config) : BaseController
{
+ [HttpGet("StartTerm")]
+ public ActionResult GetStartTerm() => config.Value.ScheduleSettings!.StartTerm;
+
+ [HttpGet("PairPeriod")]
+ public ActionResult> GetPairPeriod() => config.Value.ScheduleSettings!.PairPeriod.ConvertToDto();
+
///
/// Retrieves schedules based on various filters.
///
@@ -25,7 +37,6 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
[BadRequestResponse]
[NotFoundResponse]
public async Task>> Get([FromBody] ScheduleRequest request)
-
{
if ((request.Groups == null || request.Groups.Length == 0) &&
(request.Disciplines == null || request.Disciplines.Length == 0) &&
@@ -85,7 +96,7 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
/// An array of professor IDs.
/// An array of lecture hall IDs.
/// A response containing schedules for the specified group.
- [HttpGet("{id:int}")]
+ [HttpGet("GetByGroup/{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[BadRequestResponse]
@@ -142,7 +153,7 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
/// An array of group IDs.
/// An array of lecture hall IDs.
/// A response containing schedules for the specified professor.
- [HttpGet("{id:int}")]
+ [HttpGet("GetByProfessor/{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[BadRequestResponse]
@@ -203,7 +214,7 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
/// An array of professor IDs.
/// An array of group IDs.
/// A response containing schedules for the specified lecture hall.
- [HttpGet("{id:int}")]
+ [HttpGet("GetByLectureHall/{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[BadRequestResponse]
@@ -264,7 +275,7 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
/// An array of professor IDs.
/// An array of lecture hall IDs.
/// A response containing schedules for the specified discipline.
- [HttpGet("{id:int}")]
+ [HttpGet("GetByDiscipline/{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[BadRequestResponse]
diff --git a/Endpoint/Endpoint.csproj b/Endpoint/Endpoint.csproj
index 0191a2e..bd7ee16 100644
--- a/Endpoint/Endpoint.csproj
+++ b/Endpoint/Endpoint.csproj
@@ -22,15 +22,21 @@
-
-
+
+
+
+
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Endpoint/Middleware/CustomExceptionHandlerMiddleware.cs b/Endpoint/Middleware/CustomExceptionHandlerMiddleware.cs
new file mode 100644
index 0000000..62c41e2
--- /dev/null
+++ b/Endpoint/Middleware/CustomExceptionHandlerMiddleware.cs
@@ -0,0 +1,60 @@
+using FluentValidation;
+using Microsoft.AspNetCore.Http;
+using Mirea.Api.DataAccess.Application.Common.Exceptions;
+using Mirea.Api.Dto.Responses;
+using Mirea.Api.Endpoint.Common.Exceptions;
+using System;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace Mirea.Api.Endpoint.Middleware;
+
+public class CustomExceptionHandlerMiddleware(RequestDelegate next)
+{
+ public async Task Invoke(HttpContext context)
+ {
+ try
+ {
+ await next(context);
+ }
+ catch (Exception exception)
+ {
+ await HandleExceptionAsync(context, exception);
+ }
+ }
+
+ private static Task HandleExceptionAsync(HttpContext context, Exception exception)
+ {
+ var code = StatusCodes.Status500InternalServerError;
+ var result = string.Empty;
+ switch (exception)
+ {
+ case ValidationException validationException:
+ code = StatusCodes.Status400BadRequest;
+ result = JsonSerializer.Serialize(new ErrorResponse()
+ {
+ Error = validationException.Message,
+ Code = code
+ });
+ break;
+ case NotFoundException:
+ code = StatusCodes.Status404NotFound;
+ break;
+ case ControllerArgumentException:
+ code = StatusCodes.Status400BadRequest;
+ break;
+ }
+
+ context.Response.ContentType = "application/json";
+ context.Response.StatusCode = code;
+
+ if (string.IsNullOrEmpty(result))
+ result = JsonSerializer.Serialize(new ErrorResponse()
+ {
+ Error = exception.Message,
+ Code = code
+ });
+
+ return context.Response.WriteAsync(result);
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Middleware/MaintenanceModeMiddleware.cs b/Endpoint/Middleware/MaintenanceModeMiddleware.cs
new file mode 100644
index 0000000..2bd5d3b
--- /dev/null
+++ b/Endpoint/Middleware/MaintenanceModeMiddleware.cs
@@ -0,0 +1,39 @@
+using Microsoft.AspNetCore.Http;
+using Mirea.Api.Endpoint.Common.Attributes;
+using Mirea.Api.Endpoint.Common.Interfaces;
+using System.Threading.Tasks;
+
+namespace Mirea.Api.Endpoint.Middleware;
+
+public class MaintenanceModeMiddleware(RequestDelegate next, IMaintenanceModeService maintenanceModeService, IMaintenanceModeNotConfigureService maintenanceModeNotConfigureService)
+{
+ private static bool IsIgnoreMaintenanceMode(HttpContext context)
+ {
+ var endpoint = context.GetEndpoint();
+ return endpoint?.Metadata.GetMetadata() != null;
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ if (!maintenanceModeService.IsMaintenanceMode && !maintenanceModeNotConfigureService.IsMaintenanceMode || IsIgnoreMaintenanceMode(context))
+ await next(context);
+ else
+ {
+
+ context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
+ context.Response.ContentType = "plain/text";
+
+ string error;
+ if (maintenanceModeService.IsMaintenanceMode)
+ {
+ context.Response.Headers.RetryAfter = "600";
+ error = "The service is currently undergoing maintenance. Please try again later.";
+ }
+ else
+ error =
+ "The service is currently not configured. Go to the setup page if you are an administrator or try again later.";
+
+ await context.Response.WriteAsync(error);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Endpoint/Program.cs b/Endpoint/Program.cs
index 3607500..87756d9 100644
--- a/Endpoint/Program.cs
+++ b/Endpoint/Program.cs
@@ -1,3 +1,4 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
@@ -6,15 +7,26 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
using Mirea.Api.DataAccess.Application;
using Mirea.Api.DataAccess.Persistence;
+using Mirea.Api.DataAccess.Persistence.Common;
+using Mirea.Api.Endpoint.Common.Interfaces;
+using Mirea.Api.Endpoint.Common.Services;
+using Mirea.Api.Endpoint.Common.Services.Security;
using Mirea.Api.Endpoint.Configuration;
-using Mirea.Api.Endpoint.Properties;
+using Mirea.Api.Endpoint.Configuration.General;
+using Mirea.Api.Endpoint.Configuration.General.Settings;
+using Mirea.Api.Endpoint.Configuration.General.Validators;
+using Mirea.Api.Endpoint.Configuration.Swagger;
+using Mirea.Api.Endpoint.Middleware;
+using Mirea.Api.Security.Common.Interfaces;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections;
using System.IO;
using System.Linq;
+using System.Text;
namespace Mirea.Api.Endpoint;
@@ -35,18 +47,83 @@ public class Program
return result.Build();
}
+ private static IServiceCollection ConfigureJwtToken(IServiceCollection services, IConfiguration configuration)
+ {
+ var lifeTimeJwt = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_JWT"]!));
+
+ var jwtDecrypt = Encoding.UTF8.GetBytes(configuration["SECURITY_ENCRYPTION_TOKEN"] ?? string.Empty);
+
+ if (jwtDecrypt.Length != 32)
+ throw new InvalidOperationException("The secret token \"SECURITY_ENCRYPTION_TOKEN\" cannot be less than 32 characters long. Now the size is equal is " + jwtDecrypt.Length);
+
+ var jwtKey = Encoding.UTF8.GetBytes(configuration["SECURITY_SIGNING_TOKEN"] ?? string.Empty);
+
+ if (jwtKey.Length != 64)
+ throw new InvalidOperationException("The signature token \"SECURITY_SIGNING_TOKEN\" cannot be less than 64 characters. Now the size is " + jwtKey.Length);
+
+ var jwtIssuer = configuration["SECURITY_JWT_ISSUER"];
+ var jwtAudience = configuration["SECURITY_JWT_AUDIENCE"];
+
+ if (string.IsNullOrEmpty(jwtAudience) || string.IsNullOrEmpty(jwtIssuer))
+ throw new InvalidOperationException("The \"SECURITY_JWT_ISSUER\" and \"SECURITY_JWT_AUDIENCE\" are not specified");
+
+ services.AddSingleton(_ => new JwtTokenService
+ {
+ Audience = jwtAudience,
+ Issuer = jwtIssuer,
+ Lifetime = lifeTimeJwt,
+ EncryptionKey = jwtDecrypt,
+ SigningKey = jwtKey
+ });
+
+ services.AddAuthentication(options =>
+ {
+ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+ }).AddJwtBearer(options =>
+ {
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidIssuer = jwtIssuer,
+
+ ValidateAudience = true,
+ ValidAudience = jwtAudience,
+
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = new SymmetricSecurityKey(jwtKey),
+ TokenDecryptionKey = new SymmetricSecurityKey(jwtDecrypt)
+ };
+ });
+
+ return services;
+ }
+
+ private static IServiceCollection ConfigureSecurity(IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+
+ return services;
+ }
public static void Main(string[] args)
{
Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddConfiguration(ConfigureEnvironment());
- builder.Configuration.AddJsonFile(Settings.FilePath, optional: true, reloadOnChange: true);
+ builder.Configuration.AddJsonFile(PathBuilder.Combine(GeneralConfig.FilePath), optional: true, reloadOnChange: true);
+ builder.Services.Configure(builder.Configuration);
+ var generalConfig = builder.Configuration.Get();
builder.Services.AddApplication();
- builder.Services.AddPersistence(builder.Configuration);
+ builder.Services.AddPersistence(generalConfig?.DbSettings?.DatabaseProvider ?? DatabaseProvider.Sqlite, generalConfig?.DbSettings?.ConnectionStringSql ?? string.Empty);
builder.Services.AddControllers();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
@@ -92,8 +169,25 @@ public class Program
Console.WriteLine($"{item.Key}:{item.Value}");
#endif
- var uber = app.Services.CreateScope().ServiceProvider.GetService();
- DbInitializer.Initialize(uber!);
+ using (var scope = app.Services.CreateScope())
+ {
+ var serviceProvider = scope.ServiceProvider;
+
+ var optionsSnapshot = serviceProvider.GetRequiredService>();
+ var settingsValidator = new SettingsRequiredValidator(optionsSnapshot);
+ var isDoneConfig = settingsValidator.AreSettingsValid();
+
+ if (isDoneConfig)
+ {
+ var uberDbContext = serviceProvider.GetRequiredService();
+ var maintenanceModeService = serviceProvider.GetRequiredService();
+
+ maintenanceModeService.DisableMaintenanceMode();
+ DbInitializer.Initialize(uberDbContext);
+
+ // todo: if admin not found
+ }
+ }
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
@@ -111,6 +205,8 @@ public class Program
}
});
}
+ app.UseMiddleware();
+ app.UseMiddleware();
app.UseHttpsRedirection();
diff --git a/Endpoint/Properties/Settings.cs b/Endpoint/Properties/Settings.cs
deleted file mode 100644
index 1bee892..0000000
--- a/Endpoint/Properties/Settings.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using Mirea.Api.DataAccess.Persistence.Properties;
-
-namespace Mirea.Api.Endpoint.Properties;
-
-public class EmailSettings
-{
- public string? Server { get; set; }
- public string? User { get; set; }
- public string? Password { get; set; }
- public string? From { get; set; }
- public int? Port { get; set; }
- public bool? Ssl { get; set; }
-}
-
-public class LogSettings
-{
- public bool EnableLogToFile { get; set; }
- public string? LogFilePath { get; set; }
- public string? LogFileName { get; set; }
-}
-
-public class ScheduleSettings
-{
- // Every 6 hours
- public string CronUpdateSchedule { get; set; } = "0 0 0/6 * * *";
-}
-
-public class Settings
-{
- public const string FilePath = "Settings.json";
-
- public EmailSettings? EmailSettings { get; set; }
- public LogSettings? LogSettings { get; set; }
- public DbSettings? DbSettings { get; set; }
- public ScheduleSettings? ScheduleSettings { get; set; }
-}
\ No newline at end of file
diff --git a/Persistence/DependencyInjection.cs b/Persistence/DependencyInjection.cs
deleted file mode 100644
index ed6f554..0000000
--- a/Persistence/DependencyInjection.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Mirea.Api.DataAccess.Application.Interfaces.DbContexts.Schedule;
-using Mirea.Api.DataAccess.Persistence.Contexts.Schedule;
-using Mirea.Api.DataAccess.Persistence.Properties;
-using System;
-using System.Collections.Generic;
-
-namespace Mirea.Api.DataAccess.Persistence;
-
-public static class DependencyInjection
-{
- public static IServiceCollection AddPersistence(this IServiceCollection services, IConfiguration configuration)
- {
- var settings = configuration.GetSection(nameof(DbSettings)).Get();
- var connection = settings?.ConnectionStringSql;
-
- Dictionary> dbConfigurations = new()
- {
- {
- DatabaseEnum.Mysql,
- options => options.UseMySql(connection, ServerVersion.AutoDetect(connection))
- },
- {
- DatabaseEnum.Sqlite,
- options => options.UseSqlite(connection)
- },
- {
- DatabaseEnum.PostgresSql,
- options => options.UseNpgsql(connection)
- }
- };
-
- if (dbConfigurations.TryGetValue((DatabaseEnum)settings?.TypeDatabase!, out var dbConfig))
- {
- services.AddDbContext(dbConfig);
- services.AddDbContext(dbConfig);
- services.AddDbContext(dbConfig);
- services.AddDbContext(dbConfig);
- services.AddDbContext(dbConfig);
- services.AddDbContext(dbConfig);
- services.AddDbContext(dbConfig);
- services.AddDbContext(dbConfig);
- services.AddDbContext(dbConfig);
- services.AddDbContext(dbConfig);
-
- services.AddDbContext(dbConfig);
- }
- else
- throw new NotSupportedException("Unsupported database type");
-
- services.AddScoped(provider => provider.GetService()!);
- services.AddScoped(provider => provider.GetService()!);
- services.AddScoped(provider => provider.GetService()!);
- services.AddScoped(provider => provider.GetService()!);
- services.AddScoped(provider => provider.GetService()!);
- services.AddScoped(provider => provider.GetService()!);
- services.AddScoped(provider => provider.GetService()!);
- services.AddScoped(provider => provider.GetService()!);
- services.AddScoped(provider => provider.GetService()!);
- services.AddScoped(provider => provider.GetService()!);
-
- return services;
- }
-}
\ No newline at end of file
diff --git a/Persistence/Properties/DbSettings.cs b/Persistence/Properties/DbSettings.cs
deleted file mode 100644
index 7ee06d9..0000000
--- a/Persistence/Properties/DbSettings.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Mirea.Api.DataAccess.Persistence.Properties;
-
-public enum DatabaseEnum
-{
- Mysql,
- Sqlite,
- PostgresSql
-}
-public class DbSettings
-{
- public bool IsDoneConfiguration { get; set; }
- public DatabaseEnum TypeDatabase { get; set; }
- public required string ConnectionStringSql { get; set; }
- public DatabaseEnum? MigrateTo { get; set; }
-}
\ No newline at end of file
diff --git a/Security/Common/Domain/AuthToken.cs b/Security/Common/Domain/AuthToken.cs
new file mode 100644
index 0000000..4572e62
--- /dev/null
+++ b/Security/Common/Domain/AuthToken.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Mirea.Api.Security.Common.Domain;
+
+public class AuthToken
+{
+ public required string RefreshToken { get; set; }
+ public required string UserAgent { get; set; }
+ public required string Ip { get; set; }
+ public required string UserId { get; set; }
+ public required string AccessToken { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
\ No newline at end of file
diff --git a/Security/Common/Domain/PreAuthToken.cs b/Security/Common/Domain/PreAuthToken.cs
new file mode 100644
index 0000000..f1f9684
--- /dev/null
+++ b/Security/Common/Domain/PreAuthToken.cs
@@ -0,0 +1,10 @@
+namespace Mirea.Api.Security.Common.Domain;
+
+public class PreAuthToken
+{
+ public required string Fingerprint { get; set; }
+ public required string UserAgent { get; set; }
+ public required string UserId { get; set; }
+ public required string Ip { get; set; }
+ public required string Token { get; set; }
+}
\ No newline at end of file
diff --git a/Security/Common/Dto/Requests/TokenRequest.cs b/Security/Common/Dto/Requests/TokenRequest.cs
new file mode 100644
index 0000000..8be8038
--- /dev/null
+++ b/Security/Common/Dto/Requests/TokenRequest.cs
@@ -0,0 +1,8 @@
+namespace Mirea.Api.Security.Common.Dto.Requests;
+
+public class TokenRequest
+{
+ public required string Fingerprint { get; set; }
+ public required string UserAgent { get; set; }
+ public required string Ip { get; set; }
+}
\ No newline at end of file
diff --git a/Security/Common/Dto/Responses/AuthTokenResponse.cs b/Security/Common/Dto/Responses/AuthTokenResponse.cs
new file mode 100644
index 0000000..0c8a3d4
--- /dev/null
+++ b/Security/Common/Dto/Responses/AuthTokenResponse.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Mirea.Api.Security.Common.Dto.Responses;
+
+public class AuthTokenResponse
+{
+ public required string AccessToken { get; set; }
+ public required string RefreshToken { get; set; }
+ public DateTime ExpiresIn { get; set; }
+}
\ No newline at end of file
diff --git a/Security/Common/Dto/Responses/PreAuthTokenResponse.cs b/Security/Common/Dto/Responses/PreAuthTokenResponse.cs
new file mode 100644
index 0000000..9a7238f
--- /dev/null
+++ b/Security/Common/Dto/Responses/PreAuthTokenResponse.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Mirea.Api.Security.Common.Dto.Responses;
+
+public class PreAuthTokenResponse
+{
+ public required string Token { get; set; }
+ public DateTime ExpiresIn { get; set; }
+}
\ No newline at end of file
diff --git a/Security/Common/Interfaces/IAccessToken.cs b/Security/Common/Interfaces/IAccessToken.cs
new file mode 100644
index 0000000..a2ebed2
--- /dev/null
+++ b/Security/Common/Interfaces/IAccessToken.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Mirea.Api.Security.Common.Interfaces;
+
+public interface IAccessToken
+{
+ (string Token, DateTime ExpireIn) GenerateToken(string userId);
+ DateTimeOffset GetExpireDateTime(string token);
+}
\ No newline at end of file
diff --git a/Security/Common/Interfaces/ICacheService.cs b/Security/Common/Interfaces/ICacheService.cs
new file mode 100644
index 0000000..c2cb1e3
--- /dev/null
+++ b/Security/Common/Interfaces/ICacheService.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Mirea.Api.Security.Common.Interfaces;
+
+public interface ICacheService
+{
+ Task SetAsync(string key, T value,
+ TimeSpan? absoluteExpirationRelativeToNow = null,
+ TimeSpan? slidingExpiration = null,
+ CancellationToken cancellationToken = default);
+
+ Task GetAsync(string key, CancellationToken cancellationToken = default);
+ Task RemoveAsync(string key, CancellationToken cancellationToken = default);
+}
diff --git a/Security/Common/Interfaces/IRevokedToken.cs b/Security/Common/Interfaces/IRevokedToken.cs
new file mode 100644
index 0000000..d8d9edc
--- /dev/null
+++ b/Security/Common/Interfaces/IRevokedToken.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Mirea.Api.Security.Common.Interfaces;
+
+public interface IRevokedToken
+{
+ Task AddTokenToRevokedAsync(string token, DateTimeOffset expiresIn);
+ Task IsTokenRevokedAsync(string token);
+}
\ No newline at end of file
diff --git a/Security/DependencyInjection.cs b/Security/DependencyInjection.cs
new file mode 100644
index 0000000..ed16c5e
--- /dev/null
+++ b/Security/DependencyInjection.cs
@@ -0,0 +1,57 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Mirea.Api.Security.Common.Interfaces;
+using Mirea.Api.Security.Services;
+using System;
+
+namespace Mirea.Api.Security;
+
+public static class DependencyInjection
+{
+ public static IServiceCollection AddSecurityServices(this IServiceCollection services, IConfiguration configuration)
+ {
+ var saltSize = int.Parse(configuration["SECURITY_SALT_SIZE"]!);
+ var hashSize = int.Parse(configuration["SECURITY_HASH_SIZE"]!);
+ var iteration = int.Parse(configuration["SECURITY_HASH_ITERATION"]!);
+ var memory = int.Parse(configuration["SECURITY_HASH_MEMORY"]!);
+ var parallelism = int.Parse(configuration["SECURITY_HASH_PARALLELISM"]!);
+
+ services.AddSingleton(new PasswordHashService
+ {
+ SaltSize = saltSize,
+ HashSize = hashSize,
+ Iterations = iteration,
+ Memory = memory,
+ Parallelism = parallelism,
+ Secret = configuration["SECURITY_HASH_TOKEN"]
+ });
+
+ var lifeTimePreAuthToken = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_1_FA"]!));
+
+ services.AddSingleton(provider =>
+ {
+ var cache = provider.GetRequiredService();
+
+ return new PreAuthService(cache)
+ {
+ Lifetime = lifeTimePreAuthToken
+ };
+ });
+
+ var lifeTimeRefreshToken = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_RT"]!));
+
+ services.AddSingleton(provider =>
+ {
+ var cacheService = provider.GetRequiredService();
+ var accessTokenService = provider.GetRequiredService();
+ var revokedTokenService = provider.GetRequiredService();
+
+ return new AuthService(cacheService, accessTokenService, revokedTokenService)
+ {
+ Lifetime = lifeTimeRefreshToken
+ };
+ });
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/Security/Security.csproj b/Security/Security.csproj
new file mode 100644
index 0000000..218d9f6
--- /dev/null
+++ b/Security/Security.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net8.0
+ disable
+ enable
+ Winsomnia
+ 1.0.0-a0
+ 1.0.0.0
+ 1.0.0.0
+ Mirea.Api.Security
+ $(AssemblyName)
+
+
+
+
+
+
+
+
+
diff --git a/Security/Services/AuthService.cs b/Security/Services/AuthService.cs
new file mode 100644
index 0000000..5426532
--- /dev/null
+++ b/Security/Services/AuthService.cs
@@ -0,0 +1,103 @@
+using Mirea.Api.Security.Common.Domain;
+using Mirea.Api.Security.Common.Dto.Requests;
+using Mirea.Api.Security.Common.Dto.Responses;
+using Mirea.Api.Security.Common.Interfaces;
+using System;
+using System.Security;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Mirea.Api.Security.Services;
+
+public class AuthService(ICacheService cache, IAccessToken accessTokenService, IRevokedToken revokedToken)
+{
+ public TimeSpan Lifetime { private get; init; }
+
+ private static string GenerateRefreshToken() => Guid.NewGuid().ToString().Replace("-", "") +
+ GeneratorKey.GenerateString(32);
+ private (string Token, DateTime ExpireIn) GenerateAccessToken(string userId) =>
+ accessTokenService.GenerateToken(userId);
+
+ private static string GetAuthCacheKey(string fingerprint) => $"{fingerprint}_auth_token";
+
+ private Task SetAuthTokenDataToCache(string fingerprint, AuthToken data, CancellationToken cancellation) =>
+ cache.SetAsync(
+ GetAuthCacheKey(fingerprint),
+ JsonSerializer.SerializeToUtf8Bytes(data),
+ slidingExpiration: Lifetime,
+ cancellationToken: cancellation);
+
+ private Task RevokeAccessToken(string token) =>
+ revokedToken.AddTokenToRevokedAsync(token, accessTokenService.GetExpireDateTime(token));
+
+ public async Task GenerateAuthTokensAsync(TokenRequest request, string userId, CancellationToken cancellation = default)
+ {
+ var refreshToken = GenerateRefreshToken();
+ var accessToken = GenerateAccessToken(userId);
+
+ var authTokenStruct = new AuthToken
+ {
+ CreatedAt = DateTime.UtcNow,
+ Ip = request.Ip,
+ RefreshToken = refreshToken,
+ UserAgent = request.UserAgent,
+ UserId = userId,
+ AccessToken = accessToken.Token
+ };
+
+ await SetAuthTokenDataToCache(request.Fingerprint, authTokenStruct, cancellation);
+
+ return new AuthTokenResponse
+ {
+ AccessToken = accessToken.Token,
+ ExpiresIn = accessToken.ExpireIn,
+ RefreshToken = authTokenStruct.RefreshToken
+ };
+ }
+
+ public async Task GenerateAuthTokensWithPreAuthAsync(TokenRequest request, string preAuthToken,
+ CancellationToken cancellation = default) =>
+ await GenerateAuthTokensAsync(request,
+ await new PreAuthService(cache).MatchToken(request, preAuthToken, cancellation),
+ cancellation);
+
+ public async Task RefreshTokenAsync(TokenRequest request, string refreshToken, CancellationToken cancellation = default)
+ {
+ var authToken = await cache.GetAsync(GetAuthCacheKey(request.Fingerprint), cancellation)
+ ?? throw new SecurityException(request.Fingerprint);
+
+ if (authToken.RefreshToken != refreshToken ||
+ authToken.UserAgent != request.UserAgent &&
+ authToken.Ip != request.Ip)
+ {
+ await cache.RemoveAsync(request.Fingerprint, cancellation);
+ await RevokeAccessToken(authToken.AccessToken);
+
+ throw new SecurityException(request.Fingerprint);
+ }
+
+ var accessToken = GenerateAccessToken(authToken.UserId);
+ await RevokeAccessToken(authToken.AccessToken);
+
+ authToken.AccessToken = accessToken.Token;
+ await SetAuthTokenDataToCache(request.Fingerprint, authToken, cancellation);
+
+ return new AuthTokenResponse
+ {
+ AccessToken = accessToken.Token,
+ ExpiresIn = accessToken.ExpireIn,
+ RefreshToken = GenerateRefreshToken()
+ };
+ }
+
+ public async Task LogoutAsync(string fingerprint, CancellationToken cancellation = default)
+ {
+ var authTokenStruct = await cache.GetAsync(GetAuthCacheKey(fingerprint), cancellation);
+ if (authTokenStruct == null) return;
+
+ await RevokeAccessToken(authTokenStruct.AccessToken);
+
+ await cache.RemoveAsync(fingerprint, cancellation);
+ }
+}
\ No newline at end of file
diff --git a/Security/Services/GeneratorKey.cs b/Security/Services/GeneratorKey.cs
new file mode 100644
index 0000000..79a0430
--- /dev/null
+++ b/Security/Services/GeneratorKey.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Buffers.Text;
+using System.Text;
+
+namespace Mirea.Api.Security.Services;
+
+public static class GeneratorKey
+{
+ public static ReadOnlySpan GenerateBytes(int size)
+ {
+ var key = new byte[size];
+ using var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
+ rng.GetNonZeroBytes(key);
+ return key;
+ }
+
+ public static string GenerateBase64(int size) =>
+ Convert.ToBase64String(GenerateBytes(size));
+
+ public static string GenerateString(int size)
+ {
+ var randomBytes = GenerateBytes(size);
+ Span utf8Bytes = new byte[Base64.GetMaxEncodedToUtf8Length(randomBytes.Length)];
+
+ Base64.EncodeToUtf8(randomBytes, utf8Bytes, out _, out _);
+ return Encoding.UTF8.GetString(utf8Bytes);
+ }
+}
\ No newline at end of file
diff --git a/Security/Services/PasswordHashService.cs b/Security/Services/PasswordHashService.cs
new file mode 100644
index 0000000..8673222
--- /dev/null
+++ b/Security/Services/PasswordHashService.cs
@@ -0,0 +1,56 @@
+using Konscious.Security.Cryptography;
+using System;
+using System.Text;
+
+namespace Mirea.Api.Security.Services;
+
+public class PasswordHashService
+{
+ public int SaltSize { private get; init; }
+ public int HashSize { private get; init; }
+ public int Iterations { private get; init; }
+ public int Memory { private get; init; }
+ public int Parallelism { private get; init; }
+ public string? Secret { private get; init; }
+
+ private ReadOnlySpan HashPassword(string password, ReadOnlySpan salt)
+ {
+ var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password))
+ {
+ Iterations = Iterations,
+ MemorySize = Memory,
+ DegreeOfParallelism = Parallelism,
+ Salt = salt.ToArray()
+ };
+
+ if (!string.IsNullOrEmpty(Secret))
+ argon2.KnownSecret = Convert.FromBase64String(Secret);
+
+ return argon2.GetBytes(HashSize);
+ }
+
+ private static bool ConstantTimeComparison(ReadOnlySpan a, ReadOnlySpan b)
+ {
+ if (a.Length != b.Length)
+ return false;
+
+ int result = 0;
+ for (int i = 0; i < a.Length; i++)
+ result |= a[i] ^ b[i];
+ return result == 0;
+ }
+
+ public (string Salt, string Hash) HashPassword(string password)
+ {
+ var salt = GeneratorKey.GenerateBytes(SaltSize);
+ var hash = HashPassword(password, salt);
+
+ return (Convert.ToBase64String(salt), Convert.ToBase64String(hash));
+ }
+
+ public bool VerifyPassword(string password, ReadOnlySpan salt, ReadOnlySpan hash) =>
+ ConstantTimeComparison(HashPassword(password, salt), hash);
+
+ public bool VerifyPassword(string password, string saltBase64, string hashBase64) =>
+ VerifyPassword(password, Convert.FromBase64String(saltBase64), Convert.FromBase64String(hashBase64));
+}
\ No newline at end of file
diff --git a/Security/Services/PreAuthService.cs b/Security/Services/PreAuthService.cs
new file mode 100644
index 0000000..948b997
--- /dev/null
+++ b/Security/Services/PreAuthService.cs
@@ -0,0 +1,64 @@
+using Mirea.Api.Security.Common.Domain;
+using Mirea.Api.Security.Common.Dto.Requests;
+using Mirea.Api.Security.Common.Dto.Responses;
+using Mirea.Api.Security.Common.Interfaces;
+using System;
+using System.Security;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Mirea.Api.Security.Services;
+
+public class PreAuthService(ICacheService cache)
+{
+ public TimeSpan Lifetime { private get; init; }
+
+ private static string GeneratePreAuthToken() => Guid.NewGuid().ToString().Replace("-", "") +
+ GeneratorKey.GenerateString(16);
+
+ private static string GetPreAuthCacheKey(string fingerprint) => $"{fingerprint}_pre_auth_token";
+
+ public async Task GeneratePreAuthTokenAsync(TokenRequest request, string userId, CancellationToken cancellation = default)
+ {
+ var preAuthToken = GeneratePreAuthToken();
+
+ var preAuthTokenStruct = new PreAuthToken
+ {
+ Fingerprint = request.Fingerprint,
+ UserId = userId,
+ UserAgent = request.UserAgent,
+ Token = preAuthToken,
+ Ip = request.Ip
+ };
+
+ await cache.SetAsync(
+ GetPreAuthCacheKey(request.Fingerprint),
+ JsonSerializer.SerializeToUtf8Bytes(preAuthTokenStruct),
+ absoluteExpirationRelativeToNow: Lifetime,
+ cancellationToken: cancellation);
+
+ return new PreAuthTokenResponse
+ {
+ Token = preAuthToken,
+ ExpiresIn = DateTime.UtcNow.Add(Lifetime)
+ };
+ }
+ public async Task MatchToken(TokenRequest request, string preAuthToken, CancellationToken cancellation = default)
+ {
+ var preAuthTokenStruct = await cache.GetAsync(GetPreAuthCacheKey(request.Fingerprint), cancellation)
+ ?? throw new SecurityException($"The token was not found using fingerprint \"{request.Fingerprint}\"");
+
+ if (preAuthTokenStruct == null ||
+ preAuthTokenStruct.Token != preAuthToken ||
+ (preAuthTokenStruct.UserAgent != request.UserAgent &&
+ preAuthTokenStruct.Ip != request.Ip))
+ {
+ throw new SecurityException("It was not possible to verify the authenticity of the token");
+ }
+
+ await cache.RemoveAsync(GetPreAuthCacheKey(request.Fingerprint), cancellation);
+
+ return preAuthTokenStruct.UserId;
+ }
+}
\ No newline at end of file
diff --git a/Application/Application.csproj b/SqlData/Application/Application.csproj
similarity index 53%
rename from Application/Application.csproj
rename to SqlData/Application/Application.csproj
index 2003517..51f04ad 100644
--- a/Application/Application.csproj
+++ b/SqlData/Application/Application.csproj
@@ -13,15 +13,14 @@
-
-
-
-
-
+
+
+
+
-
+
\ No newline at end of file
diff --git a/Application/Common/Behaviors/ValidationBehavior.cs b/SqlData/Application/Common/Behaviors/ValidationBehavior.cs
similarity index 100%
rename from Application/Common/Behaviors/ValidationBehavior.cs
rename to SqlData/Application/Common/Behaviors/ValidationBehavior.cs
diff --git a/Application/Common/Exceptions/NotFoundException.cs b/SqlData/Application/Common/Exceptions/NotFoundException.cs
similarity index 100%
rename from Application/Common/Exceptions/NotFoundException.cs
rename to SqlData/Application/Common/Exceptions/NotFoundException.cs
diff --git a/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/CampusBasicInfoDto.cs b/SqlData/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/CampusBasicInfoDto.cs
similarity index 100%
rename from Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/CampusBasicInfoDto.cs
rename to SqlData/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/CampusBasicInfoDto.cs
diff --git a/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/CampusBasicInfoVm.cs b/SqlData/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/CampusBasicInfoVm.cs
similarity index 100%
rename from Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/CampusBasicInfoVm.cs
rename to SqlData/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/CampusBasicInfoVm.cs
diff --git a/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/GetCampusBasicInfoListQuery.cs b/SqlData/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/GetCampusBasicInfoListQuery.cs
similarity index 100%
rename from Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/GetCampusBasicInfoListQuery.cs
rename to SqlData/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/GetCampusBasicInfoListQuery.cs
diff --git a/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/GetCampusBasicInfoListQueryHandler.cs b/SqlData/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/GetCampusBasicInfoListQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/GetCampusBasicInfoListQueryHandler.cs
rename to SqlData/Application/Cqrs/Campus/Queries/GetCampusBasicInfoList/GetCampusBasicInfoListQueryHandler.cs
diff --git a/Application/Cqrs/Campus/Queries/GetCampusDetails/CampusDetailsVm.cs b/SqlData/Application/Cqrs/Campus/Queries/GetCampusDetails/CampusDetailsVm.cs
similarity index 100%
rename from Application/Cqrs/Campus/Queries/GetCampusDetails/CampusDetailsVm.cs
rename to SqlData/Application/Cqrs/Campus/Queries/GetCampusDetails/CampusDetailsVm.cs
diff --git a/Application/Cqrs/Campus/Queries/GetCampusDetails/GetCampusDetailsQuery.cs b/SqlData/Application/Cqrs/Campus/Queries/GetCampusDetails/GetCampusDetailsQuery.cs
similarity index 100%
rename from Application/Cqrs/Campus/Queries/GetCampusDetails/GetCampusDetailsQuery.cs
rename to SqlData/Application/Cqrs/Campus/Queries/GetCampusDetails/GetCampusDetailsQuery.cs
diff --git a/Application/Cqrs/Campus/Queries/GetCampusDetails/GetCampusDetailsQueryHandler.cs b/SqlData/Application/Cqrs/Campus/Queries/GetCampusDetails/GetCampusDetailsQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/Campus/Queries/GetCampusDetails/GetCampusDetailsQueryHandler.cs
rename to SqlData/Application/Cqrs/Campus/Queries/GetCampusDetails/GetCampusDetailsQueryHandler.cs
diff --git a/Application/Cqrs/Discipline/Queries/GetDisciplineDetails/DisciplineInfoVm.cs b/SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineDetails/DisciplineInfoVm.cs
similarity index 100%
rename from Application/Cqrs/Discipline/Queries/GetDisciplineDetails/DisciplineInfoVm.cs
rename to SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineDetails/DisciplineInfoVm.cs
diff --git a/Application/Cqrs/Discipline/Queries/GetDisciplineDetails/GetDisciplineInfoQuery.cs b/SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineDetails/GetDisciplineInfoQuery.cs
similarity index 100%
rename from Application/Cqrs/Discipline/Queries/GetDisciplineDetails/GetDisciplineInfoQuery.cs
rename to SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineDetails/GetDisciplineInfoQuery.cs
diff --git a/Application/Cqrs/Discipline/Queries/GetDisciplineDetails/GetDisciplineInfoQueryHandler.cs b/SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineDetails/GetDisciplineInfoQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/Discipline/Queries/GetDisciplineDetails/GetDisciplineInfoQueryHandler.cs
rename to SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineDetails/GetDisciplineInfoQueryHandler.cs
diff --git a/Application/Cqrs/Discipline/Queries/GetDisciplineList/DisciplineListVm.cs b/SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineList/DisciplineListVm.cs
similarity index 100%
rename from Application/Cqrs/Discipline/Queries/GetDisciplineList/DisciplineListVm.cs
rename to SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineList/DisciplineListVm.cs
diff --git a/Application/Cqrs/Discipline/Queries/GetDisciplineList/DisciplineLookupDto.cs b/SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineList/DisciplineLookupDto.cs
similarity index 100%
rename from Application/Cqrs/Discipline/Queries/GetDisciplineList/DisciplineLookupDto.cs
rename to SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineList/DisciplineLookupDto.cs
diff --git a/Application/Cqrs/Discipline/Queries/GetDisciplineList/GetDisciplineListQuery.cs b/SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineList/GetDisciplineListQuery.cs
similarity index 100%
rename from Application/Cqrs/Discipline/Queries/GetDisciplineList/GetDisciplineListQuery.cs
rename to SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineList/GetDisciplineListQuery.cs
diff --git a/Application/Cqrs/Discipline/Queries/GetDisciplineList/GetDisciplineListQueryHandler.cs b/SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineList/GetDisciplineListQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/Discipline/Queries/GetDisciplineList/GetDisciplineListQueryHandler.cs
rename to SqlData/Application/Cqrs/Discipline/Queries/GetDisciplineList/GetDisciplineListQueryHandler.cs
diff --git a/Application/Cqrs/Faculty/Queries/GetFacultyDetails/FacultyInfoVm.cs b/SqlData/Application/Cqrs/Faculty/Queries/GetFacultyDetails/FacultyInfoVm.cs
similarity index 100%
rename from Application/Cqrs/Faculty/Queries/GetFacultyDetails/FacultyInfoVm.cs
rename to SqlData/Application/Cqrs/Faculty/Queries/GetFacultyDetails/FacultyInfoVm.cs
diff --git a/Application/Cqrs/Faculty/Queries/GetFacultyDetails/GetFacultyInfoQuery.cs b/SqlData/Application/Cqrs/Faculty/Queries/GetFacultyDetails/GetFacultyInfoQuery.cs
similarity index 100%
rename from Application/Cqrs/Faculty/Queries/GetFacultyDetails/GetFacultyInfoQuery.cs
rename to SqlData/Application/Cqrs/Faculty/Queries/GetFacultyDetails/GetFacultyInfoQuery.cs
diff --git a/Application/Cqrs/Faculty/Queries/GetFacultyDetails/GetFacultyInfoQueryHandler.cs b/SqlData/Application/Cqrs/Faculty/Queries/GetFacultyDetails/GetFacultyInfoQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/Faculty/Queries/GetFacultyDetails/GetFacultyInfoQueryHandler.cs
rename to SqlData/Application/Cqrs/Faculty/Queries/GetFacultyDetails/GetFacultyInfoQueryHandler.cs
diff --git a/Application/Cqrs/Faculty/Queries/GetFacultyList/FacultyListVm.cs b/SqlData/Application/Cqrs/Faculty/Queries/GetFacultyList/FacultyListVm.cs
similarity index 100%
rename from Application/Cqrs/Faculty/Queries/GetFacultyList/FacultyListVm.cs
rename to SqlData/Application/Cqrs/Faculty/Queries/GetFacultyList/FacultyListVm.cs
diff --git a/Application/Cqrs/Faculty/Queries/GetFacultyList/FacultyLookupDto.cs b/SqlData/Application/Cqrs/Faculty/Queries/GetFacultyList/FacultyLookupDto.cs
similarity index 100%
rename from Application/Cqrs/Faculty/Queries/GetFacultyList/FacultyLookupDto.cs
rename to SqlData/Application/Cqrs/Faculty/Queries/GetFacultyList/FacultyLookupDto.cs
diff --git a/Application/Cqrs/Faculty/Queries/GetFacultyList/GetFacultyListQuery.cs b/SqlData/Application/Cqrs/Faculty/Queries/GetFacultyList/GetFacultyListQuery.cs
similarity index 100%
rename from Application/Cqrs/Faculty/Queries/GetFacultyList/GetFacultyListQuery.cs
rename to SqlData/Application/Cqrs/Faculty/Queries/GetFacultyList/GetFacultyListQuery.cs
diff --git a/Application/Cqrs/Faculty/Queries/GetFacultyList/GetFacultyListQueryHandler.cs b/SqlData/Application/Cqrs/Faculty/Queries/GetFacultyList/GetFacultyListQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/Faculty/Queries/GetFacultyList/GetFacultyListQueryHandler.cs
rename to SqlData/Application/Cqrs/Faculty/Queries/GetFacultyList/GetFacultyListQueryHandler.cs
diff --git a/Application/Cqrs/Group/Queries/GetGroupDetails/GetGroupInfoQuery.cs b/SqlData/Application/Cqrs/Group/Queries/GetGroupDetails/GetGroupInfoQuery.cs
similarity index 100%
rename from Application/Cqrs/Group/Queries/GetGroupDetails/GetGroupInfoQuery.cs
rename to SqlData/Application/Cqrs/Group/Queries/GetGroupDetails/GetGroupInfoQuery.cs
diff --git a/Application/Cqrs/Group/Queries/GetGroupDetails/GetGroupInfoQueryHandler.cs b/SqlData/Application/Cqrs/Group/Queries/GetGroupDetails/GetGroupInfoQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/Group/Queries/GetGroupDetails/GetGroupInfoQueryHandler.cs
rename to SqlData/Application/Cqrs/Group/Queries/GetGroupDetails/GetGroupInfoQueryHandler.cs
diff --git a/Application/Cqrs/Group/Queries/GetGroupDetails/GroupInfoVm.cs b/SqlData/Application/Cqrs/Group/Queries/GetGroupDetails/GroupInfoVm.cs
similarity index 100%
rename from Application/Cqrs/Group/Queries/GetGroupDetails/GroupInfoVm.cs
rename to SqlData/Application/Cqrs/Group/Queries/GetGroupDetails/GroupInfoVm.cs
diff --git a/Application/Cqrs/Group/Queries/GetGroupList/GetGroupListQuery.cs b/SqlData/Application/Cqrs/Group/Queries/GetGroupList/GetGroupListQuery.cs
similarity index 100%
rename from Application/Cqrs/Group/Queries/GetGroupList/GetGroupListQuery.cs
rename to SqlData/Application/Cqrs/Group/Queries/GetGroupList/GetGroupListQuery.cs
diff --git a/Application/Cqrs/Group/Queries/GetGroupList/GetGroupListQueryHandler.cs b/SqlData/Application/Cqrs/Group/Queries/GetGroupList/GetGroupListQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/Group/Queries/GetGroupList/GetGroupListQueryHandler.cs
rename to SqlData/Application/Cqrs/Group/Queries/GetGroupList/GetGroupListQueryHandler.cs
diff --git a/Application/Cqrs/Group/Queries/GetGroupList/GroupListVm.cs b/SqlData/Application/Cqrs/Group/Queries/GetGroupList/GroupListVm.cs
similarity index 100%
rename from Application/Cqrs/Group/Queries/GetGroupList/GroupListVm.cs
rename to SqlData/Application/Cqrs/Group/Queries/GetGroupList/GroupListVm.cs
diff --git a/Application/Cqrs/Group/Queries/GetGroupList/GroupLookupDto.cs b/SqlData/Application/Cqrs/Group/Queries/GetGroupList/GroupLookupDto.cs
similarity index 100%
rename from Application/Cqrs/Group/Queries/GetGroupList/GroupLookupDto.cs
rename to SqlData/Application/Cqrs/Group/Queries/GetGroupList/GroupLookupDto.cs
diff --git a/Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/GetLectureHallInfoQuery.cs b/SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/GetLectureHallInfoQuery.cs
similarity index 100%
rename from Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/GetLectureHallInfoQuery.cs
rename to SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/GetLectureHallInfoQuery.cs
diff --git a/Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/GetLectureHallInfoQueryHandler.cs b/SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/GetLectureHallInfoQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/GetLectureHallInfoQueryHandler.cs
rename to SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/GetLectureHallInfoQueryHandler.cs
diff --git a/Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/LectureHallInfoVm.cs b/SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/LectureHallInfoVm.cs
similarity index 100%
rename from Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/LectureHallInfoVm.cs
rename to SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallDetails/LectureHallInfoVm.cs
diff --git a/Application/Cqrs/LectureHall/Queries/GetLectureHallList/GetLectureHallListQuery.cs b/SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallList/GetLectureHallListQuery.cs
similarity index 100%
rename from Application/Cqrs/LectureHall/Queries/GetLectureHallList/GetLectureHallListQuery.cs
rename to SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallList/GetLectureHallListQuery.cs
diff --git a/Application/Cqrs/LectureHall/Queries/GetLectureHallList/GetLectureHallListQueryHandler.cs b/SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallList/GetLectureHallListQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/LectureHall/Queries/GetLectureHallList/GetLectureHallListQueryHandler.cs
rename to SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallList/GetLectureHallListQueryHandler.cs
diff --git a/Application/Cqrs/LectureHall/Queries/GetLectureHallList/LectureHallListVm.cs b/SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallList/LectureHallListVm.cs
similarity index 100%
rename from Application/Cqrs/LectureHall/Queries/GetLectureHallList/LectureHallListVm.cs
rename to SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallList/LectureHallListVm.cs
diff --git a/Application/Cqrs/LectureHall/Queries/GetLectureHallList/LectureHallLookupDto.cs b/SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallList/LectureHallLookupDto.cs
similarity index 100%
rename from Application/Cqrs/LectureHall/Queries/GetLectureHallList/LectureHallLookupDto.cs
rename to SqlData/Application/Cqrs/LectureHall/Queries/GetLectureHallList/LectureHallLookupDto.cs
diff --git a/Application/Cqrs/Professor/Queries/GetProfessorDetails/GetProfessorInfoQuery.cs b/SqlData/Application/Cqrs/Professor/Queries/GetProfessorDetails/GetProfessorInfoQuery.cs
similarity index 100%
rename from Application/Cqrs/Professor/Queries/GetProfessorDetails/GetProfessorInfoQuery.cs
rename to SqlData/Application/Cqrs/Professor/Queries/GetProfessorDetails/GetProfessorInfoQuery.cs
diff --git a/Application/Cqrs/Professor/Queries/GetProfessorDetails/GetProfessorInfoQueryHandler.cs b/SqlData/Application/Cqrs/Professor/Queries/GetProfessorDetails/GetProfessorInfoQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/Professor/Queries/GetProfessorDetails/GetProfessorInfoQueryHandler.cs
rename to SqlData/Application/Cqrs/Professor/Queries/GetProfessorDetails/GetProfessorInfoQueryHandler.cs
diff --git a/Application/Cqrs/Professor/Queries/GetProfessorDetails/ProfessorInfoVm.cs b/SqlData/Application/Cqrs/Professor/Queries/GetProfessorDetails/ProfessorInfoVm.cs
similarity index 100%
rename from Application/Cqrs/Professor/Queries/GetProfessorDetails/ProfessorInfoVm.cs
rename to SqlData/Application/Cqrs/Professor/Queries/GetProfessorDetails/ProfessorInfoVm.cs
diff --git a/Application/Cqrs/Professor/Queries/GetProfessorList/GetProfessorListQuery.cs b/SqlData/Application/Cqrs/Professor/Queries/GetProfessorList/GetProfessorListQuery.cs
similarity index 100%
rename from Application/Cqrs/Professor/Queries/GetProfessorList/GetProfessorListQuery.cs
rename to SqlData/Application/Cqrs/Professor/Queries/GetProfessorList/GetProfessorListQuery.cs
diff --git a/Application/Cqrs/Professor/Queries/GetProfessorList/GetProfessorListQueryHandler.cs b/SqlData/Application/Cqrs/Professor/Queries/GetProfessorList/GetProfessorListQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/Professor/Queries/GetProfessorList/GetProfessorListQueryHandler.cs
rename to SqlData/Application/Cqrs/Professor/Queries/GetProfessorList/GetProfessorListQueryHandler.cs
diff --git a/Application/Cqrs/Professor/Queries/GetProfessorList/ProfessorListVm.cs b/SqlData/Application/Cqrs/Professor/Queries/GetProfessorList/ProfessorListVm.cs
similarity index 100%
rename from Application/Cqrs/Professor/Queries/GetProfessorList/ProfessorListVm.cs
rename to SqlData/Application/Cqrs/Professor/Queries/GetProfessorList/ProfessorListVm.cs
diff --git a/Application/Cqrs/Professor/Queries/GetProfessorList/ProfessorLookupDto.cs b/SqlData/Application/Cqrs/Professor/Queries/GetProfessorList/ProfessorLookupDto.cs
similarity index 100%
rename from Application/Cqrs/Professor/Queries/GetProfessorList/ProfessorLookupDto.cs
rename to SqlData/Application/Cqrs/Professor/Queries/GetProfessorList/ProfessorLookupDto.cs
diff --git a/Application/Cqrs/Schedule/Queries/GetScheduleList/GetScheduleListQuery.cs b/SqlData/Application/Cqrs/Schedule/Queries/GetScheduleList/GetScheduleListQuery.cs
similarity index 100%
rename from Application/Cqrs/Schedule/Queries/GetScheduleList/GetScheduleListQuery.cs
rename to SqlData/Application/Cqrs/Schedule/Queries/GetScheduleList/GetScheduleListQuery.cs
diff --git a/Application/Cqrs/Schedule/Queries/GetScheduleList/GetScheduleListQueryHandler.cs b/SqlData/Application/Cqrs/Schedule/Queries/GetScheduleList/GetScheduleListQueryHandler.cs
similarity index 100%
rename from Application/Cqrs/Schedule/Queries/GetScheduleList/GetScheduleListQueryHandler.cs
rename to SqlData/Application/Cqrs/Schedule/Queries/GetScheduleList/GetScheduleListQueryHandler.cs
diff --git a/Application/Cqrs/Schedule/Queries/GetScheduleList/ScheduleListVm.cs b/SqlData/Application/Cqrs/Schedule/Queries/GetScheduleList/ScheduleListVm.cs
similarity index 100%
rename from Application/Cqrs/Schedule/Queries/GetScheduleList/ScheduleListVm.cs
rename to SqlData/Application/Cqrs/Schedule/Queries/GetScheduleList/ScheduleListVm.cs
diff --git a/Application/Cqrs/Schedule/Queries/GetScheduleList/ScheduleLookupDto.cs b/SqlData/Application/Cqrs/Schedule/Queries/GetScheduleList/ScheduleLookupDto.cs
similarity index 100%
rename from Application/Cqrs/Schedule/Queries/GetScheduleList/ScheduleLookupDto.cs
rename to SqlData/Application/Cqrs/Schedule/Queries/GetScheduleList/ScheduleLookupDto.cs
diff --git a/Application/DependencyInjection.cs b/SqlData/Application/DependencyInjection.cs
similarity index 100%
rename from Application/DependencyInjection.cs
rename to SqlData/Application/DependencyInjection.cs
diff --git a/Application/Interfaces/DbContexts/IDbContextBase.cs b/SqlData/Application/Interfaces/DbContexts/IDbContextBase.cs
similarity index 100%
rename from Application/Interfaces/DbContexts/IDbContextBase.cs
rename to SqlData/Application/Interfaces/DbContexts/IDbContextBase.cs
diff --git a/Application/Interfaces/DbContexts/Schedule/ICampusDbContext.cs b/SqlData/Application/Interfaces/DbContexts/Schedule/ICampusDbContext.cs
similarity index 100%
rename from Application/Interfaces/DbContexts/Schedule/ICampusDbContext.cs
rename to SqlData/Application/Interfaces/DbContexts/Schedule/ICampusDbContext.cs
diff --git a/Application/Interfaces/DbContexts/Schedule/IDisciplineDbContext.cs b/SqlData/Application/Interfaces/DbContexts/Schedule/IDisciplineDbContext.cs
similarity index 100%
rename from Application/Interfaces/DbContexts/Schedule/IDisciplineDbContext.cs
rename to SqlData/Application/Interfaces/DbContexts/Schedule/IDisciplineDbContext.cs
diff --git a/Application/Interfaces/DbContexts/Schedule/IFacultyDbContext.cs b/SqlData/Application/Interfaces/DbContexts/Schedule/IFacultyDbContext.cs
similarity index 100%
rename from Application/Interfaces/DbContexts/Schedule/IFacultyDbContext.cs
rename to SqlData/Application/Interfaces/DbContexts/Schedule/IFacultyDbContext.cs
diff --git a/Application/Interfaces/DbContexts/Schedule/IGroupDbContext.cs b/SqlData/Application/Interfaces/DbContexts/Schedule/IGroupDbContext.cs
similarity index 100%
rename from Application/Interfaces/DbContexts/Schedule/IGroupDbContext.cs
rename to SqlData/Application/Interfaces/DbContexts/Schedule/IGroupDbContext.cs
diff --git a/Application/Interfaces/DbContexts/Schedule/ILectureHallDbContext.cs b/SqlData/Application/Interfaces/DbContexts/Schedule/ILectureHallDbContext.cs
similarity index 100%
rename from Application/Interfaces/DbContexts/Schedule/ILectureHallDbContext.cs
rename to SqlData/Application/Interfaces/DbContexts/Schedule/ILectureHallDbContext.cs
diff --git a/Application/Interfaces/DbContexts/Schedule/ILessonAssociationDbContext.cs b/SqlData/Application/Interfaces/DbContexts/Schedule/ILessonAssociationDbContext.cs
similarity index 100%
rename from Application/Interfaces/DbContexts/Schedule/ILessonAssociationDbContext.cs
rename to SqlData/Application/Interfaces/DbContexts/Schedule/ILessonAssociationDbContext.cs
diff --git a/Application/Interfaces/DbContexts/Schedule/ILessonDbContext.cs b/SqlData/Application/Interfaces/DbContexts/Schedule/ILessonDbContext.cs
similarity index 100%
rename from Application/Interfaces/DbContexts/Schedule/ILessonDbContext.cs
rename to SqlData/Application/Interfaces/DbContexts/Schedule/ILessonDbContext.cs
diff --git a/Application/Interfaces/DbContexts/Schedule/IProfessorDbContext.cs b/SqlData/Application/Interfaces/DbContexts/Schedule/IProfessorDbContext.cs
similarity index 100%
rename from Application/Interfaces/DbContexts/Schedule/IProfessorDbContext.cs
rename to SqlData/Application/Interfaces/DbContexts/Schedule/IProfessorDbContext.cs
diff --git a/Application/Interfaces/DbContexts/Schedule/ISpecificWeekDbContext.cs b/SqlData/Application/Interfaces/DbContexts/Schedule/ISpecificWeekDbContext.cs
similarity index 100%
rename from Application/Interfaces/DbContexts/Schedule/ISpecificWeekDbContext.cs
rename to SqlData/Application/Interfaces/DbContexts/Schedule/ISpecificWeekDbContext.cs
diff --git a/Application/Interfaces/DbContexts/Schedule/ITypeOfOccupationDbContext.cs b/SqlData/Application/Interfaces/DbContexts/Schedule/ITypeOfOccupationDbContext.cs
similarity index 100%
rename from Application/Interfaces/DbContexts/Schedule/ITypeOfOccupationDbContext.cs
rename to SqlData/Application/Interfaces/DbContexts/Schedule/ITypeOfOccupationDbContext.cs
diff --git a/Domain/Domain.csproj b/SqlData/Domain/Domain.csproj
similarity index 100%
rename from Domain/Domain.csproj
rename to SqlData/Domain/Domain.csproj
diff --git a/Domain/Schedule/Campus.cs b/SqlData/Domain/Schedule/Campus.cs
similarity index 100%
rename from Domain/Schedule/Campus.cs
rename to SqlData/Domain/Schedule/Campus.cs
diff --git a/Domain/Schedule/Discipline.cs b/SqlData/Domain/Schedule/Discipline.cs
similarity index 100%
rename from Domain/Schedule/Discipline.cs
rename to SqlData/Domain/Schedule/Discipline.cs
diff --git a/Domain/Schedule/Faculty.cs b/SqlData/Domain/Schedule/Faculty.cs
similarity index 100%
rename from Domain/Schedule/Faculty.cs
rename to SqlData/Domain/Schedule/Faculty.cs
diff --git a/Domain/Schedule/Group.cs b/SqlData/Domain/Schedule/Group.cs
similarity index 100%
rename from Domain/Schedule/Group.cs
rename to SqlData/Domain/Schedule/Group.cs
diff --git a/Domain/Schedule/LectureHall.cs b/SqlData/Domain/Schedule/LectureHall.cs
similarity index 100%
rename from Domain/Schedule/LectureHall.cs
rename to SqlData/Domain/Schedule/LectureHall.cs
diff --git a/Domain/Schedule/Lesson.cs b/SqlData/Domain/Schedule/Lesson.cs
similarity index 100%
rename from Domain/Schedule/Lesson.cs
rename to SqlData/Domain/Schedule/Lesson.cs
diff --git a/Domain/Schedule/LessonAssociation.cs b/SqlData/Domain/Schedule/LessonAssociation.cs
similarity index 100%
rename from Domain/Schedule/LessonAssociation.cs
rename to SqlData/Domain/Schedule/LessonAssociation.cs
diff --git a/Domain/Schedule/Professor.cs b/SqlData/Domain/Schedule/Professor.cs
similarity index 100%
rename from Domain/Schedule/Professor.cs
rename to SqlData/Domain/Schedule/Professor.cs
diff --git a/Domain/Schedule/SpecificWeek.cs b/SqlData/Domain/Schedule/SpecificWeek.cs
similarity index 100%
rename from Domain/Schedule/SpecificWeek.cs
rename to SqlData/Domain/Schedule/SpecificWeek.cs
diff --git a/Domain/Schedule/TypeOfOccupation.cs b/SqlData/Domain/Schedule/TypeOfOccupation.cs
similarity index 100%
rename from Domain/Schedule/TypeOfOccupation.cs
rename to SqlData/Domain/Schedule/TypeOfOccupation.cs
diff --git a/SqlData/Migrations/MysqlMigrations/Migrations/20240601023106_InitialMigration.Designer.cs b/SqlData/Migrations/MysqlMigrations/Migrations/20240601023106_InitialMigration.Designer.cs
new file mode 100644
index 0000000..d4c0d32
--- /dev/null
+++ b/SqlData/Migrations/MysqlMigrations/Migrations/20240601023106_InitialMigration.Designer.cs
@@ -0,0 +1,442 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Mirea.Api.DataAccess.Persistence;
+
+#nullable disable
+
+namespace MysqlMigrations.Migrations
+{
+ [DbContext(typeof(UberDbContext))]
+ [Migration("20240601023106_InitialMigration")]
+ partial class InitialMigration
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.6")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Campus", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("CodeName")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("TEXT");
+
+ b.Property("FullName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("Campus", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Discipline", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("Discipline", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Faculty", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("CampusId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CampusId");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("Faculty", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Group", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("FacultyId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FacultyId");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("Group", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.LectureHall", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("CampusId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CampusId");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("LectureHall", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Lesson", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("DisciplineId")
+ .HasColumnType("INTEGER");
+
+ b.Property("GroupId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsEven")
+ .HasColumnType("BIT(1)");
+
+ b.Property("IsExcludedWeeks")
+ .HasColumnType("BIT(1)");
+
+ b.Property("PairNumber")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisciplineId");
+
+ b.HasIndex("GroupId");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("Lesson", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.LessonAssociation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("LectureHallId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LessonId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LinkToMeet")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("ProfessorId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TypeOfOccupationId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.HasIndex("LectureHallId");
+
+ b.HasIndex("LessonId");
+
+ b.HasIndex("ProfessorId");
+
+ b.HasIndex("TypeOfOccupationId");
+
+ b.ToTable("LessonAssociation", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Professor", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("AltName")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("Professor", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.SpecificWeek", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("LessonId")
+ .HasColumnType("INTEGER");
+
+ b.Property("WeekNumber")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.HasIndex("LessonId");
+
+ b.ToTable("SpecificWeek", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.TypeOfOccupation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("ShortName")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("TypeOfOccupation", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Faculty", b =>
+ {
+ b.HasOne("Mirea.Api.DataAccess.Domain.Schedule.Campus", "Campus")
+ .WithMany("Faculties")
+ .HasForeignKey("CampusId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("Campus");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Group", b =>
+ {
+ b.HasOne("Mirea.Api.DataAccess.Domain.Schedule.Faculty", "Faculty")
+ .WithMany("Groups")
+ .HasForeignKey("FacultyId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("Faculty");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.LectureHall", b =>
+ {
+ b.HasOne("Mirea.Api.DataAccess.Domain.Schedule.Campus", "Campus")
+ .WithMany("LectureHalls")
+ .HasForeignKey("CampusId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Campus");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Lesson", b =>
+ {
+ b.HasOne("Mirea.Api.DataAccess.Domain.Schedule.Discipline", "Discipline")
+ .WithMany("Lessons")
+ .HasForeignKey("DisciplineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Mirea.Api.DataAccess.Domain.Schedule.Group", "Group")
+ .WithMany("Lessons")
+ .HasForeignKey("GroupId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Discipline");
+
+ b.Navigation("Group");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.LessonAssociation", b =>
+ {
+ b.HasOne("Mirea.Api.DataAccess.Domain.Schedule.LectureHall", "LectureHall")
+ .WithMany("LessonAssociations")
+ .HasForeignKey("LectureHallId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Mirea.Api.DataAccess.Domain.Schedule.Lesson", "Lesson")
+ .WithMany("LessonAssociations")
+ .HasForeignKey("LessonId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Mirea.Api.DataAccess.Domain.Schedule.Professor", "Professor")
+ .WithMany("LessonAssociations")
+ .HasForeignKey("ProfessorId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Mirea.Api.DataAccess.Domain.Schedule.TypeOfOccupation", "TypeOfOccupation")
+ .WithMany("Lessons")
+ .HasForeignKey("TypeOfOccupationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("LectureHall");
+
+ b.Navigation("Lesson");
+
+ b.Navigation("Professor");
+
+ b.Navigation("TypeOfOccupation");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.SpecificWeek", b =>
+ {
+ b.HasOne("Mirea.Api.DataAccess.Domain.Schedule.Lesson", "Lesson")
+ .WithMany("SpecificWeeks")
+ .HasForeignKey("LessonId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Lesson");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Campus", b =>
+ {
+ b.Navigation("Faculties");
+
+ b.Navigation("LectureHalls");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Discipline", b =>
+ {
+ b.Navigation("Lessons");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Faculty", b =>
+ {
+ b.Navigation("Groups");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Group", b =>
+ {
+ b.Navigation("Lessons");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.LectureHall", b =>
+ {
+ b.Navigation("LessonAssociations");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Lesson", b =>
+ {
+ b.Navigation("LessonAssociations");
+
+ b.Navigation("SpecificWeeks");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Professor", b =>
+ {
+ b.Navigation("LessonAssociations");
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.TypeOfOccupation", b =>
+ {
+ b.Navigation("Lessons");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/SqlData/Migrations/MysqlMigrations/Migrations/20240601023106_InitialMigration.cs b/SqlData/Migrations/MysqlMigrations/Migrations/20240601023106_InitialMigration.cs
new file mode 100644
index 0000000..573b78f
--- /dev/null
+++ b/SqlData/Migrations/MysqlMigrations/Migrations/20240601023106_InitialMigration.cs
@@ -0,0 +1,389 @@
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace MysqlMigrations.Migrations
+{
+ ///
+ public partial class InitialMigration : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterDatabase()
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "Campus",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ CodeName = table.Column(type: "TEXT", maxLength: 16, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ FullName = table.Column(type: "TEXT", maxLength: 256, nullable: true)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ Address = table.Column(type: "TEXT", maxLength: 512, nullable: true)
+ .Annotation("MySql:CharSet", "utf8mb4")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Campus", x => x.Id);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "Discipline",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ Name = table.Column(type: "TEXT", maxLength: 256, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Discipline", x => x.Id);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "Professor",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ Name = table.Column(type: "TEXT", nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ AltName = table.Column(type: "TEXT", nullable: true)
+ .Annotation("MySql:CharSet", "utf8mb4")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Professor", x => x.Id);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "TypeOfOccupation",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ ShortName = table.Column(type: "TEXT", maxLength: 16, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_TypeOfOccupation", x => x.Id);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "Faculty",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ Name = table.Column(type: "TEXT", maxLength: 256, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ CampusId = table.Column(type: "INTEGER", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Faculty", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Faculty_Campus_CampusId",
+ column: x => x.CampusId,
+ principalTable: "Campus",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "LectureHall",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ Name = table.Column(type: "TEXT", maxLength: 64, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ CampusId = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_LectureHall", x => x.Id);
+ table.ForeignKey(
+ name: "FK_LectureHall_Campus_CampusId",
+ column: x => x.CampusId,
+ principalTable: "Campus",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "Group",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ Name = table.Column(type: "TEXT", maxLength: 64, nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ FacultyId = table.Column(type: "INTEGER", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Group", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Group_Faculty_FacultyId",
+ column: x => x.FacultyId,
+ principalTable: "Faculty",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "Lesson",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ IsEven = table.Column(type: "BIT(1)", nullable: false),
+ DayOfWeek = table.Column(type: "INTEGER", nullable: false),
+ PairNumber = table.Column(type: "INTEGER", nullable: false),
+ IsExcludedWeeks = table.Column(type: "BIT(1)", nullable: true),
+ GroupId = table.Column(type: "INTEGER", nullable: false),
+ DisciplineId = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Lesson", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Lesson_Discipline_DisciplineId",
+ column: x => x.DisciplineId,
+ principalTable: "Discipline",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_Lesson_Group_GroupId",
+ column: x => x.GroupId,
+ principalTable: "Group",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "LessonAssociation",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ LinkToMeet = table.Column(type: "TEXT", maxLength: 512, nullable: true)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ TypeOfOccupationId = table.Column(type: "INTEGER", nullable: false),
+ LessonId = table.Column(type: "INTEGER", nullable: false),
+ ProfessorId = table.Column(type: "INTEGER", nullable: true),
+ LectureHallId = table.Column(type: "INTEGER", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_LessonAssociation", x => x.Id);
+ table.ForeignKey(
+ name: "FK_LessonAssociation_LectureHall_LectureHallId",
+ column: x => x.LectureHallId,
+ principalTable: "LectureHall",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ table.ForeignKey(
+ name: "FK_LessonAssociation_Lesson_LessonId",
+ column: x => x.LessonId,
+ principalTable: "Lesson",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_LessonAssociation_Professor_ProfessorId",
+ column: x => x.ProfessorId,
+ principalTable: "Professor",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ table.ForeignKey(
+ name: "FK_LessonAssociation_TypeOfOccupation_TypeOfOccupationId",
+ column: x => x.TypeOfOccupationId,
+ principalTable: "TypeOfOccupation",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "SpecificWeek",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ WeekNumber = table.Column(type: "INTEGER", nullable: false),
+ LessonId = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_SpecificWeek", x => x.Id);
+ table.ForeignKey(
+ name: "FK_SpecificWeek_Lesson_LessonId",
+ column: x => x.LessonId,
+ principalTable: "Lesson",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Campus_Id",
+ table: "Campus",
+ column: "Id",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Discipline_Id",
+ table: "Discipline",
+ column: "Id",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Faculty_CampusId",
+ table: "Faculty",
+ column: "CampusId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Faculty_Id",
+ table: "Faculty",
+ column: "Id",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Group_FacultyId",
+ table: "Group",
+ column: "FacultyId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Group_Id",
+ table: "Group",
+ column: "Id",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_LectureHall_CampusId",
+ table: "LectureHall",
+ column: "CampusId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_LectureHall_Id",
+ table: "LectureHall",
+ column: "Id",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Lesson_DisciplineId",
+ table: "Lesson",
+ column: "DisciplineId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Lesson_GroupId",
+ table: "Lesson",
+ column: "GroupId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Lesson_Id",
+ table: "Lesson",
+ column: "Id",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_LessonAssociation_Id",
+ table: "LessonAssociation",
+ column: "Id",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_LessonAssociation_LectureHallId",
+ table: "LessonAssociation",
+ column: "LectureHallId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_LessonAssociation_LessonId",
+ table: "LessonAssociation",
+ column: "LessonId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_LessonAssociation_ProfessorId",
+ table: "LessonAssociation",
+ column: "ProfessorId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_LessonAssociation_TypeOfOccupationId",
+ table: "LessonAssociation",
+ column: "TypeOfOccupationId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Professor_Id",
+ table: "Professor",
+ column: "Id",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_SpecificWeek_Id",
+ table: "SpecificWeek",
+ column: "Id",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_SpecificWeek_LessonId",
+ table: "SpecificWeek",
+ column: "LessonId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_TypeOfOccupation_Id",
+ table: "TypeOfOccupation",
+ column: "Id",
+ unique: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "LessonAssociation");
+
+ migrationBuilder.DropTable(
+ name: "SpecificWeek");
+
+ migrationBuilder.DropTable(
+ name: "LectureHall");
+
+ migrationBuilder.DropTable(
+ name: "Professor");
+
+ migrationBuilder.DropTable(
+ name: "TypeOfOccupation");
+
+ migrationBuilder.DropTable(
+ name: "Lesson");
+
+ migrationBuilder.DropTable(
+ name: "Discipline");
+
+ migrationBuilder.DropTable(
+ name: "Group");
+
+ migrationBuilder.DropTable(
+ name: "Faculty");
+
+ migrationBuilder.DropTable(
+ name: "Campus");
+ }
+ }
+}
diff --git a/SqlData/Migrations/MysqlMigrations/Migrations/UberDbContextModelSnapshot.cs b/SqlData/Migrations/MysqlMigrations/Migrations/UberDbContextModelSnapshot.cs
new file mode 100644
index 0000000..5e0b48f
--- /dev/null
+++ b/SqlData/Migrations/MysqlMigrations/Migrations/UberDbContextModelSnapshot.cs
@@ -0,0 +1,439 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Mirea.Api.DataAccess.Persistence;
+
+#nullable disable
+
+namespace MysqlMigrations.Migrations
+{
+ [DbContext(typeof(UberDbContext))]
+ partial class UberDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.6")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Campus", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("CodeName")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("TEXT");
+
+ b.Property("FullName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("Campus", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Discipline", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("Discipline", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Faculty", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("CampusId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CampusId");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("Faculty", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Group", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("FacultyId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FacultyId");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("Group", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.LectureHall", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("CampusId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CampusId");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("LectureHall", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.Lesson", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("DisciplineId")
+ .HasColumnType("INTEGER");
+
+ b.Property("GroupId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsEven")
+ .HasColumnType("BIT(1)");
+
+ b.Property("IsExcludedWeeks")
+ .HasColumnType("BIT(1)");
+
+ b.Property("PairNumber")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisciplineId");
+
+ b.HasIndex("GroupId");
+
+ b.HasIndex("Id")
+ .IsUnique();
+
+ b.ToTable("Lesson", (string)null);
+ });
+
+ modelBuilder.Entity("Mirea.Api.DataAccess.Domain.Schedule.LessonAssociation", b =>
+ {
+ b.Property