Add Application configuration (#11)
All checks were successful
.NET Test Pipeline / build-and-test (push) Successful in 1m43s
All checks were successful
.NET Test Pipeline / build-and-test (push) Successful in 1m43s
Reviewed-on: #11
This commit is contained in:
commit
ea538ac340
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:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, 'release/*']
|
push:
|
||||||
|
branches:
|
||||||
|
[master, 'release/*']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build-and-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up .NET Core
|
- name: Set up .NET Core
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
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
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.8.34330.188
|
VisualStudioVersion = 17.8.34330.188
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
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}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Endpoint", "Endpoint\Endpoint.csproj", "{F3A1D12E-F5B2-4339-9966-DBF869E78357}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Elements of the solution", "Elements of the solution", "{3E087889-A4A0-4A55-A07D-7D149A5BC928}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Elements of the solution", "Elements of the solution", "{3E087889-A4A0-4A55-A07D-7D149A5BC928}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
.dockerignore = .dockerignore
|
.dockerignore = .dockerignore
|
||||||
|
.env = .env
|
||||||
.gitattributes = .gitattributes
|
.gitattributes = .gitattributes
|
||||||
.gitignore = .gitignore
|
.gitignore = .gitignore
|
||||||
Dockerfile = Dockerfile
|
Dockerfile = Dockerfile
|
||||||
LICENSE.txt = LICENSE.txt
|
LICENSE.txt = LICENSE.txt
|
||||||
README.md = README.md
|
README.md = README.md
|
||||||
|
.gitea\workflows\test.yaml = .gitea\workflows\test.yaml
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
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
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
{C27FB5CD-6A70-4FB2-847A-847B34806902} = {C27FB5CD-6A70-4FB2-847A-847B34806902}
|
{48C9998C-ECE2-407F-835F-1A7255A5C99E} = {48C9998C-ECE2-407F-835F-1A7255A5C99E}
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
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
|
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
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
@ -33,26 +52,55 @@ Global
|
|||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
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.ActiveCfg = Debug|Any CPU
|
||||||
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Release|Any CPU.Build.0 = 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
|
{0335FA36-E137-453F-853B-916674C168FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{E7F0A4D4-B032-4BB9-9526-1AF688F341A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{0335FA36-E137-453F-853B-916674C168FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{E7F0A4D4-B032-4BB9-9526-1AF688F341A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{E7F0A4D4-B032-4BB9-9526-1AF688F341A4}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{4C1E558F-633F-438E-AC3A-61CDDED917C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{4C1E558F-633F-438E-AC3A-61CDDED917C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4C1E558F-633F-438E-AC3A-61CDDED917C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4C1E558F-633F-438E-AC3A-61CDDED917C5}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
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
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E80A1224-87F5-4FEB-82AE-89006BE98B12}
|
SolutionGuid = {E80A1224-87F5-4FEB-82AE-89006BE98B12}
|
||||||
EndGlobalSection
|
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
|
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);
|
StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
if (parts.Length > 2)
|
||||||
|
parts = [parts[0], string.Join("=", parts[1..])];
|
||||||
|
|
||||||
if (parts.Length != 2)
|
if (parts.Length != 2)
|
||||||
continue;
|
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 Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration;
|
namespace Mirea.Api.Endpoint.Configuration.Swagger;
|
||||||
|
|
||||||
public class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions<SwaggerGenOptions>
|
public class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions<SwaggerGenOptions>
|
||||||
{
|
{
|
@ -6,7 +6,7 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration;
|
namespace Mirea.Api.Endpoint.Configuration.Swagger;
|
||||||
|
|
||||||
public class SwaggerDefaultValues : IOperationFilter
|
public class SwaggerDefaultValues : IOperationFilter
|
||||||
{
|
{
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Controllers;
|
namespace Mirea.Api.Endpoint.Controllers;
|
||||||
|
|
||||||
|
[Produces("application/json")]
|
||||||
|
[Route("api/v{version:apiVersion}/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]/[action]")]
|
|
||||||
public class BaseController : ControllerBase;
|
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.Linq;
|
||||||
using System.Threading.Tasks;
|
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>
|
var result = await mediator.Send(new GetCampusBasicInfoListQuery());
|
||||||
/// 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());
|
|
||||||
|
|
||||||
return Ok(result.Campuses
|
return Ok(result.Campuses
|
||||||
.Select(c => new CampusBasicInfoResponse()
|
.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()
|
|
||||||
{
|
{
|
||||||
Id = id
|
Id = c.Id,
|
||||||
});
|
CodeName = c.CodeName,
|
||||||
|
FullName = c.FullName
|
||||||
return Ok(new CampusDetailsResponse()
|
})
|
||||||
{
|
);
|
||||||
Id = result.Id,
|
|
||||||
CodeName = result.CodeName,
|
|
||||||
FullName = result.FullName,
|
|
||||||
Address = result.Address
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <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.Linq;
|
||||||
using System.Threading.Tasks;
|
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>
|
var result = await mediator.Send(new GetDisciplineListQuery()
|
||||||
/// 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()
|
Page = page,
|
||||||
{
|
PageSize = pageSize
|
||||||
Page = page,
|
});
|
||||||
PageSize = pageSize
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(result.Disciplines
|
return Ok(result.Disciplines
|
||||||
.Select(d => new DisciplineResponse()
|
.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()
|
|
||||||
{
|
{
|
||||||
Id = id
|
Id = d.Id,
|
||||||
});
|
Name = d.Name
|
||||||
|
})
|
||||||
return Ok(new DisciplineResponse()
|
);
|
||||||
{
|
|
||||||
Id = result.Id,
|
|
||||||
Name = result.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.Linq;
|
||||||
using System.Threading.Tasks;
|
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>
|
var result = await mediator.Send(new GetFacultyListQuery()
|
||||||
/// 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()
|
Page = page,
|
||||||
{
|
PageSize = pageSize
|
||||||
Page = page,
|
});
|
||||||
PageSize = pageSize
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(result.Faculties
|
return Ok(result.Faculties
|
||||||
.Select(f => new FacultyResponse()
|
.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()
|
|
||||||
{
|
{
|
||||||
Id = id
|
Id = f.Id,
|
||||||
});
|
Name = f.Name,
|
||||||
|
CampusId = f.CampusId
|
||||||
return Ok(new FacultyDetailsResponse()
|
})
|
||||||
{
|
);
|
||||||
Id = result.Id,
|
|
||||||
Name = result.Name,
|
|
||||||
CampusId = result.CampusId,
|
|
||||||
CampusCode = result.CampusCode,
|
|
||||||
CampusName = result.CampusName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <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.Linq;
|
||||||
using System.Threading.Tasks;
|
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)
|
||||||
var current = DateTime.Now;
|
&& !int.TryParse(groupName.Split('-')[^1][..2], out yearOfGroup))
|
||||||
if (!int.TryParse(groupName[2..], out var yearOfGroup)
|
return -1;
|
||||||
&& !int.TryParse(groupName.Split('-')[^1][..2], out yearOfGroup))
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
// Convert a two-digit year to a four-digit one
|
// Convert a two-digit year to a four-digit one
|
||||||
yearOfGroup += current.Year / 100 * 100;
|
yearOfGroup += current.Year / 100 * 100;
|
||||||
|
|
||||||
return current.Year - yearOfGroup + (current.Month < 9 ? 0 : 1);
|
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
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <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.Linq;
|
||||||
using System.Threading.Tasks;
|
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>
|
var result = await mediator.Send(new GetLectureHallListQuery());
|
||||||
/// 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());
|
|
||||||
|
|
||||||
return Ok(result.LectureHalls
|
return Ok(result.LectureHalls
|
||||||
.Select(l => new LectureHallResponse()
|
.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()
|
|
||||||
{
|
{
|
||||||
Id = id
|
Id = l.Id,
|
||||||
});
|
Name = l.Name,
|
||||||
|
CampusId = l.CampusId
|
||||||
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
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <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;
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||||
|
|
||||||
public class ProfessorController(IMediator mediator) : BaseControllerV1
|
[ApiVersion("1.0")]
|
||||||
|
public class ProfessorController(IMediator mediator) : BaseController
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a list of professors.
|
/// Retrieves a list of professors.
|
||||||
|
@ -1,19 +1,31 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Mirea.Api.DataAccess.Application.Cqrs.Schedule.Queries.GetScheduleList;
|
using Mirea.Api.DataAccess.Application.Cqrs.Schedule.Queries.GetScheduleList;
|
||||||
|
using Mirea.Api.Dto.Common;
|
||||||
using Mirea.Api.Dto.Requests;
|
using Mirea.Api.Dto.Requests;
|
||||||
using Mirea.Api.Dto.Responses;
|
using Mirea.Api.Dto.Responses;
|
||||||
using Mirea.Api.Dto.Responses.Schedule;
|
using Mirea.Api.Dto.Responses.Schedule;
|
||||||
using Mirea.Api.Endpoint.Common.Attributes;
|
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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Controllers.V1;
|
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>
|
/// <summary>
|
||||||
/// Retrieves schedules based on various filters.
|
/// Retrieves schedules based on various filters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -25,7 +37,6 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
|
|||||||
[BadRequestResponse]
|
[BadRequestResponse]
|
||||||
[NotFoundResponse]
|
[NotFoundResponse]
|
||||||
public async Task<ActionResult<List<ScheduleResponse>>> Get([FromBody] ScheduleRequest request)
|
public async Task<ActionResult<List<ScheduleResponse>>> Get([FromBody] ScheduleRequest request)
|
||||||
|
|
||||||
{
|
{
|
||||||
if ((request.Groups == null || request.Groups.Length == 0) &&
|
if ((request.Groups == null || request.Groups.Length == 0) &&
|
||||||
(request.Disciplines == null || request.Disciplines.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="professors">An array of professor IDs.</param>
|
||||||
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
||||||
/// <returns>A response containing schedules for the specified group.</returns>
|
/// <returns>A response containing schedules for the specified group.</returns>
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("GetByGroup/{id:int}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[BadRequestResponse]
|
[BadRequestResponse]
|
||||||
@ -142,7 +153,7 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
|
|||||||
/// <param name="groups">An array of group IDs.</param>
|
/// <param name="groups">An array of group IDs.</param>
|
||||||
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
||||||
/// <returns>A response containing schedules for the specified professor.</returns>
|
/// <returns>A response containing schedules for the specified professor.</returns>
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("GetByProfessor/{id:int}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[BadRequestResponse]
|
[BadRequestResponse]
|
||||||
@ -203,7 +214,7 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
|
|||||||
/// <param name="professors">An array of professor IDs.</param>
|
/// <param name="professors">An array of professor IDs.</param>
|
||||||
/// <param name="groups">An array of group IDs.</param>
|
/// <param name="groups">An array of group IDs.</param>
|
||||||
/// <returns>A response containing schedules for the specified lecture hall.</returns>
|
/// <returns>A response containing schedules for the specified lecture hall.</returns>
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("GetByLectureHall/{id:int}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[BadRequestResponse]
|
[BadRequestResponse]
|
||||||
@ -264,7 +275,7 @@ public class ScheduleController(IMediator mediator) : BaseControllerV1
|
|||||||
/// <param name="professors">An array of professor IDs.</param>
|
/// <param name="professors">An array of professor IDs.</param>
|
||||||
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
/// <param name="lectureHalls">An array of lecture hall IDs.</param>
|
||||||
/// <returns>A response containing schedules for the specified discipline.</returns>
|
/// <returns>A response containing schedules for the specified discipline.</returns>
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("GetByDiscipline/{id:int}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[BadRequestResponse]
|
[BadRequestResponse]
|
||||||
|
@ -22,15 +22,21 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
|
<PackageReference Include="Cronos" Version="0.8.4" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<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="Swashbuckle.AspNetCore.Versioning" Version="2.0.0" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ApiDto\ApiDto.csproj" />
|
<ProjectReference Include="..\ApiDto\ApiDto.csproj" />
|
||||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\SqlData\Domain\Domain.csproj" />
|
||||||
<ProjectReference Include="..\Persistence\Persistence.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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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.Builder;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||||
@ -6,15 +7,26 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Mirea.Api.DataAccess.Application;
|
using Mirea.Api.DataAccess.Application;
|
||||||
using Mirea.Api.DataAccess.Persistence;
|
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.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 Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint;
|
namespace Mirea.Api.Endpoint;
|
||||||
|
|
||||||
@ -35,18 +47,83 @@ public class Program
|
|||||||
return result.Build();
|
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)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
|
Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Configuration.AddConfiguration(ConfigureEnvironment());
|
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.AddApplication();
|
||||||
builder.Services.AddPersistence(builder.Configuration);
|
builder.Services.AddPersistence(generalConfig?.DbSettings?.DatabaseProvider ?? DatabaseProvider.Sqlite, generalConfig?.DbSettings?.ConnectionStringSql ?? string.Empty);
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IMaintenanceModeNotConfigureService, MaintenanceModeNotConfigureService>();
|
||||||
|
builder.Services.AddSingleton<IMaintenanceModeService, MaintenanceModeService>();
|
||||||
|
builder.Services.AddSingleton<ISetupToken, SetupTokenService>();
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy("AllowAll", policy =>
|
options.AddPolicy("AllowAll", policy =>
|
||||||
@ -92,8 +169,25 @@ public class Program
|
|||||||
Console.WriteLine($"{item.Key}:{item.Value}");
|
Console.WriteLine($"{item.Key}:{item.Value}");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var uber = app.Services.CreateScope().ServiceProvider.GetService<UberDbContext>();
|
using (var scope = app.Services.CreateScope())
|
||||||
DbInitializer.Initialize(uber!);
|
{
|
||||||
|
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.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
@ -111,6 +205,8 @@ public class Program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
app.UseMiddleware<MaintenanceModeMiddleware>();
|
||||||
|
app.UseMiddleware<CustomExceptionHandlerMiddleware>();
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
<PackageReference Include="FluentValidation" Version="11.9.1" />
|
||||||
<PackageReference Include="FluentValidation" Version="11.9.0" />
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.1" />
|
||||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
|
<PackageReference Include="MediatR" Version="12.2.0" />
|
||||||
<PackageReference Include="MediatR" Version="12.2.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user