Add Application configuration #11
.envDbInitializer.csDependencyInjection.cs
.gitea/workflows
ApiDto
Common
Requests
Application/Common/Mappings
Backend.slnEndpoint
Common
Attributes
Exceptions
Interfaces
Services
Configuration
EnvironmentManager.cs
General
Swagger
Controllers
Endpoint.csprojMiddleware
Program.csProperties
Persistence
Security
SqlData
Application
Application.csprojDependencyInjection.cs
Common
Cqrs
Campus
Queries
Discipline
Queries
Faculty
Queries
Group
Queries
LectureHall
Queries
Professor
Queries
Schedule
Interfaces
Domain
Domain.csproj
Schedule
Migrations
MysqlMigrations
Migrations
20240601023106_InitialMigration.Designer.cs20240601023106_InitialMigration.csUberDbContextModelSnapshot.cs
MysqlMigrations.csprojPsqlMigrations
Migrations
20240601021702_InitialMigration.Designer.cs20240601021702_InitialMigration.csUberDbContextModelSnapshot.cs
PsqlMigrations.csprojSqliteMigrations
Persistence
Common
BaseDbContext.csConfigurationResolver.csDatabaseProvider.csDbContextFactory.csModelBuilderExtensions.cs
Contexts
Schedule
EntityTypeConfigurations
Persistence.csprojUberDbContext.cs
101
.env
Normal file
101
.env
Normal file
@ -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
|
@ -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
|
||||
|
||||
|
22
ApiDto/Common/PairPeriodTime.cs
Normal file
22
ApiDto/Common/PairPeriodTime.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a pair of time periods.
|
||||
/// </summary>
|
||||
public class PairPeriodTime
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the start time of the period.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public TimeOnly Start { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the end time of the period.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public TimeOnly End { get; set; }
|
||||
}
|
26
ApiDto/Requests/Configuration/CacheRequest.cs
Normal file
26
ApiDto/Requests/Configuration/CacheRequest.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Requests.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to configure cache settings.
|
||||
/// </summary>
|
||||
public class CacheRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the server address.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the port number.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
}
|
44
ApiDto/Requests/Configuration/DatabaseRequest.cs
Normal file
44
ApiDto/Requests/Configuration/DatabaseRequest.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Requests.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to configure the database connection settings.
|
||||
/// </summary>
|
||||
public class DatabaseRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the server address.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the port number.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the database name.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Database { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether SSL is enabled.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool Ssl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
}
|
45
ApiDto/Requests/Configuration/EmailRequest.cs
Normal file
45
ApiDto/Requests/Configuration/EmailRequest.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Requests.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to configure email settings.
|
||||
/// </summary>
|
||||
public class EmailRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the server address.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email address from which emails will be sent.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string From { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password for the email account.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the port number.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether SSL is enabled.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool Ssl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string User { get; set; }
|
||||
}
|
25
ApiDto/Requests/Configuration/LoggingRequest.cs
Normal file
25
ApiDto/Requests/Configuration/LoggingRequest.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Requests.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to configure logging settings.
|
||||
/// </summary>
|
||||
public class LoggingRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether logging to file is enabled.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool EnableLogToFile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the log file name.
|
||||
/// </summary>
|
||||
public string? LogFileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the log file path.
|
||||
/// </summary>
|
||||
public string? LogFilePath { get; set; }
|
||||
}
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to configure the schedule settings.
|
||||
/// </summary>
|
||||
public class ScheduleConfigurationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the cron expression for updating the schedule.
|
||||
/// </summary>
|
||||
public string? CronUpdateSchedule { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start date of the term.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateOnly StartTerm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pair period times, keyed by pair number.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required IDictionary<int, PairPeriodTime> PairPeriod { get; set; }
|
||||
}
|
36
ApiDto/Requests/CreateUserRequest.cs
Normal file
36
ApiDto/Requests/CreateUserRequest.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request model for creating a user.
|
||||
/// </summary>
|
||||
public class CreateUserRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the email address of the user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The email address is a required field.
|
||||
/// </remarks>
|
||||
[Required]
|
||||
public required string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username of the user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The username is a required field.
|
||||
/// </remarks>
|
||||
[Required]
|
||||
public required string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password of the user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The password is a required field.
|
||||
/// </remarks>
|
||||
[Required]
|
||||
public required string Password { get; set; }
|
||||
}
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
using AutoMapper;
|
||||
|
||||
namespace Mirea.Api.DataAccess.Application.Common.Mappings;
|
||||
|
||||
public interface IMapWith<T>
|
||||
{
|
||||
void Mapping(Profile profile) =>
|
||||
profile.CreateMap(typeof(T), GetType());
|
||||
}
|
84
Backend.sln
84
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
|
||||
|
19
Endpoint/Common/Attributes/LocalhostAttribute.cs
Normal file
19
Endpoint/Common/Attributes/LocalhostAttribute.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class MaintenanceModeIgnoreAttribute : Attribute;
|
27
Endpoint/Common/Attributes/TokenAuthenticationAttribute.cs
Normal file
27
Endpoint/Common/Attributes/TokenAuthenticationAttribute.cs
Normal file
@ -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<ISetupToken>();
|
||||
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) { }
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Exceptions;
|
||||
|
||||
public class ControllerArgumentException(string message) : Exception(message);
|
@ -0,0 +1,8 @@
|
||||
namespace Mirea.Api.Endpoint.Common.Interfaces;
|
||||
|
||||
public interface IMaintenanceModeNotConfigureService
|
||||
{
|
||||
bool IsMaintenanceMode { get; }
|
||||
|
||||
void DisableMaintenanceMode();
|
||||
}
|
10
Endpoint/Common/Interfaces/IMaintenanceModeService.cs
Normal file
10
Endpoint/Common/Interfaces/IMaintenanceModeService.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Mirea.Api.Endpoint.Common.Interfaces;
|
||||
|
||||
public interface IMaintenanceModeService
|
||||
{
|
||||
bool IsMaintenanceMode { get; }
|
||||
|
||||
void EnableMaintenanceMode();
|
||||
|
||||
void DisableMaintenanceMode();
|
||||
}
|
9
Endpoint/Common/Interfaces/ISetupToken.cs
Normal file
9
Endpoint/Common/Interfaces/ISetupToken.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Interfaces;
|
||||
|
||||
public interface ISetupToken
|
||||
{
|
||||
bool MatchToken(ReadOnlySpan<byte> token);
|
||||
void SetToken(ReadOnlySpan<byte> token);
|
||||
}
|
@ -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;
|
||||
}
|
14
Endpoint/Common/Services/MaintenanceModeService.cs
Normal file
14
Endpoint/Common/Services/MaintenanceModeService.cs
Normal file
@ -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;
|
||||
}
|
13
Endpoint/Common/Services/PairPeriodTimeConverter.cs
Normal file
13
Endpoint/Common/Services/PairPeriodTimeConverter.cs
Normal file
@ -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<int, Dto.Common.PairPeriodTime> ConvertToDto(this IDictionary<int, ScheduleSettings.PairPeriodTime> pairPeriod) =>
|
||||
pairPeriod.ToDictionary(kvp => kvp.Key, kvp => new Dto.Common.PairPeriodTime { Start = kvp.Value.Start, End = kvp.Value.End });
|
||||
|
||||
public static Dictionary<int, ScheduleSettings.PairPeriodTime> ConvertFromDto(this IDictionary<int, Dto.Common.PairPeriodTime> pairPeriod) => pairPeriod.ToDictionary(kvp => kvp.Key, kvp => new ScheduleSettings.PairPeriodTime(kvp.Value.Start, kvp.Value.End));
|
||||
}
|
12
Endpoint/Common/Services/PathBuilder.cs
Normal file
12
Endpoint/Common/Services/PathBuilder.cs
Normal file
@ -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)]);
|
||||
}
|
32
Endpoint/Common/Services/Security/DistributedCacheService.cs
Normal file
32
Endpoint/Common/Services/Security/DistributedCacheService.cs
Normal file
@ -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<T>(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<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var cachedValue = await cache.GetAsync(key, cancellationToken);
|
||||
return cachedValue == null ? default : JsonSerializer.Deserialize<T>(cachedValue);
|
||||
}
|
||||
|
||||
public Task RemoveAsync(string key, CancellationToken cancellationToken = default) =>
|
||||
cache.RemoveAsync(key, cancellationToken);
|
||||
}
|
82
Endpoint/Common/Services/Security/JwtTokenService.cs
Normal file
82
Endpoint/Common/Services/Security/JwtTokenService.cs
Normal file
@ -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<byte> EncryptionKey { get; init; }
|
||||
public ReadOnlyMemory<byte> 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;
|
||||
}
|
||||
}
|
34
Endpoint/Common/Services/Security/MemoryCacheService.cs
Normal file
34
Endpoint/Common/Services/Security/MemoryCacheService.cs
Normal file
@ -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<T>(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<T?> GetAsync<T>(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;
|
||||
}
|
||||
}
|
@ -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<bool> IsTokenRevokedAsync(string token) => Task.FromResult(cache.TryGetValue(token, out _));
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
14
Endpoint/Configuration/General/GeneralConfig.cs
Normal file
14
Endpoint/Configuration/General/GeneralConfig.cs
Normal file
@ -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; }
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Mirea.Api.Endpoint.Configuration.General.Interfaces;
|
||||
|
||||
public interface IIsConfigured
|
||||
{
|
||||
bool IsConfigured();
|
||||
}
|
23
Endpoint/Configuration/General/Settings/CacheSettings.cs
Normal file
23
Endpoint/Configuration/General/Settings/CacheSettings.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
31
Endpoint/Configuration/General/Settings/DbSettings.cs
Normal file
31
Endpoint/Configuration/General/Settings/DbSettings.cs
Normal file
@ -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);
|
||||
}
|
23
Endpoint/Configuration/General/Settings/EmailSettings.cs
Normal file
23
Endpoint/Configuration/General/Settings/EmailSettings.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
19
Endpoint/Configuration/General/Settings/LogSettings.cs
Normal file
19
Endpoint/Configuration/General/Settings/LogSettings.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
45
Endpoint/Configuration/General/Settings/ScheduleSettings.cs
Normal file
45
Endpoint/Configuration/General/Settings/ScheduleSettings.cs
Normal file
@ -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<int, PairPeriodTime> PairPeriod { get; set; }
|
||||
|
||||
public bool IsConfigured()
|
||||
{
|
||||
return !string.IsNullOrEmpty(CronUpdateSchedule) &&
|
||||
StartTerm != default &&
|
||||
PairPeriod.Count != 0 &&
|
||||
PairPeriod.Any();
|
||||
}
|
||||
}
|
28
Endpoint/Configuration/General/SetupTokenService.cs
Normal file
28
Endpoint/Configuration/General/SetupTokenService.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.General;
|
||||
|
||||
public class SetupTokenService : ISetupToken
|
||||
{
|
||||
public ReadOnlyMemory<byte>? Token { get; private set; }
|
||||
|
||||
public bool MatchToken(ReadOnlySpan<byte> 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<byte> token)
|
||||
{
|
||||
Token = token.ToArray();
|
||||
}
|
||||
}
|
@ -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<GeneralConfig> 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;
|
||||
}
|
||||
}
|
@ -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<SwaggerGenOptions>
|
||||
{
|
@ -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
|
||||
{
|
@ -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;
|
312
Endpoint/Controllers/Configuration/SetupController.cs
Normal file
312
Endpoint/Controllers/Configuration/SetupController.cs
Normal file
@ -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<GeneralConfig>(CacheGeneralKey) ?? new GeneralConfig();
|
||||
set => cache.Set(CacheGeneralKey, value);
|
||||
}
|
||||
|
||||
[HttpGet("GenerateToken")]
|
||||
[Localhost]
|
||||
public ActionResult<string> 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<bool> 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<bool> SetDatabase<TConnection, TException>(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<bool> 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<NpgsqlConnection, NpgsqlException>(connectionString, DbSettings.DatabaseEnum.PostgresSql);
|
||||
}
|
||||
|
||||
[HttpPost("SetMysql")]
|
||||
[TokenAuthentication]
|
||||
[BadRequestResponse]
|
||||
public ActionResult<bool> 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<MySqlConnection, MySqlException>(connectionString, DbSettings.DatabaseEnum.Mysql);
|
||||
}
|
||||
|
||||
[HttpPost("SetSqlite")]
|
||||
[TokenAuthentication]
|
||||
public ActionResult<bool> 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<SqliteConnection, SqliteException>(connectionString, DbSettings.DatabaseEnum.Sqlite);
|
||||
}
|
||||
|
||||
[HttpPost("SetRedis")]
|
||||
[TokenAuthentication]
|
||||
[BadRequestResponse]
|
||||
public ActionResult<bool> 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<bool> 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<string> CreateAdmin([FromBody] CreateUserRequest user)
|
||||
{
|
||||
// todo: change CreateUserRequest to Domain entity
|
||||
cache.Set(CacheAdminKey, user);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
[HttpPost("SetLogging")]
|
||||
[TokenAuthentication]
|
||||
[BadRequestResponse]
|
||||
public ActionResult<bool> 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<bool> 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<bool> 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<bool> 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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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
|
||||
/// <summary>
|
||||
/// Gets basic information about campuses.
|
||||
/// </summary>
|
||||
/// <returns>Basic information about campuses.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<CampusBasicInfoResponse>>> Get()
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets basic information about campuses.
|
||||
/// </summary>
|
||||
/// <returns>Basic information about campuses.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<CampusBasicInfoResponse>>> 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
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets details of a specific campus by ID.
|
||||
/// </summary>
|
||||
/// <param name="id">Campus ID.</param>
|
||||
/// <returns>Details of the specified campus.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<CampusDetailsResponse>> 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
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets details of a specific campus by ID.
|
||||
/// </summary>
|
||||
/// <param name="id">Campus ID.</param>
|
||||
/// <returns>Details of the specified campus.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<CampusDetailsResponse>> 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
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
/// <summary>
|
||||
/// Gets a paginated list of disciplines.
|
||||
/// </summary>
|
||||
/// <param name="page">Page number. Start from 0.</param>
|
||||
/// <param name="pageSize">Number of items per page.</param>
|
||||
/// <returns>Paginated list of disciplines.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
public async Task<ActionResult<List<DisciplineResponse>>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a paginated list of disciplines.
|
||||
/// </summary>
|
||||
/// <param name="page">Page number. Start from 0.</param>
|
||||
/// <param name="pageSize">Number of items per page.</param>
|
||||
/// <returns>Paginated list of disciplines.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
public async Task<ActionResult<List<DisciplineResponse>>> 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
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets details of a specific discipline by ID.
|
||||
/// </summary>
|
||||
/// <param name="id">Discipline ID.</param>
|
||||
/// <returns>Details of the specified discipline.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<DisciplineResponse>> 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
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets details of a specific discipline by ID.
|
||||
/// </summary>
|
||||
/// <param name="id">Discipline ID.</param>
|
||||
/// <returns>Details of the specified discipline.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<DisciplineResponse>> GetDetails(int id)
|
||||
{
|
||||
var result = await mediator.Send(new GetDisciplineInfoQuery()
|
||||
{
|
||||
Id = id
|
||||
});
|
||||
|
||||
return Ok(new DisciplineResponse()
|
||||
{
|
||||
Id = result.Id,
|
||||
Name = result.Name
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
/// <summary>
|
||||
/// Gets a paginated list of faculties.
|
||||
/// </summary>
|
||||
/// <param name="page">Page number. Start from 0.</param>
|
||||
/// <param name="pageSize">Number of items per page.</param>
|
||||
/// <returns>Paginated list of faculties.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
public async Task<ActionResult<List<FacultyResponse>>> Get([FromQuery] int? page, [FromQuery] int? pageSize)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a paginated list of faculties.
|
||||
/// </summary>
|
||||
/// <param name="page">Page number. Start from 0.</param>
|
||||
/// <param name="pageSize">Number of items per page.</param>
|
||||
/// <returns>Paginated list of faculties.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
public async Task<ActionResult<List<FacultyResponse>>> 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
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets details of a specific faculty by ID.
|
||||
/// </summary>
|
||||
/// <param name="id">Faculty ID.</param>
|
||||
/// <returns>Details of the specified faculty.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<FacultyDetailsResponse>> 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
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets details of a specific faculty by ID.
|
||||
/// </summary>
|
||||
/// <param name="id">Faculty ID.</param>
|
||||
/// <returns>Details of the specified faculty.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<FacultyDetailsResponse>> 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
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of groups.
|
||||
/// </summary>
|
||||
/// <param name="page">The page number for pagination (optional).</param>
|
||||
/// <param name="pageSize">The page size for pagination (optional).</param>
|
||||
/// <returns>A list of groups.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
public async Task<ActionResult<List<GroupResponse>>> 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)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves detailed information about a specific group.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the group to retrieve.</param>
|
||||
/// <returns>Detailed information about the group.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<GroupDetailsResponse>> 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)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of groups by faculty ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the faculty.</param>
|
||||
/// <returns>A list of groups belonging to the specified faculty.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<List<GroupResponse>>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of groups.
|
||||
/// </summary>
|
||||
/// <param name="page">The page number for pagination (optional).</param>
|
||||
/// <param name="pageSize">The page size for pagination (optional).</param>
|
||||
/// <returns>A list of groups.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
public async Task<ActionResult<List<GroupResponse>>> 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)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves detailed information about a specific group.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the group to retrieve.</param>
|
||||
/// <returns>Detailed information about the group.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<GroupDetailsResponse>> 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)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of groups by faculty ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the faculty.</param>
|
||||
/// <returns>A list of groups belonging to the specified faculty.</returns>
|
||||
[HttpGet("GetByFaculty/{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<List<GroupResponse>>> 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
|
||||
}));
|
||||
}
|
||||
}
|
@ -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
|
||||
/// <summary>
|
||||
/// Retrieves a list of all lecture halls.
|
||||
/// </summary>
|
||||
/// <returns>A list of lecture halls.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<LectureHallResponse>>> Get()
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a list of all lecture halls.
|
||||
/// </summary>
|
||||
/// <returns>A list of lecture halls.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<LectureHallResponse>>> 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
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves details of a specific lecture hall by its ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the lecture hall to retrieve.</param>
|
||||
/// <returns>The details of the specified lecture hall.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<LectureHallDetailsResponse>> 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
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of lecture halls by campus ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the campus.</param>
|
||||
/// <returns>A list of lecture halls in the specified campus.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<List<LectureHallResponse>>> 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
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves details of a specific lecture hall by its ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the lecture hall to retrieve.</param>
|
||||
/// <returns>The details of the specified lecture hall.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<LectureHallDetailsResponse>> 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
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of lecture halls by campus ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the campus.</param>
|
||||
/// <returns>A list of lecture halls in the specified campus.</returns>
|
||||
[HttpGet("GetByCampus/{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<List<LectureHallResponse>>> 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
|
||||
}));
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a list of professors.
|
||||
|
@ -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<GeneralConfig> config) : BaseController
|
||||
{
|
||||
[HttpGet("StartTerm")]
|
||||
public ActionResult<DateOnly> GetStartTerm() => config.Value.ScheduleSettings!.StartTerm;
|
||||
|
||||
[HttpGet("PairPeriod")]
|
||||
public ActionResult<Dictionary<int, PairPeriodTime>> GetPairPeriod() => config.Value.ScheduleSettings!.PairPeriod.ConvertToDto();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves schedules based on various filters.
|
||||
/// </summary>
|
||||
@ -25,7 +37,6 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
|
||||
[BadRequestResponse]
|
||||
[NotFoundResponse]
|
||||
public async Task<ActionResult<List<ScheduleResponse>>> 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
|
||||
/// <param name="professors">An array of professor IDs.</param>
|
||||
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
||||
/// <returns>A response containing schedules for the specified group.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[HttpGet("GetByGroup/{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[BadRequestResponse]
|
||||
@ -142,7 +153,7 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
|
||||
/// <param name="groups">An array of group IDs.</param>
|
||||
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
||||
/// <returns>A response containing schedules for the specified professor.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[HttpGet("GetByProfessor/{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[BadRequestResponse]
|
||||
@ -203,7 +214,7 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
|
||||
/// <param name="professors">An array of professor IDs.</param>
|
||||
/// <param name="groups">An array of group IDs.</param>
|
||||
/// <returns>A response containing schedules for the specified lecture hall.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[HttpGet("GetByLectureHall/{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[BadRequestResponse]
|
||||
@ -264,7 +275,7 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
|
||||
/// <param name="professors">An array of professor IDs.</param>
|
||||
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
||||
/// <returns>A response containing schedules for the specified discipline.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[HttpGet("GetByDiscipline/{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[BadRequestResponse]
|
||||
|
@ -22,15 +22,21 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Cronos" Version="0.8.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.6" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Versioning" Version="2.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApiDto\ApiDto.csproj" />
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
<ProjectReference Include="..\Persistence\Persistence.csproj" />
|
||||
<ProjectReference Include="..\SqlData\Domain\Domain.csproj" />
|
||||
<ProjectReference Include="..\SqlData\Persistence\Persistence.csproj" />
|
||||
<ProjectReference Include="..\Security\Security.csproj" />
|
||||
<ProjectReference Include="..\SqlData\Migrations\PsqlMigrations\PsqlMigrations.csproj" />
|
||||
<ProjectReference Include="..\SqlData\Migrations\SqliteMigrations\SqliteMigrations.csproj" />
|
||||
<ProjectReference Include="..\SqlData\Migrations\MysqlMigrations\MysqlMigrations.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
60
Endpoint/Middleware/CustomExceptionHandlerMiddleware.cs
Normal file
60
Endpoint/Middleware/CustomExceptionHandlerMiddleware.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
39
Endpoint/Middleware/MaintenanceModeMiddleware.cs
Normal file
39
Endpoint/Middleware/MaintenanceModeMiddleware.cs
Normal file
@ -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<MaintenanceModeIgnoreAttribute>() != 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<IAccessToken, JwtTokenService>(_ => 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<IAccessToken, JwtTokenService>();
|
||||
services.AddSingleton<IRevokedToken, MemoryRevokedTokenService>();
|
||||
|
||||
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<GeneralConfig>(builder.Configuration);
|
||||
|
||||
var generalConfig = builder.Configuration.Get<GeneralConfig>();
|
||||
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<IMaintenanceModeNotConfigureService, MaintenanceModeNotConfigureService>();
|
||||
builder.Services.AddSingleton<IMaintenanceModeService, MaintenanceModeService>();
|
||||
builder.Services.AddSingleton<ISetupToken, SetupTokenService>();
|
||||
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<UberDbContext>();
|
||||
DbInitializer.Initialize(uber!);
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var serviceProvider = scope.ServiceProvider;
|
||||
|
||||
var optionsSnapshot = serviceProvider.GetRequiredService<IOptionsSnapshot<GeneralConfig>>();
|
||||
var settingsValidator = new SettingsRequiredValidator(optionsSnapshot);
|
||||
var isDoneConfig = settingsValidator.AreSettingsValid();
|
||||
|
||||
if (isDoneConfig)
|
||||
{
|
||||
var uberDbContext = serviceProvider.GetRequiredService<UberDbContext>();
|
||||
var maintenanceModeService = serviceProvider.GetRequiredService<IMaintenanceModeNotConfigureService>();
|
||||
|
||||
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<MaintenanceModeMiddleware>();
|
||||
app.UseMiddleware<CustomExceptionHandlerMiddleware>();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
|
@ -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; }
|
||||
}
|
@ -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<DbSettings>();
|
||||
var connection = settings?.ConnectionStringSql;
|
||||
|
||||
Dictionary<DatabaseEnum, Action<DbContextOptionsBuilder>> 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<CampusDbContext>(dbConfig);
|
||||
services.AddDbContext<DisciplineDbContext>(dbConfig);
|
||||
services.AddDbContext<FacultyDbContext>(dbConfig);
|
||||
services.AddDbContext<GroupDbContext>(dbConfig);
|
||||
services.AddDbContext<LectureHallDbContext>(dbConfig);
|
||||
services.AddDbContext<LessonAssociationDbContext>(dbConfig);
|
||||
services.AddDbContext<ProfessorDbContext>(dbConfig);
|
||||
services.AddDbContext<LessonDbContext>(dbConfig);
|
||||
services.AddDbContext<TypeOfOccupationDbContext>(dbConfig);
|
||||
services.AddDbContext<SpecificWeekDbContext>(dbConfig);
|
||||
|
||||
services.AddDbContext<UberDbContext>(dbConfig);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException("Unsupported database type");
|
||||
|
||||
services.AddScoped<ICampusDbContext>(provider => provider.GetService<CampusDbContext>()!);
|
||||
services.AddScoped<IDisciplineDbContext>(provider => provider.GetService<DisciplineDbContext>()!);
|
||||
services.AddScoped<IFacultyDbContext>(provider => provider.GetService<FacultyDbContext>()!);
|
||||
services.AddScoped<IGroupDbContext>(provider => provider.GetService<GroupDbContext>()!);
|
||||
services.AddScoped<ILectureHallDbContext>(provider => provider.GetService<LectureHallDbContext>()!);
|
||||
services.AddScoped<ILessonAssociationDbContext>(provider => provider.GetService<LessonAssociationDbContext>()!);
|
||||
services.AddScoped<IProfessorDbContext>(provider => provider.GetService<ProfessorDbContext>()!);
|
||||
services.AddScoped<ILessonDbContext>(provider => provider.GetService<LessonDbContext>()!);
|
||||
services.AddScoped<ITypeOfOccupationDbContext>(provider => provider.GetService<TypeOfOccupationDbContext>()!);
|
||||
services.AddScoped<ISpecificWeekDbContext>(provider => provider.GetService<SpecificWeekDbContext>()!);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
13
Security/Common/Domain/AuthToken.cs
Normal file
13
Security/Common/Domain/AuthToken.cs
Normal file
@ -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; }
|
||||
}
|
10
Security/Common/Domain/PreAuthToken.cs
Normal file
10
Security/Common/Domain/PreAuthToken.cs
Normal file
@ -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; }
|
||||
}
|
8
Security/Common/Dto/Requests/TokenRequest.cs
Normal file
8
Security/Common/Dto/Requests/TokenRequest.cs
Normal file
@ -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; }
|
||||
}
|
10
Security/Common/Dto/Responses/AuthTokenResponse.cs
Normal file
10
Security/Common/Dto/Responses/AuthTokenResponse.cs
Normal file
@ -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; }
|
||||
}
|
9
Security/Common/Dto/Responses/PreAuthTokenResponse.cs
Normal file
9
Security/Common/Dto/Responses/PreAuthTokenResponse.cs
Normal file
@ -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; }
|
||||
}
|
9
Security/Common/Interfaces/IAccessToken.cs
Normal file
9
Security/Common/Interfaces/IAccessToken.cs
Normal file
@ -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);
|
||||
}
|
16
Security/Common/Interfaces/ICacheService.cs
Normal file
16
Security/Common/Interfaces/ICacheService.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mirea.Api.Security.Common.Interfaces;
|
||||
|
||||
public interface ICacheService
|
||||
{
|
||||
Task SetAsync<T>(string key, T value,
|
||||
TimeSpan? absoluteExpirationRelativeToNow = null,
|
||||
TimeSpan? slidingExpiration = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default);
|
||||
Task RemoveAsync(string key, CancellationToken cancellationToken = default);
|
||||
}
|
10
Security/Common/Interfaces/IRevokedToken.cs
Normal file
10
Security/Common/Interfaces/IRevokedToken.cs
Normal file
@ -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<bool> IsTokenRevokedAsync(string token);
|
||||
}
|
57
Security/DependencyInjection.cs
Normal file
57
Security/DependencyInjection.cs
Normal file
@ -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<ICacheService>();
|
||||
|
||||
return new PreAuthService(cache)
|
||||
{
|
||||
Lifetime = lifeTimePreAuthToken
|
||||
};
|
||||
});
|
||||
|
||||
var lifeTimeRefreshToken = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_RT"]!));
|
||||
|
||||
services.AddSingleton(provider =>
|
||||
{
|
||||
var cacheService = provider.GetRequiredService<ICacheService>();
|
||||
var accessTokenService = provider.GetRequiredService<IAccessToken>();
|
||||
var revokedTokenService = provider.GetRequiredService<IRevokedToken>();
|
||||
|
||||
return new AuthService(cacheService, accessTokenService, revokedTokenService)
|
||||
{
|
||||
Lifetime = lifeTimeRefreshToken
|
||||
};
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
21
Security/Security.csproj
Normal file
21
Security/Security.csproj
Normal file
@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Company>Winsomnia</Company>
|
||||
<Version>1.0.0-a0</Version>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<AssemblyName>Mirea.Api.Security</AssemblyName>
|
||||
<RootNamespace>$(AssemblyName)</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
103
Security/Services/AuthService.cs
Normal file
103
Security/Services/AuthService.cs
Normal file
@ -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<AuthTokenResponse> 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<AuthTokenResponse> GenerateAuthTokensWithPreAuthAsync(TokenRequest request, string preAuthToken,
|
||||
CancellationToken cancellation = default) =>
|
||||
await GenerateAuthTokensAsync(request,
|
||||
await new PreAuthService(cache).MatchToken(request, preAuthToken, cancellation),
|
||||
cancellation);
|
||||
|
||||
public async Task<AuthTokenResponse> RefreshTokenAsync(TokenRequest request, string refreshToken, CancellationToken cancellation = default)
|
||||
{
|
||||
var authToken = await cache.GetAsync<AuthToken>(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<AuthToken>(GetAuthCacheKey(fingerprint), cancellation);
|
||||
if (authTokenStruct == null) return;
|
||||
|
||||
await RevokeAccessToken(authTokenStruct.AccessToken);
|
||||
|
||||
await cache.RemoveAsync(fingerprint, cancellation);
|
||||
}
|
||||
}
|
28
Security/Services/GeneratorKey.cs
Normal file
28
Security/Services/GeneratorKey.cs
Normal file
@ -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<byte> 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<byte> utf8Bytes = new byte[Base64.GetMaxEncodedToUtf8Length(randomBytes.Length)];
|
||||
|
||||
Base64.EncodeToUtf8(randomBytes, utf8Bytes, out _, out _);
|
||||
return Encoding.UTF8.GetString(utf8Bytes);
|
||||
}
|
||||
}
|
56
Security/Services/PasswordHashService.cs
Normal file
56
Security/Services/PasswordHashService.cs
Normal file
@ -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<byte> HashPassword(string password, ReadOnlySpan<byte> 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<byte> a, ReadOnlySpan<byte> 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<byte> salt, ReadOnlySpan<byte> hash) =>
|
||||
ConstantTimeComparison(HashPassword(password, salt), hash);
|
||||
|
||||
public bool VerifyPassword(string password, string saltBase64, string hashBase64) =>
|
||||
VerifyPassword(password, Convert.FromBase64String(saltBase64), Convert.FromBase64String(hashBase64));
|
||||
}
|
64
Security/Services/PreAuthService.cs
Normal file
64
Security/Services/PreAuthService.cs
Normal file
@ -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<PreAuthTokenResponse> 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<string> MatchToken(TokenRequest request, string preAuthToken, CancellationToken cancellation = default)
|
||||
{
|
||||
var preAuthTokenStruct = await cache.GetAsync<PreAuthToken>(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;
|
||||
}
|
||||
}
|
@ -13,15 +13,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="FluentValidation" Version="11.9.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
|
||||
<PackageReference Include="MediatR" Version="12.2.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.9.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.1" />
|
||||
<PackageReference Include="MediatR" Version="12.2.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user