Release v1.0.0 #16
.editorconfig.envDbInitializer.csDependencyInjection.cs
.github/workflows
.gitignoreApiDto
ApiDto.csproj
Backend.slnDockerfileCommon
AuthRoles.csCacheType.csCronUpdateSkip.csDatabaseType.csOAuthAction.csOAuthProvider.csPairPeriodTime.csPasswordPolicy.csTwoFactorAuthentication.cs
Requests
Responses
AvailableOAuthProvidersResponse.csCampusBasicInfoResponse.csCampusDetailsResponse.cs
Configuration
DisciplineResponse.csFacultyResponse.csGroupDetailsResponse.csGroupResponse.csLectureHallDetailsResponse.csLectureHallResponse.csLessonTypeResponse.csProfessorResponse.csScheduleResponse.csTotpKeyResponse.csUserResponse.csEndpoint
Backend.httpISaveSettings.cs
README.mdCommon
Attributes
BadRequestResponseAttribute.csCacheMaxAgeAttribute.csLocalhostAttribute.csMaintenanceModeIgnoreAttribute.csNotFoundResponseAttribute.csSwaggerDefaultAttribute.csTokenAuthenticationAttribute.cs
Exceptions
Interfaces
MapperDto
AvailableProvidersConverter.csCronUpdateSkipConverter.csPairPeriodTimeConverter.csPasswordPolicyConverter.csTwoFactorAuthenticationConverter.csUserConverter.cs
Services
Configuration
Core
BackgroundTasks
Middleware
CacheMaxAgeMiddleware.csCookieAuthorizationMiddleware.csCustomExceptionHandlerMiddleware.csJwtRevocationMiddleware.csMaintenanceModeMiddleware.cs
Startup
Model
SwaggerOptions
ActionResultSchemaFilter.csConfigureSwaggerOptions.csDefaultValues.csEnumSchemaFilter.csExampleFilter.csTagSchemeFilter.cs
Validation
Controllers
BaseController.csConfigurationBaseController.cs
Endpoint.csprojProgram.csSetupConfiguration
V1
AuthController.csCampusController.cs
WeatherForecastController.csConfiguration
DisciplineController.csFacultyController.csGroupController.csImportController.csLectureHallController.csLessonTypeController.csProfessorController.csScheduleController.csSecurityController.csSync
WeatherForecast.cswwwroot
css
swagger
Security
Common
CookieNames.cs
DependencyInjection.csDomain
Interfaces
Model
OAuth2
ViewModel
Properties
Security.csprojServices
SqlData
Application
Application.csprojDependencyInjection.cs
Common
Cqrs
Campus
Queries
Discipline
Queries
Faculty
Queries
Group
Queries
LectureHall
Queries
Professor
Queries
GetProfessorDetails
GetProfessorDetailsBySearch
GetProfessorList
Schedule
Queries
TypeOfOccupation
Interfaces
Domain
Domain.csproj
Schedule
Migrations
MysqlMigrations
Migrations
20240601023106_InitialMigration.Designer.cs20240601023106_InitialMigration.cs20241027034820_RemoveUnusedRef.Designer.cs20241027034820_RemoveUnusedRef.csUberDbContextModelSnapshot.cs
MysqlMigrations.csprojPsqlMigrations
Migrations
20240601021702_InitialMigration.Designer.cs20240601021702_InitialMigration.cs20241027032753_RemoveUnusedRef.Designer.cs20241027032753_RemoveUnusedRef.csUberDbContextModelSnapshot.cs
PsqlMigrations.csprojSqliteMigrations
Persistence
Common
BaseDbContext.csConfigurationResolver.csDatabaseProvider.csDbContextFactory.csModelBuilderExtensions.cs
Contexts
Schedule
EntityTypeConfigurations
Persistence.csprojUberDbContext.cs
278
.editorconfig
Normal file
278
.editorconfig
Normal file
@ -0,0 +1,278 @@
|
||||
# Удалите строку ниже, если вы хотите наследовать параметры .editorconfig из каталогов, расположенных выше в иерархии
|
||||
root = true
|
||||
|
||||
# Файлы C#
|
||||
[*.cs]
|
||||
|
||||
#### Основные параметры EditorConfig ####
|
||||
|
||||
# Отступы и интервалы
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
|
||||
# Предпочтения для новых строк
|
||||
end_of_line = unset
|
||||
insert_final_newline = false
|
||||
|
||||
#### Действия кода .NET ####
|
||||
|
||||
# Члены типа
|
||||
dotnet_hide_advanced_members = false
|
||||
dotnet_member_insertion_location = with_other_members_of_the_same_kind
|
||||
dotnet_property_generation_behavior = prefer_throwing_properties
|
||||
|
||||
# Поиск символов
|
||||
dotnet_search_reference_assemblies = true
|
||||
|
||||
#### Рекомендации по написанию кода .NET ####
|
||||
|
||||
# Упорядочение Using
|
||||
dotnet_separate_import_directive_groups = false
|
||||
dotnet_sort_system_directives_first = false
|
||||
file_header_template = unset
|
||||
|
||||
# Предпочтения для this. и Me.
|
||||
dotnet_style_qualification_for_event = false
|
||||
dotnet_style_qualification_for_field = false
|
||||
dotnet_style_qualification_for_method = false
|
||||
dotnet_style_qualification_for_property = false
|
||||
|
||||
# Параметры использования ключевых слов языка и типов BCL
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true
|
||||
dotnet_style_predefined_type_for_member_access = true
|
||||
|
||||
# Предпочтения для скобок
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
|
||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
|
||||
|
||||
# Предпочтения модификатора
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members
|
||||
|
||||
# Выражения уровень предпочтения
|
||||
dotnet_prefer_system_hash_code = true
|
||||
dotnet_style_coalesce_expression = true
|
||||
dotnet_style_collection_initializer = true
|
||||
dotnet_style_explicit_tuple_names = true
|
||||
dotnet_style_namespace_match_folder = true
|
||||
dotnet_style_null_propagation = true
|
||||
dotnet_style_object_initializer = true
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
dotnet_style_prefer_auto_properties = true
|
||||
dotnet_style_prefer_collection_expression = when_types_loosely_match
|
||||
dotnet_style_prefer_compound_assignment = true
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true
|
||||
dotnet_style_prefer_conditional_expression_over_return = true
|
||||
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true
|
||||
dotnet_style_prefer_inferred_tuple_names = true
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true
|
||||
dotnet_style_prefer_simplified_interpolation = true
|
||||
|
||||
# Предпочтения для полей
|
||||
dotnet_style_readonly_field = true
|
||||
|
||||
# Настройки параметров
|
||||
dotnet_code_quality_unused_parameters = non_public
|
||||
|
||||
# Параметры подавления
|
||||
dotnet_remove_unnecessary_suppression_exclusions = none
|
||||
|
||||
# Предпочтения для новых строк
|
||||
dotnet_style_allow_multiple_blank_lines_experimental = true
|
||||
dotnet_style_allow_statement_immediately_after_block_experimental = false
|
||||
|
||||
#### Рекомендации по написанию кода C# ####
|
||||
|
||||
# Предпочтения var
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
csharp_style_var_for_built_in_types = true:silent
|
||||
csharp_style_var_when_type_is_apparent = true:silent
|
||||
|
||||
# Члены, заданные выражениями
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_constructors = true:silent
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = true:silent
|
||||
csharp_style_expression_bodied_methods = true:silent
|
||||
csharp_style_expression_bodied_operators = true:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
|
||||
# Настройки соответствия шаблонов
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_prefer_extended_property_pattern = true:suggestion
|
||||
csharp_style_prefer_not_pattern = true:suggestion
|
||||
csharp_style_prefer_pattern_matching = true:silent
|
||||
csharp_style_prefer_switch_expression = true:suggestion
|
||||
|
||||
# Настройки проверки на null
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Предпочтения модификатора
|
||||
csharp_prefer_static_anonymous_function = true:suggestion
|
||||
csharp_prefer_static_local_function = true:suggestion
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
|
||||
csharp_style_prefer_readonly_struct = true:suggestion
|
||||
csharp_style_prefer_readonly_struct_member = true:suggestion
|
||||
|
||||
# Предпочтения для блоков кода
|
||||
csharp_prefer_braces = when_multiline:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_system_threading_lock = true:suggestion
|
||||
csharp_style_namespace_declarations = file_scoped:silent
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
csharp_style_prefer_top_level_statements = false:silent
|
||||
|
||||
# Выражения уровень предпочтения
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_prefer_index_operator = true:suggestion
|
||||
csharp_style_prefer_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_prefer_null_check_over_type_check = true:suggestion
|
||||
csharp_style_prefer_range_operator = true:suggestion
|
||||
csharp_style_prefer_tuple_swap = true:suggestion
|
||||
csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
||||
|
||||
# предпочтения для директивы using
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
|
||||
# Предпочтения для новых строк
|
||||
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
|
||||
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
|
||||
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
|
||||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:silent
|
||||
csharp_style_allow_embedded_statements_on_same_line_experimental = false:silent
|
||||
|
||||
#### Правила форматирования C# ####
|
||||
|
||||
# Предпочтения для новых строк
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
|
||||
# Предпочтения для отступов
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_indent_switch_labels = true
|
||||
|
||||
# Предпочтения для интервалов
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = false
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
# Предпочтения переноса
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = true
|
||||
|
||||
#### Стили именования ####
|
||||
|
||||
# Правила именования
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = error
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Спецификации символов
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Стили именования
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
[*.{cs,vb}]
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
end_of_line = unset
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||
dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
|
||||
dotnet_style_namespace_match_folder = true:suggestion
|
||||
dotnet_code_quality_unused_parameters = non_public:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:silent
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
|
||||
dotnet_style_qualification_for_field = false:silent
|
||||
dotnet_style_qualification_for_property = false:silent
|
||||
dotnet_style_qualification_for_method = false:silent
|
||||
dotnet_style_qualification_for_event = false:silent
|
||||
dotnet_style_allow_multiple_blank_lines_experimental = true:silent
|
||||
dotnet_style_allow_statement_immediately_after_block_experimental = false:silent
|
||||
dotnet_style_readonly_field = true:suggestion
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
|
163
.env
Normal file
163
.env
Normal file
@ -0,0 +1,163 @@
|
||||
# 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=
|
||||
|
||||
# The actual sub path to the api
|
||||
# string
|
||||
# (optional)
|
||||
# If the specified path ends with "/api", the system will avoid duplicating "api" in the final URL.
|
||||
# This allows flexible API structuring, especially when running behind a reverse proxy or in containerized environments.
|
||||
ACTUAL_SUB_PATH=
|
||||
|
||||
# The sub path to the swagger
|
||||
# string
|
||||
# (optional)
|
||||
SWAGGER_SUB_PATH=swagger
|
||||
|
||||
# Internal port configuration
|
||||
# integer
|
||||
# (optional)
|
||||
# Specify the internal port on which the server will listen.
|
||||
INTERNAL_PORT=8080
|
||||
|
||||
# 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
|
||||
|
||||
### OAuth2
|
||||
|
||||
#### GOOGLE
|
||||
|
||||
# The client ID for Google OAuth
|
||||
# string
|
||||
# This is the client ID provided by Google when you register your application for OAuth.
|
||||
# It's necessary for enabling Google login functionality.
|
||||
GOOGLE_CLIENT_ID=
|
||||
|
||||
# The client secret for Google OAuth
|
||||
# string
|
||||
# This is the client secret provided by Google, used alongside the client ID to authenticate your application.
|
||||
# Make sure to keep it confidential.
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
#### Yandex
|
||||
|
||||
# The client ID for Yandex OAuth
|
||||
# string
|
||||
# This is the client ID provided by Yandex when you register your application for OAuth.
|
||||
# It's required for enabling Yandex login functionality.
|
||||
YANDEX_CLIENT_ID=
|
||||
|
||||
# The client secret for Yandex OAuth
|
||||
# string
|
||||
# This is the client secret provided by Yandex, used alongside the client ID to authenticate your application.
|
||||
# Keep it confidential to ensure the security of your app.
|
||||
YANDEX_CLIENT_SECRET=
|
||||
|
||||
#### MailRu
|
||||
|
||||
# The client ID for MailRu OAuth
|
||||
# string
|
||||
# This is the client ID provided by MailRu (Mail.ru Group) when you register your application for OAuth.
|
||||
# It's necessary for enabling MailRu login functionality.
|
||||
MAILRU_CLIENT_ID=
|
||||
|
||||
# The client secret for MailRu OAuth
|
||||
# string
|
||||
# This is the client secret provided by MailRu, used alongside the client ID to authenticate your application.
|
||||
# Keep it confidential to ensure the security of your app.
|
||||
MAILRU_CLIENT_SECRET=
|
30
.github/workflows/code-analyze.yaml
vendored
Normal file
30
.github/workflows/code-analyze.yaml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
name: .NET Test Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checking out
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: SonarScanner for .NET 8 with pull request decoration support
|
||||
uses: highbyte/sonarscan-dotnet@v2.3.3
|
||||
with:
|
||||
sonarProjectKey: $(echo "${{ github.repository }}" | cut -d'/' -f2)
|
||||
sonarProjectName: $(echo "${{ github.repository }}" | cut -d'/' -f2)
|
||||
sonarHostname: ${{ secrets.SONAR_HOST_URL }}
|
||||
dotnetPreBuildCmd: dotnet nuget add source --name="Winsomnia" --username ${{ secrets.NUGET_USERNAME }} --password ${{ secrets.NUGET_PASSWORD }} --store-password-in-clear-text ${{ secrets.NUGET_ADDRESS }} && dotnet format --verify-no-changes --diagnostics -v diag --severity warn
|
||||
dotnetTestArguments: --logger trx --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
|
||||
sonarBeginArguments: /d:sonar.cs.opencover.reportsPaths="**/TestResults/**/coverage.opencover.xml" -d:sonar.cs.vstest.reportsPaths="**/TestResults/*.trx"
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
97
.github/workflows/release-version.yml
vendored
Normal file
97
.github/workflows/release-version.yml
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
name: Build and Deploy Docker Container
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
[master]
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
docker build --build-arg NUGET_USERNAME=${{ secrets.NUGET_USERNAME }} --build-arg NUGET_PASSWORD=${{ secrets.NUGET_PASSWORD }} --build-arg NUGET_ADDRESS=${{ secrets.NUGET_ADDRESS }} -t ${{ secrets.DOCKER_USERNAME }}/mirea-backend:latest .
|
||||
docker push ${{ secrets.DOCKER_USERNAME }}/mirea-backend:latest
|
||||
|
||||
- name: Start ssh-agent
|
||||
id: ssh-agent
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Deploy to Server
|
||||
env:
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
||||
DOCKER_IMAGE: ${{ secrets.DOCKER_USERNAME }}/mirea-backend:latest
|
||||
PATH_TO_SAVE: /data
|
||||
SECURITY_SIGNING_TOKEN: ${{ secrets.SECURITY_SIGNING_TOKEN }}
|
||||
SECURITY_ENCRYPTION_TOKEN: ${{ secrets.SECURITY_ENCRYPTION_TOKEN }}
|
||||
SECURITY_LIFE_TIME_RT: ${{ secrets.SECURITY_LIFE_TIME_RT }}
|
||||
SECURITY_LIFE_TIME_JWT: ${{ secrets.SECURITY_LIFE_TIME_JWT }}
|
||||
SECURITY_LIFE_TIME_1_FA: ${{ secrets.SECURITY_LIFE_TIME_1_FA }}
|
||||
SECURITY_JWT_ISSUER: ${{ secrets.SECURITY_JWT_ISSUER }}
|
||||
SECURITY_JWT_AUDIENCE: ${{ secrets.SECURITY_JWT_AUDIENCE }}
|
||||
SECURITY_HASH_ITERATION: ${{ secrets.SECURITY_HASH_ITERATION }}
|
||||
SECURITY_HASH_MEMORY: ${{ secrets.SECURITY_HASH_MEMORY }}
|
||||
SECURITY_HASH_PARALLELISM: ${{ secrets.SECURITY_HASH_PARALLELISM }}
|
||||
SECURITY_HASH_SIZE: ${{ secrets.SECURITY_HASH_SIZE }}
|
||||
SECURITY_HASH_TOKEN: ${{ secrets.SECURITY_HASH_TOKEN }}
|
||||
SECURITY_SALT_SIZE: ${{ secrets.SECURITY_SALT_SIZE }}
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
YANDEX_CLIENT_ID: ${{ secrets.YANDEX_CLIENT_ID }}
|
||||
YANDEX_CLIENT_SECRET: ${{ secrets.YANDEX_CLIENT_SECRET }}
|
||||
MAILRU_CLIENT_ID: ${{ secrets.MAILRU_CLIENT_ID }}
|
||||
MAILRU_CLIENT_SECRET: ${{ secrets.MAILRU_CLIENT_SECRET }}
|
||||
run: |
|
||||
ssh-keyscan $SSH_HOST >> ~/.ssh/known_hosts
|
||||
ssh $SSH_USER@$SSH_HOST "
|
||||
docker pull $DOCKER_IMAGE &&
|
||||
docker stop mirea-backend || true &&
|
||||
docker rm mirea-backend || true &&
|
||||
docker run -d --name mirea-backend -p 8085:8080 \
|
||||
--restart=on-failure:10 \
|
||||
-v mirea-data:/data \
|
||||
-e PATH_TO_SAVE=$PATH_TO_SAVE \
|
||||
-e SECURITY_SIGNING_TOKEN=$SECURITY_SIGNING_TOKEN \
|
||||
-e SECURITY_ENCRYPTION_TOKEN=$SECURITY_ENCRYPTION_TOKEN \
|
||||
-e SECURITY_LIFE_TIME_RT=$SECURITY_LIFE_TIME_RT \
|
||||
-e SECURITY_LIFE_TIME_JWT=$SECURITY_LIFE_TIME_JWT \
|
||||
-e SECURITY_LIFE_TIME_1_FA=$SECURITY_LIFE_TIME_1_FA \
|
||||
-e SECURITY_JWT_ISSUER=$SECURITY_JWT_ISSUER \
|
||||
-e SECURITY_JWT_AUDIENCE=$SECURITY_JWT_AUDIENCE \
|
||||
-e SECURITY_HASH_ITERATION=$SECURITY_HASH_ITERATION \
|
||||
-e SECURITY_HASH_MEMORY=$SECURITY_HASH_MEMORY \
|
||||
-e SECURITY_HASH_PARALLELISM=$SECURITY_HASH_PARALLELISM \
|
||||
-e SECURITY_HASH_SIZE=$SECURITY_HASH_SIZE \
|
||||
-e SECURITY_HASH_TOKEN=$SECURITY_HASH_TOKEN \
|
||||
-e SECURITY_SALT_SIZE=$SECURITY_SALT_SIZE \
|
||||
-e ACTUAL_SUB_PATH=api \
|
||||
-e SWAGGER_SUB_PATH=swagger \
|
||||
-e TZ=Europe/Moscow \
|
||||
-e GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID \
|
||||
-e GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET \
|
||||
-e YANDEX_CLIENT_ID=$YANDEX_CLIENT_ID \
|
||||
-e YANDEX_CLIENT_SECRET=$YANDEX_CLIENT_SECRET \
|
||||
-e MAILRU_CLIENT_ID=$MAILRU_CLIENT_ID \
|
||||
-e MAILRU_CLIENT_SECRET=$MAILRU_CLIENT_SECRET \
|
||||
$DOCKER_IMAGE
|
||||
"
|
||||
|
||||
- name: Remove all keys from ssh-agent
|
||||
run: ssh-add -D
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -361,3 +361,5 @@ MigrationBackup/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
/ApiDto/ApiDtoDocs.xml
|
||||
/Endpoint/docs.xml
|
||||
|
42
ApiDto/ApiDto.csproj
Normal file
42
ApiDto/ApiDto.csproj
Normal file
@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Company>Winsomnia</Company>
|
||||
<Version>1.0.0</Version>
|
||||
<AssemblyVersion>1.0.3.0</AssemblyVersion>
|
||||
<FileVersion>1.0.3.0</FileVersion>
|
||||
<AssemblyName>Mirea.Api.Dto</AssemblyName>
|
||||
<RootNamespace>$(AssemblyName)</RootNamespace>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<DocumentationFile>ApiDtoDocs.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="ApiDtoDocs.xml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<CopyAllFilesToSingleFolderForPackageDependsOn>
|
||||
CopyXmlDocuments;
|
||||
$(CopyAllFilesToSingleFolderForPackageDependsOn);
|
||||
</CopyAllFilesToSingleFolderForPackageDependsOn>
|
||||
<CopyAllFilesToSingleFolderForMsdeployDependsOn>
|
||||
CopyXmlDocuments;
|
||||
$(CopyAllFilesToSingleFolderForMsdeployDependsOn);
|
||||
</CopyAllFilesToSingleFolderForMsdeployDependsOn>
|
||||
</PropertyGroup>
|
||||
<Target Name="CopyXmlDocuments">
|
||||
<ItemGroup>
|
||||
<XmlDocuments Include="$(OutDir)*.xml" />
|
||||
<FilesForPackagingFromProject Include="%(XmlDocuments.Identity)">
|
||||
<DestinationRelativePath>bin\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
|
||||
</FilesForPackagingFromProject>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
12
ApiDto/Common/AuthRoles.cs
Normal file
12
ApiDto/Common/AuthRoles.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace Mirea.Api.Dto.Common;
|
||||
|
||||
/// <summary>
|
||||
/// An enumeration that indicates which role the user belongs to
|
||||
/// </summary>
|
||||
public enum AuthRoles
|
||||
{
|
||||
/// <summary>
|
||||
/// Administrator
|
||||
/// </summary>
|
||||
Admin
|
||||
}
|
17
ApiDto/Common/CacheType.cs
Normal file
17
ApiDto/Common/CacheType.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Mirea.Api.Dto.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the types of caching mechanisms available.
|
||||
/// </summary>
|
||||
public enum CacheType
|
||||
{
|
||||
/// <summary>
|
||||
/// Memcached caching type.
|
||||
/// </summary>
|
||||
Memcached,
|
||||
|
||||
/// <summary>
|
||||
/// Redis caching type.
|
||||
/// </summary>
|
||||
Redis
|
||||
}
|
24
ApiDto/Common/CronUpdateSkip.cs
Normal file
24
ApiDto/Common/CronUpdateSkip.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Dto.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a date or date range to skip during cron update scheduling.
|
||||
/// </summary>
|
||||
public class CronUpdateSkip
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the start date of the skip range.
|
||||
/// </summary>
|
||||
public DateOnly? Start { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the end date of the skip range.
|
||||
/// </summary>
|
||||
public DateOnly? End { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific date to skip.
|
||||
/// </summary>
|
||||
public DateOnly? Date { get; set; }
|
||||
}
|
22
ApiDto/Common/DatabaseType.cs
Normal file
22
ApiDto/Common/DatabaseType.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace Mirea.Api.Dto.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the types of databases supported.
|
||||
/// </summary>
|
||||
public enum DatabaseType
|
||||
{
|
||||
/// <summary>
|
||||
/// MySQL database type.
|
||||
/// </summary>
|
||||
Mysql,
|
||||
|
||||
/// <summary>
|
||||
/// SQLite database type.
|
||||
/// </summary>
|
||||
Sqlite,
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL database type.
|
||||
/// </summary>
|
||||
PostgresSql
|
||||
}
|
17
ApiDto/Common/OAuthAction.cs
Normal file
17
ApiDto/Common/OAuthAction.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Mirea.Api.Dto.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the actions that can be performed with an OAuth token.
|
||||
/// </summary>
|
||||
public enum OAuthAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The action to log in the user using the provided OAuth token.
|
||||
/// </summary>
|
||||
Login,
|
||||
|
||||
/// <summary>
|
||||
/// The action to bind an OAuth provider to the user's account.
|
||||
/// </summary>
|
||||
Bind
|
||||
}
|
22
ApiDto/Common/OAuthProvider.cs
Normal file
22
ApiDto/Common/OAuthProvider.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace Mirea.Api.Dto.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents different OAuth providers for authentication.
|
||||
/// </summary>
|
||||
public enum OAuthProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth provider for Google.
|
||||
/// </summary>
|
||||
Google,
|
||||
|
||||
/// <summary>
|
||||
/// OAuth provider for Yandex.
|
||||
/// </summary>
|
||||
Yandex,
|
||||
|
||||
/// <summary>
|
||||
/// OAuth provider for Mail.ru.
|
||||
/// </summary>
|
||||
MailRu
|
||||
}
|
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; }
|
||||
}
|
32
ApiDto/Common/PasswordPolicy.cs
Normal file
32
ApiDto/Common/PasswordPolicy.cs
Normal file
@ -0,0 +1,32 @@
|
||||
namespace Mirea.Api.Dto.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the password policy settings for user authentication.
|
||||
/// </summary>
|
||||
public class PasswordPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum length required for a password.
|
||||
/// </summary>
|
||||
public int MinimumLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether at least one letter is required in the password.
|
||||
/// </summary>
|
||||
public bool RequireLetter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the password must contain both lowercase and uppercase letters.
|
||||
/// </summary>
|
||||
public bool RequireLettersDifferentCase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether at least one digit is required in the password.
|
||||
/// </summary>
|
||||
public bool RequireDigit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether at least one special character is required in the password.
|
||||
/// </summary>
|
||||
public bool RequireSpecialCharacter { get; set; }
|
||||
}
|
17
ApiDto/Common/TwoFactorAuthentication.cs
Normal file
17
ApiDto/Common/TwoFactorAuthentication.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Mirea.Api.Dto.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the steps required after a login attempt.
|
||||
/// </summary>
|
||||
public enum TwoFactorAuthentication
|
||||
{
|
||||
/// <summary>
|
||||
/// No additional steps required; the user is successfully logged in.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// TOTP (Time-based One-Time Password) is required for additional verification.
|
||||
/// </summary>
|
||||
TotpRequired,
|
||||
}
|
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; }
|
||||
}
|
38
ApiDto/Requests/Configuration/LoggingRequest.cs
Normal file
38
ApiDto/Requests/Configuration/LoggingRequest.cs
Normal file
@ -0,0 +1,38 @@
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the API key for integrating with Seq, a log aggregation service.
|
||||
/// If provided, logs will be sent to a Seq server using this API key.
|
||||
/// </summary>
|
||||
public string? ApiKeySeq { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server URL for the Seq logging service.
|
||||
/// This property specifies the Seq server endpoint to which logs will be sent.
|
||||
/// If <see cref="ApiKeySeq"/> is provided, logs will be sent to this server.
|
||||
/// </summary>
|
||||
public string? ApiServerSeq { get; set; }
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
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; }
|
||||
}
|
30
ApiDto/Requests/CreateUserRequest.cs
Normal file
30
ApiDto/Requests/CreateUserRequest.cs
Normal file
@ -0,0 +1,30 @@
|
||||
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>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public required string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username of the user.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
public required string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password of the user.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
public required string Password { get; set; }
|
||||
}
|
21
ApiDto/Requests/LoginRequest.cs
Normal file
21
ApiDto/Requests/LoginRequest.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request to receive protected content
|
||||
/// </summary>
|
||||
public class LoginRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Login or Email to identify the client.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The client's password.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Password { get; set; }
|
||||
}
|
37
ApiDto/Requests/ScheduleRequest.cs
Normal file
37
ApiDto/Requests/ScheduleRequest.cs
Normal file
@ -0,0 +1,37 @@
|
||||
namespace Mirea.Api.Dto.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request object for retrieving schedules based on various filters.
|
||||
/// </summary>
|
||||
public class ScheduleRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets an array of group IDs.
|
||||
/// </summary>
|
||||
public int[]? Groups { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to retrieve schedules for even weeks.
|
||||
/// </summary>
|
||||
public bool? IsEven { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an array of discipline IDs.
|
||||
/// </summary>
|
||||
public int[]? Disciplines { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an array of professor IDs.
|
||||
/// </summary>
|
||||
public int[]? Professors { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an array of lecture hall IDs.
|
||||
/// </summary>
|
||||
public int[]? LectureHalls { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an array of lesson type IDs.
|
||||
/// </summary>
|
||||
public int[]? LessonType { get; set; } = null;
|
||||
}
|
19
ApiDto/Requests/TwoFactorAuthRequest.cs
Normal file
19
ApiDto/Requests/TwoFactorAuthRequest.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Mirea.Api.Dto.Common;
|
||||
|
||||
namespace Mirea.Api.Dto.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request for verifying two-factor authentication.
|
||||
/// </summary>
|
||||
public class TwoFactorAuthRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the two-factor authentication code provided by the user.
|
||||
/// </summary>
|
||||
public required string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the two-factor authentication method used (e.g., TOTP, Email).
|
||||
/// </summary>
|
||||
public TwoFactorAuthentication Method { get; set; }
|
||||
}
|
27
ApiDto/Responses/AvailableOAuthProvidersResponse.cs
Normal file
27
ApiDto/Responses/AvailableOAuthProvidersResponse.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using Mirea.Api.Dto.Common;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response containing information about available OAuth providers.
|
||||
/// </summary>
|
||||
public class AvailableOAuthProvidersResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the OAuth provider.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string ProviderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the enum value representing the OAuth provider.
|
||||
/// </summary>
|
||||
public OAuthProvider Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the redirect URL for the OAuth provider's authorization process.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Redirect { get; set; }
|
||||
}
|
26
ApiDto/Responses/CampusBasicInfoResponse.cs
Normal file
26
ApiDto/Responses/CampusBasicInfoResponse.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents basic information about a campus.
|
||||
/// </summary>
|
||||
public class CampusBasicInfoResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the campus.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the code name of the campus.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string CodeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the full name of the campus (optional).
|
||||
/// </summary>
|
||||
public string? FullName { get; set; }
|
||||
}
|
31
ApiDto/Responses/CampusDetailsResponse.cs
Normal file
31
ApiDto/Responses/CampusDetailsResponse.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents detailed information about a campus.
|
||||
/// </summary>
|
||||
public class CampusDetailsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the campus.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the code name of the campus.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string CodeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the full name of the campus (optional).
|
||||
/// </summary>
|
||||
public string? FullName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address of the campus (optional).
|
||||
/// </summary>
|
||||
public string? Address { get; set; }
|
||||
}
|
29
ApiDto/Responses/Configuration/CacheResponse.cs
Normal file
29
ApiDto/Responses/Configuration/CacheResponse.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using Mirea.Api.Dto.Common;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response containing cache configuration details.
|
||||
/// </summary>
|
||||
public class CacheResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the type of cache database.
|
||||
/// </summary>
|
||||
public CacheType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server address.
|
||||
/// </summary>
|
||||
public string? Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the port number.
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
}
|
23
ApiDto/Responses/Configuration/CronUpdateScheduleResponse.cs
Normal file
23
ApiDto/Responses/Configuration/CronUpdateScheduleResponse.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response containing the cron update schedule and the next scheduled task dates.
|
||||
/// </summary>
|
||||
public class CronUpdateScheduleResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the cron expression representing the update schedule.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Cron { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of next scheduled task dates based on the cron expression.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required List<DateTime> NextStart { get; set; }
|
||||
}
|
49
ApiDto/Responses/Configuration/DatabaseResponse.cs
Normal file
49
ApiDto/Responses/Configuration/DatabaseResponse.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using Mirea.Api.Dto.Common;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response containing database configuration details.
|
||||
/// </summary>
|
||||
public class DatabaseResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the type of database.
|
||||
/// </summary>
|
||||
public DatabaseType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server address.
|
||||
/// </summary>
|
||||
public string? Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the port number.
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the database name.
|
||||
/// </summary>
|
||||
public string? Database { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username.
|
||||
/// </summary>
|
||||
public string? User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether SSL is enabled.
|
||||
/// </summary>
|
||||
public bool Ssl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to database. Only for Sqlite
|
||||
/// </summary>
|
||||
public string? PathToDatabase { get; set; }
|
||||
}
|
21
ApiDto/Responses/DisciplineResponse.cs
Normal file
21
ApiDto/Responses/DisciplineResponse.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about a discipline.
|
||||
/// </summary>
|
||||
public class DisciplineResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the discipline.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the discipline.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Name { get; set; }
|
||||
}
|
21
ApiDto/Responses/FacultyResponse.cs
Normal file
21
ApiDto/Responses/FacultyResponse.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about a faculty.
|
||||
/// </summary>
|
||||
public class FacultyResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the faculty.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the faculty.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Name { get; set; }
|
||||
}
|
37
ApiDto/Responses/GroupDetailsResponse.cs
Normal file
37
ApiDto/Responses/GroupDetailsResponse.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents detailed information about a group.
|
||||
/// </summary>
|
||||
public class GroupDetailsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the group.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the group.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the course number of the group.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int CourseNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the faculty to which the group belongs (optional).
|
||||
/// </summary>
|
||||
public int? FacultyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the faculty to which the group belongs (optional).
|
||||
/// </summary>
|
||||
public string? FacultyName { get; set; }
|
||||
}
|
32
ApiDto/Responses/GroupResponse.cs
Normal file
32
ApiDto/Responses/GroupResponse.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents basic information about a group.
|
||||
/// </summary>
|
||||
public class GroupResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the group.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the group.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the course number of the group.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int CourseNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the faculty to which the group belongs (optional).
|
||||
/// </summary>
|
||||
public int? FacultyId { get; set; }
|
||||
}
|
37
ApiDto/Responses/LectureHallDetailsResponse.cs
Normal file
37
ApiDto/Responses/LectureHallDetailsResponse.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the detailed response model for a lecture hall.
|
||||
/// </summary>
|
||||
public class LectureHallDetailsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the lecture hall.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the lecture hall.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the campus to which the lecture hall belongs.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int CampusId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the campus.
|
||||
/// </summary>
|
||||
public string? CampusName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the code of the campus.
|
||||
/// </summary>
|
||||
public string? CampusCode { get; set; }
|
||||
}
|
27
ApiDto/Responses/LectureHallResponse.cs
Normal file
27
ApiDto/Responses/LectureHallResponse.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response model for a lecture hall.
|
||||
/// </summary>
|
||||
public class LectureHallResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the lecture hall.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the lecture hall.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the campus to which the lecture hall belongs.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int CampusId { get; set; }
|
||||
}
|
21
ApiDto/Responses/LessonTypeResponse.cs
Normal file
21
ApiDto/Responses/LessonTypeResponse.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about a lesson type.
|
||||
/// </summary>
|
||||
public class LessonTypeResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the lesson type.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the lesson type.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Name { get; set; }
|
||||
}
|
26
ApiDto/Responses/ProfessorResponse.cs
Normal file
26
ApiDto/Responses/ProfessorResponse.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about a professor.
|
||||
/// </summary>
|
||||
public class ProfessorResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the professor.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the professor.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alternate name of the professor (optional).
|
||||
/// </summary>
|
||||
public string? AltName { get; set; }
|
||||
}
|
117
ApiDto/Responses/ScheduleResponse.cs
Normal file
117
ApiDto/Responses/ScheduleResponse.cs
Normal file
@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response object containing schedule information.
|
||||
/// </summary>
|
||||
public class ScheduleResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the day of the week for the schedule entry.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DayOfWeek DayOfWeek { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pair number for the schedule entry.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int PairNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the pair is on an even week.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool IsEven { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the discipline for the schedule entry.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Discipline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the discipline for the schedule entry.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required int DisciplineId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets exclude or include weeks for a specific discipline.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If is <see langword="true"/>, then the values in <see cref="Weeks"/> show the weeks when there will be no discipline.
|
||||
/// <br/>
|
||||
/// If is <see langword="false"/>, then the values in <see cref="Weeks"/> indicate the weeks during which a particular discipline will be studied.
|
||||
/// <br/>
|
||||
/// If is <see langword="null"/>, then there are no specific <see cref="Weeks"/>
|
||||
/// </remarks>
|
||||
///
|
||||
public bool? IsExcludedWeeks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The week numbers required for the correct display of the schedule.
|
||||
/// <br/>
|
||||
/// Whether there will be <see cref="Discipline"/> during the week or not depends on the <see cref="IsExcludedWeeks"/> property.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To get the current week's number, use other queries.
|
||||
/// </remarks>
|
||||
public IEnumerable<int>? Weeks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of occupation for the schedule entry.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required IEnumerable<string> TypeOfOccupations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the group for the schedule entry.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the group for the schedule entry.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required int GroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the names of the lecture halls for the schedule entry.
|
||||
/// </summary>
|
||||
public required IEnumerable<string?> LectureHalls { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the IDs of the lecture halls for the schedule entry.
|
||||
/// </summary>
|
||||
public required IEnumerable<int?> LectureHallsId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the names of the professors for the schedule entry.
|
||||
/// </summary>
|
||||
public required IEnumerable<string?> Professors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the IDs of the professors for the schedule entry.
|
||||
/// </summary>
|
||||
public required IEnumerable<int?> ProfessorsId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the names of the campuses for the schedule entry.
|
||||
/// </summary>
|
||||
public required IEnumerable<string?> Campus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the IDs of the campuses for the schedule entry.
|
||||
/// </summary>
|
||||
public required IEnumerable<int?> CampusId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the links to online meetings for the schedule entry.
|
||||
/// </summary>
|
||||
public required IEnumerable<string?> LinkToMeet { get; set; }
|
||||
}
|
17
ApiDto/Responses/TotpKeyResponse.cs
Normal file
17
ApiDto/Responses/TotpKeyResponse.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response containing the TOTP (Time-Based One-Time Password) key details.
|
||||
/// </summary>
|
||||
public class TotpKeyResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the secret key used for TOTP generation.
|
||||
/// </summary>
|
||||
public required string Secret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image (QR code) representing the TOTP key.
|
||||
/// </summary>
|
||||
public required string Image { get; set; }
|
||||
}
|
35
ApiDto/Responses/UserResponse.cs
Normal file
35
ApiDto/Responses/UserResponse.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Mirea.Api.Dto.Common;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Mirea.Api.Dto.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response containing user information.
|
||||
/// </summary>
|
||||
public class UserResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the email address of the user.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the username of the user.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the user has two-factor authentication enabled.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool TwoFactorAuthenticatorEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of OAuth providers used by the user.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required IEnumerable<OAuthProvider> UsedOAuthProviders { get; set; }
|
||||
}
|
80
Backend.sln
80
Backend.sln
@ -3,18 +3,49 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.8.34330.188
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "Domain\Domain.csproj", "{C27FB5CD-6A70-4FB2-847A-847B34806902}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Endpoint", "Endpoint\Endpoint.csproj", "{F3A1D12E-F5B2-4339-9966-DBF869E78357}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Elements of the solution", "Elements of the solution", "{3E087889-A4A0-4A55-A07D-7D149A5BC928}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.dockerignore = .dockerignore
|
||||
.editorconfig = .editorconfig
|
||||
.env = .env
|
||||
.gitattributes = .gitattributes
|
||||
.gitignore = .gitignore
|
||||
.github\workflows\code-analyze.yaml = .github\workflows\code-analyze.yaml
|
||||
Dockerfile = Dockerfile
|
||||
LICENSE.txt = LICENSE.txt
|
||||
README.md = README.md
|
||||
.github\workflows\release-version.yml = .github\workflows\release-version.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
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
|
||||
{48C9998C-ECE2-407F-835F-1A7255A5C99E} = {48C9998C-ECE2-407F-835F-1A7255A5C99E}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MysqlMigrations", "SqlData\Migrations\MysqlMigrations\MysqlMigrations.csproj", "{5861915B-9574-4D5D-872F-D54A09651697}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{48C9998C-ECE2-407F-835F-1A7255A5C99E} = {48C9998C-ECE2-407F-835F-1A7255A5C99E}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsqlMigrations", "SqlData\Migrations\PsqlMigrations\PsqlMigrations.csproj", "{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{48C9998C-ECE2-407F-835F-1A7255A5C99E} = {48C9998C-ECE2-407F-835F-1A7255A5C99E}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
@ -23,18 +54,55 @@ Global
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{C27FB5CD-6A70-4FB2-847A-847B34806902}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C27FB5CD-6A70-4FB2-847A-847B34806902}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C27FB5CD-6A70-4FB2-847A-847B34806902}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C27FB5CD-6A70-4FB2-847A-847B34806902}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F3A1D12E-F5B2-4339-9966-DBF869E78357}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0335FA36-E137-453F-853B-916674C168FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0335FA36-E137-453F-853B-916674C168FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3BFD6180-7CA7-4E85-A379-225B872439A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3BFD6180-7CA7-4E85-A379-225B872439A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3BFD6180-7CA7-4E85-A379-225B872439A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3BFD6180-7CA7-4E85-A379-225B872439A1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0B1F3656-E5B3-440C-961F-A7D004FBE9A8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{48C9998C-ECE2-407F-835F-1A7255A5C99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{48C9998C-ECE2-407F-835F-1A7255A5C99E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{48C9998C-ECE2-407F-835F-1A7255A5C99E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{48C9998C-ECE2-407F-835F-1A7255A5C99E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5861915B-9574-4D5D-872F-D54A09651697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5861915B-9574-4D5D-872F-D54A09651697}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5861915B-9574-4D5D-872F-D54A09651697}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5861915B-9574-4D5D-872F-D54A09651697}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{3BFD6180-7CA7-4E85-A379-225B872439A1} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
|
||||
{0B1F3656-E5B3-440C-961F-A7D004FBE9A8} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
|
||||
{48C9998C-ECE2-407F-835F-1A7255A5C99E} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
|
||||
{79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
|
||||
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7} = {79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5}
|
||||
{5861915B-9574-4D5D-872F-D54A09651697} = {79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5}
|
||||
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD} = {79639CD4-7A16-4AB4-BBE8-672B9ACCB3F5}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E80A1224-87F5-4FEB-82AE-89006BE98B12}
|
||||
EndGlobalSection
|
||||
|
39
Dockerfile
39
Dockerfile
@ -1,25 +1,32 @@
|
||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
USER app
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
LABEL company="Winsomnia"
|
||||
LABEL maintainer.name="Wesser" maintainer.email="support@winsomnia.net"
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl --fail http://localhost:8080/health || exit 1
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["Backend.csproj", "."]
|
||||
RUN dotnet restore "./././Backend.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/."
|
||||
RUN dotnet build "./Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
ARG NUGET_USERNAME
|
||||
ARG NUGET_PASSWORD
|
||||
ARG NUGET_ADDRESS
|
||||
|
||||
ENV NUGET_USERNAME=$NUGET_USERNAME
|
||||
ENV NUGET_PASSWORD=$NUGET_PASSWORD
|
||||
ENV NUGET_ADDRESS=$NUGET_ADDRESS
|
||||
|
||||
RUN dotnet nuget add source --name="Winsomnia" --username ${NUGET_USERNAME} --store-password-in-clear-text --password ${NUGET_PASSWORD} ${NUGET_ADDRESS}
|
||||
RUN dotnet restore ./Backend.sln
|
||||
WORKDIR /app
|
||||
WORKDIR /src
|
||||
RUN dotnet publish ./Endpoint/Endpoint.csproj -c Release --self-contained false -p:PublishSingleFile=false -o /app
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Backend.dll"]
|
||||
COPY --from=build /app .
|
||||
RUN find . -name "*.pdb" -type f -delete
|
||||
ENTRYPOINT ["dotnet", "Mirea.Api.Endpoint.dll"]
|
@ -1,6 +0,0 @@
|
||||
@Backend_HostAddress = http://localhost:5269
|
||||
|
||||
GET {{Backend_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public class BadRequestResponseAttribute() : ProducesResponseTypeAttribute(typeof(ProblemDetails), StatusCodes.Status400BadRequest);
|
26
Endpoint/Common/Attributes/CacheMaxAgeAttribute.cs
Normal file
26
Endpoint/Common/Attributes/CacheMaxAgeAttribute.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
|
||||
public class CacheMaxAgeAttribute : Attribute
|
||||
{
|
||||
public int MaxAge { get; }
|
||||
|
||||
public CacheMaxAgeAttribute(int days = 0, int hours = 0, int minutes = 0)
|
||||
{
|
||||
MaxAge = (int)new TimeSpan(days, hours, minutes, 0).TotalSeconds;
|
||||
}
|
||||
|
||||
public CacheMaxAgeAttribute(int minutes) : this(0, 0, minutes)
|
||||
{
|
||||
}
|
||||
|
||||
public CacheMaxAgeAttribute(bool usingSetting = false)
|
||||
{
|
||||
if (usingSetting)
|
||||
MaxAge = -1;
|
||||
else
|
||||
MaxAge = 0;
|
||||
}
|
||||
}
|
30
Endpoint/Common/Attributes/LocalhostAttribute.cs
Normal file
30
Endpoint/Common/Attributes/LocalhostAttribute.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using System;
|
||||
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)
|
||||
{
|
||||
context.Result = new UnauthorizedResult();
|
||||
return;
|
||||
}
|
||||
|
||||
var isRunningInContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER")?.ToLower() == "true";
|
||||
|
||||
if (IPAddress.IsLoopback(ip) || (isRunningInContainer && ip.ToString().StartsWith("172.")))
|
||||
{
|
||||
base.OnActionExecuting(context);
|
||||
return;
|
||||
}
|
||||
|
||||
context.Result = new UnauthorizedResult();
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
|
||||
public class MaintenanceModeIgnoreAttribute : Attribute;
|
8
Endpoint/Common/Attributes/NotFoundResponseAttribute.cs
Normal file
8
Endpoint/Common/Attributes/NotFoundResponseAttribute.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public class NotFoundResponseAttribute() : ProducesResponseTypeAttribute(typeof(ProblemDetails), StatusCodes.Status404NotFound);
|
9
Endpoint/Common/Attributes/SwaggerDefaultAttribute.cs
Normal file
9
Endpoint/Common/Attributes/SwaggerDefaultAttribute.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
public class SwaggerDefaultAttribute(string value) : Attribute
|
||||
{
|
||||
public string Value { get; } = value;
|
||||
}
|
28
Endpoint/Common/Attributes/TokenAuthenticationAttribute.cs
Normal file
28
Endpoint/Common/Attributes/TokenAuthenticationAttribute.cs
Normal file
@ -0,0 +1,28 @@
|
||||
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 const string AuthToken = "AuthToken";
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var setupToken = context.HttpContext.RequestServices.GetRequiredService<ISetupToken>();
|
||||
if (!context.HttpContext.Request.Cookies.TryGetValue(AuthToken, out var 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);
|
8
Endpoint/Common/Exceptions/ServerUnavailableException.cs
Normal file
8
Endpoint/Common/Exceptions/ServerUnavailableException.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Exceptions;
|
||||
|
||||
public class ServerUnavailableException(string message, bool addRetryAfter) : Exception(message)
|
||||
{
|
||||
public bool AddRetryAfter { get; } = addRetryAfter;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Mirea.Api.Endpoint.Common.Interfaces;
|
||||
|
||||
public interface IMaintenanceModeNotConfigureService
|
||||
{
|
||||
bool IsMaintenanceMode { get; }
|
||||
void DisableMaintenanceMode();
|
||||
}
|
8
Endpoint/Common/Interfaces/IMaintenanceModeService.cs
Normal file
8
Endpoint/Common/Interfaces/IMaintenanceModeService.cs
Normal file
@ -0,0 +1,8 @@
|
||||
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);
|
||||
}
|
27
Endpoint/Common/MapperDto/AvailableProvidersConverter.cs
Normal file
27
Endpoint/Common/MapperDto/AvailableProvidersConverter.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using Mirea.Api.Dto.Responses;
|
||||
using Mirea.Api.Security.Common.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
||||
|
||||
public static class AvailableProvidersConverter
|
||||
{
|
||||
public static Dto.Common.OAuthProvider ConvertToDto(this OAuthProvider provider) =>
|
||||
provider switch
|
||||
{
|
||||
OAuthProvider.Google => Dto.Common.OAuthProvider.Google,
|
||||
OAuthProvider.Yandex => Dto.Common.OAuthProvider.Yandex,
|
||||
OAuthProvider.MailRu => Dto.Common.OAuthProvider.MailRu,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(provider), provider, null)
|
||||
};
|
||||
|
||||
public static List<AvailableOAuthProvidersResponse> ConvertToDto(this IEnumerable<(OAuthProvider Provider, Uri Redirect)> data) =>
|
||||
data.Select(x => new AvailableOAuthProvidersResponse()
|
||||
{
|
||||
ProviderName = Enum.GetName(x.Provider)!,
|
||||
Provider = x.Provider.ConvertToDto(),
|
||||
Redirect = x.Redirect.ToString()
|
||||
}).ToList();
|
||||
}
|
19
Endpoint/Common/MapperDto/CronUpdateSkipConverter.cs
Normal file
19
Endpoint/Common/MapperDto/CronUpdateSkipConverter.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Mirea.Api.Endpoint.Common.Services;
|
||||
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
||||
|
||||
public static class CronUpdateSkipConverter
|
||||
{
|
||||
public static List<Dto.Common.CronUpdateSkip> ConvertToDto(this IEnumerable<ScheduleSettings.CronUpdateSkip> pairPeriod) =>
|
||||
pairPeriod.Select(x => new Dto.Common.CronUpdateSkip()
|
||||
{
|
||||
Start = x.Start,
|
||||
End = x.End,
|
||||
Date = x.Date
|
||||
}).ToList();
|
||||
public static List<ScheduleSettings.CronUpdateSkip> ConvertFromDto(this IEnumerable<Dto.Common.CronUpdateSkip> pairPeriod) =>
|
||||
pairPeriod.Select(x => x.Get()).ToList();
|
||||
}
|
14
Endpoint/Common/MapperDto/PairPeriodTimeConverter.cs
Normal file
14
Endpoint/Common/MapperDto/PairPeriodTimeConverter.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
||||
|
||||
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));
|
||||
}
|
23
Endpoint/Common/MapperDto/PasswordPolicyConverter.cs
Normal file
23
Endpoint/Common/MapperDto/PasswordPolicyConverter.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using PasswordPolicy = Mirea.Api.Dto.Common.PasswordPolicy;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
||||
|
||||
public static class PasswordPolicyConverter
|
||||
{
|
||||
public static Security.Common.Model.PasswordPolicy ConvertFromDto(this PasswordPolicy policy) =>
|
||||
new(policy.MinimumLength,
|
||||
policy.RequireLetter,
|
||||
policy.RequireLettersDifferentCase,
|
||||
policy.RequireDigit,
|
||||
policy.RequireSpecialCharacter);
|
||||
|
||||
public static PasswordPolicy ConvertToDto(this Security.Common.Model.PasswordPolicy policy) =>
|
||||
new()
|
||||
{
|
||||
MinimumLength = policy.MinimumLength,
|
||||
RequireLetter = policy.RequireLetter,
|
||||
RequireDigit = policy.RequireDigit,
|
||||
RequireSpecialCharacter = policy.RequireSpecialCharacter,
|
||||
RequireLettersDifferentCase = policy.RequireLettersDifferentCase
|
||||
};
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using Mirea.Api.Dto.Common;
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
||||
|
||||
public static class TwoFactorAuthenticationConverter
|
||||
{
|
||||
public static TwoFactorAuthentication ConvertToDto(this Security.Common.Model.TwoFactorAuthenticator authenticator) =>
|
||||
authenticator switch
|
||||
{
|
||||
Security.Common.Model.TwoFactorAuthenticator.None => TwoFactorAuthentication.None,
|
||||
Security.Common.Model.TwoFactorAuthenticator.Totp => TwoFactorAuthentication.TotpRequired,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(authenticator), authenticator, null)
|
||||
};
|
||||
|
||||
public static Security.Common.Model.TwoFactorAuthenticator ConvertFromDto(this TwoFactorAuthentication authentication) =>
|
||||
authentication switch
|
||||
{
|
||||
TwoFactorAuthentication.None => Security.Common.Model.TwoFactorAuthenticator.None,
|
||||
TwoFactorAuthentication.TotpRequired => Security.Common.Model.TwoFactorAuthenticator.Totp,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(authentication), authentication, null)
|
||||
};
|
||||
}
|
20
Endpoint/Common/MapperDto/UserConverter.cs
Normal file
20
Endpoint/Common/MapperDto/UserConverter.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using Mirea.Api.Endpoint.Configuration.Model;
|
||||
using Mirea.Api.Security.Common.Model;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
||||
|
||||
public static class UserConverter
|
||||
{
|
||||
public static User ConvertToSecurity(this Admin data) =>
|
||||
new()
|
||||
{
|
||||
Id = 1.ToString(),
|
||||
Email = data.Email,
|
||||
Username = data.Username,
|
||||
PasswordHash = data.PasswordHash,
|
||||
Salt = data.Salt,
|
||||
SecondFactorToken = data.Secret,
|
||||
TwoFactorAuthenticator = data.TwoFactorAuthenticator,
|
||||
OAuthProviders = data.OAuthProviders
|
||||
};
|
||||
}
|
78
Endpoint/Common/Services/CronUpdateSkipService.cs
Normal file
78
Endpoint/Common/Services/CronUpdateSkipService.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using Cronos;
|
||||
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Services;
|
||||
|
||||
public static class CronUpdateSkipService
|
||||
{
|
||||
public static ScheduleSettings.CronUpdateSkip Get(this Dto.Common.CronUpdateSkip date)
|
||||
{
|
||||
if (date.Date.HasValue)
|
||||
return new ScheduleSettings.CronUpdateSkip(date.Date.Value);
|
||||
if (date is { Start: not null, End: not null })
|
||||
return new ScheduleSettings.CronUpdateSkip(date.Start.Value, date.End.Value);
|
||||
|
||||
throw new ArgumentException("It is impossible to create a structure because it has incorrect values.");
|
||||
}
|
||||
|
||||
public static List<ScheduleSettings.CronUpdateSkip> FilterDateEntry(this List<ScheduleSettings.CronUpdateSkip> data, DateOnly? currentDate = null)
|
||||
{
|
||||
currentDate ??= DateOnly.FromDateTime(DateTime.Now);
|
||||
return data.OrderBy(x => x.End ?? x.Date)
|
||||
.Where(x => x.Date == currentDate || (x.Start <= currentDate && x.End >= currentDate))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<ScheduleSettings.CronUpdateSkip> FilterDateEntry(this List<ScheduleSettings.CronUpdateSkip> data, DateTime? currentDate = null) =>
|
||||
data.FilterDateEntry(DateOnly.FromDateTime(currentDate ?? DateTime.Now));
|
||||
|
||||
public static List<ScheduleSettings.CronUpdateSkip> Filter(this List<ScheduleSettings.CronUpdateSkip> data, DateOnly? currentDate = null)
|
||||
{
|
||||
currentDate ??= DateOnly.FromDateTime(DateTime.Now);
|
||||
|
||||
return data.Where(x => x.Date >= currentDate || x.End >= currentDate)
|
||||
.OrderBy(x => x.End ?? x.Date)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
public static List<DateTimeOffset> GetNextTask(this List<ScheduleSettings.CronUpdateSkip> data,
|
||||
CronExpression expression, int depth = 1, DateOnly? currentDate = null)
|
||||
{
|
||||
if (depth <= 0)
|
||||
return [];
|
||||
|
||||
DateTimeOffset nextRunTime = (currentDate?.ToDateTime(TimeOnly.MinValue) ?? DateTime.Now).ToUniversalTime();
|
||||
List<DateTimeOffset> result = [];
|
||||
|
||||
do
|
||||
{
|
||||
var lastSkippedEntry = data.FilterDateEntry(nextRunTime.DateTime).LastOrDefault();
|
||||
|
||||
if (lastSkippedEntry is { Start: not null, End: not null })
|
||||
nextRunTime = lastSkippedEntry.End.Value.ToDateTime(TimeOnly.MinValue).AddDays(1);
|
||||
else if (lastSkippedEntry.Date.HasValue)
|
||||
nextRunTime = lastSkippedEntry.Date.Value.ToDateTime(TimeOnly.MinValue).AddDays(1);
|
||||
|
||||
var nextOccurrence = expression.GetNextOccurrence(nextRunTime.AddMinutes(-1), TimeZoneInfo.Local);
|
||||
|
||||
if (!nextOccurrence.HasValue)
|
||||
return result;
|
||||
|
||||
if (data.FilterDateEntry(nextOccurrence.Value.DateTime).Count != 0)
|
||||
{
|
||||
nextRunTime = nextOccurrence.Value.AddDays(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(nextOccurrence.Value.ToLocalTime());
|
||||
nextRunTime = nextOccurrence.Value.AddMinutes(1);
|
||||
|
||||
} while (result.Count < depth);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
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)]);
|
||||
}
|
10
Endpoint/Common/Services/ScheduleSyncManager.cs
Normal file
10
Endpoint/Common/Services/ScheduleSyncManager.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Services;
|
||||
|
||||
public static class ScheduleSyncManager
|
||||
{
|
||||
public static event Action? OnForceSyncRequested;
|
||||
public static void RequestForceSync() =>
|
||||
OnForceSyncRequested?.Invoke();
|
||||
}
|
64
Endpoint/Common/Services/Security/DistributedCacheService.cs
Normal file
64
Endpoint/Common/Services/Security/DistributedCacheService.cs
Normal file
@ -0,0 +1,64 @@
|
||||
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 type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
if (type.IsPrimitive || type == typeof(string) || type == typeof(DateTime))
|
||||
{
|
||||
await cache.SetStringAsync(key, value?.ToString() ?? string.Empty, options, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var serializedValue = value as byte[] ?? JsonSerializer.SerializeToUtf8Bytes(value);
|
||||
await cache.SetAsync(key, serializedValue, options, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
|
||||
if (type.IsPrimitive || type == typeof(string) || type == typeof(DateTime))
|
||||
{
|
||||
var primitiveValue = await cache.GetStringAsync(key, cancellationToken);
|
||||
|
||||
if (string.IsNullOrEmpty(primitiveValue))
|
||||
return default;
|
||||
|
||||
if (type == typeof(string))
|
||||
return (T?)(object?)primitiveValue;
|
||||
|
||||
var tryParseMethod = type.GetMethod("TryParse", [typeof(string), type.MakeByRefType()])
|
||||
?? throw new NotSupportedException($"Type {type.Name} does not support TryParse.");
|
||||
|
||||
var parameters = new[] { primitiveValue, Activator.CreateInstance(type) };
|
||||
var success = (bool)tryParseMethod.Invoke(null, parameters)!;
|
||||
|
||||
if (success)
|
||||
return (T)parameters[1]!;
|
||||
|
||||
return 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 { private 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.NameIdentifier, 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;
|
||||
}
|
||||
}
|
63
Endpoint/Common/Services/Security/MemoryCacheService.cs
Normal file
63
Endpoint/Common/Services/Security/MemoryCacheService.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
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 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
|
||||
};
|
||||
|
||||
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
if (type.IsPrimitive || type == typeof(string) || type == typeof(DateTime))
|
||||
{
|
||||
cache.Set(key, value?.ToString() ?? string.Empty, options);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
cache.Set(key, value as byte[] ?? JsonSerializer.SerializeToUtf8Bytes(value), options);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
|
||||
if (!type.IsPrimitive && type != typeof(string) && type != typeof(DateTime))
|
||||
return Task.FromResult(
|
||||
cache.TryGetValue(key, out byte[]? value) ? JsonSerializer.Deserialize<T>(value) : default
|
||||
);
|
||||
|
||||
var primitiveValue = cache.Get(key);
|
||||
|
||||
if (string.IsNullOrEmpty(primitiveValue?.ToString()))
|
||||
return Task.FromResult<T?>(default);
|
||||
|
||||
if (type == typeof(string))
|
||||
return Task.FromResult((T?)primitiveValue);
|
||||
|
||||
var tryParseMethod = type.GetMethod("TryParse", [typeof(string), type.MakeByRefType()])
|
||||
?? throw new NotSupportedException($"Type {type.Name} does not support TryParse.");
|
||||
|
||||
var parameters = new[] { primitiveValue, Activator.CreateInstance(type) };
|
||||
var success = (bool)tryParseMethod.Invoke(null, parameters)!;
|
||||
|
||||
return success ? Task.FromResult((T?)parameters[1]) : Task.FromResult<T?>(default);
|
||||
}
|
||||
|
||||
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 _));
|
||||
}
|
58
Endpoint/Common/Services/UrlHelper.cs
Normal file
58
Endpoint/Common/Services/UrlHelper.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.Services;
|
||||
|
||||
public static class UrlHelper
|
||||
{
|
||||
public static string GetCurrentScheme(this HttpContext context) =>
|
||||
context.Request.Headers["X-Forwarded-Proto"].FirstOrDefault() ?? context.Request.Scheme;
|
||||
|
||||
public static string GetCurrentDomain(this HttpContext context) =>
|
||||
context.Request.Headers["X-Forwarded-Host"].FirstOrDefault() ?? context.Request.Host.Host;
|
||||
public static int? GetCurrentPort(this HttpContext context) =>
|
||||
string.IsNullOrEmpty(context.Request.Headers["X-Forwarded-Port"].FirstOrDefault()) ? context.Request.Host.Port :
|
||||
int.Parse(context.Request.Headers["X-Forwarded-Port"].First()!);
|
||||
|
||||
private static string CreateSubPath(string? path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return "/";
|
||||
|
||||
return "/" + path.Trim('/') + "/";
|
||||
}
|
||||
|
||||
public static string GetSubPath => CreateSubPath(Environment.GetEnvironmentVariable("ACTUAL_SUB_PATH"));
|
||||
|
||||
public static string GetSubPathWithoutFirstApiName
|
||||
{
|
||||
get
|
||||
{
|
||||
var path = GetSubPath;
|
||||
|
||||
if (string.IsNullOrEmpty(path) || path == "/")
|
||||
return CreateSubPath(null);
|
||||
|
||||
var parts = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts[^1].Equals("api", StringComparison.CurrentCultureIgnoreCase))
|
||||
parts = parts.Take(parts.Length - 1).ToArray();
|
||||
|
||||
return CreateSubPath(string.Join("/", parts));
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetSubPathSwagger => CreateSubPath(Environment.GetEnvironmentVariable("SWAGGER_SUB_PATH"));
|
||||
|
||||
public static string GetApiUrl(this HttpContext context, string apiPath = "")
|
||||
{
|
||||
var scheme = GetCurrentScheme(context);
|
||||
var domain = GetCurrentDomain(context).TrimEnd('/').Replace("localhost", "127.0.0.1");
|
||||
|
||||
var port = GetCurrentPort(context);
|
||||
var portString = port.HasValue && port != 80 && port != 443 ? $":{port}" : string.Empty;
|
||||
|
||||
return $"{scheme}://{domain}{portString}{GetSubPathWithoutFirstApiName}{apiPath.Trim('/')}";
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
using Cronos;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Mirea.Api.Endpoint.Common.Services;
|
||||
using Mirea.Api.Endpoint.Configuration.Model;
|
||||
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||
using Mirea.Api.Endpoint.Sync;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.BackgroundTasks;
|
||||
|
||||
public class ScheduleSyncService : IHostedService, IDisposable
|
||||
{
|
||||
private Timer? _timer;
|
||||
private string _cronUpdate;
|
||||
private List<ScheduleSettings.CronUpdateSkip> _cronUpdateSkip;
|
||||
private readonly ILogger<ScheduleSyncService> _logger;
|
||||
private CancellationTokenSource _cancellationTokenSource = new();
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IDisposable? _onChangeUpdateCron;
|
||||
|
||||
public ScheduleSyncService(IOptionsMonitor<GeneralConfig> generalConfigMonitor, ILogger<ScheduleSyncService> logger, IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
_cronUpdate = generalConfigMonitor.CurrentValue.ScheduleSettings!.CronUpdateSchedule;
|
||||
_cronUpdateSkip = generalConfigMonitor.CurrentValue.ScheduleSettings!.CronUpdateSkipDateList;
|
||||
|
||||
ScheduleSyncManager.OnForceSyncRequested += OnForceSyncRequested;
|
||||
_onChangeUpdateCron = generalConfigMonitor.OnChange((config) =>
|
||||
{
|
||||
var updated = false;
|
||||
if (config.ScheduleSettings?.CronUpdateSchedule != null && _cronUpdate != config.ScheduleSettings.CronUpdateSchedule)
|
||||
{
|
||||
_cronUpdate = config.ScheduleSettings.CronUpdateSchedule;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (config.ScheduleSettings?.CronUpdateSkipDateList != null && !config.ScheduleSettings.CronUpdateSkipDateList.SequenceEqual(_cronUpdateSkip))
|
||||
{
|
||||
_cronUpdateSkip = config.ScheduleSettings.CronUpdateSkipDateList
|
||||
.OrderBy(x => x.End ?? x.Date)
|
||||
.ToList();
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated)
|
||||
OnUpdateIntervalRequested();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnForceSyncRequested()
|
||||
{
|
||||
_logger.LogInformation("It was requested to synchronize the data immediately.");
|
||||
StopAsync(CancellationToken.None).ContinueWith(_ =>
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
ExecuteTask(null);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnUpdateIntervalRequested()
|
||||
{
|
||||
_logger.LogInformation("It was requested to update the time interval immediately.");
|
||||
StopAsync(CancellationToken.None).ContinueWith(_ =>
|
||||
{
|
||||
StartAsync(CancellationToken.None);
|
||||
});
|
||||
}
|
||||
|
||||
private void ScheduleNextRun()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_cronUpdate))
|
||||
{
|
||||
_logger.LogWarning("Cron expression is not set. The scheduled task will not run.");
|
||||
return;
|
||||
}
|
||||
|
||||
var expression = CronExpression.Parse(_cronUpdate);
|
||||
|
||||
var nextRunTime = _cronUpdateSkip.GetNextTask(expression).FirstOrDefault();
|
||||
|
||||
if (nextRunTime == default)
|
||||
{
|
||||
_logger.LogWarning("No next run time found. The task will not be scheduled. Timezone: {TimeZone}",
|
||||
TimeZoneInfo.Local.DisplayName);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Next task run in {Time}", nextRunTime.ToString("G"));
|
||||
|
||||
var delay = (nextRunTime - DateTimeOffset.Now).TotalMilliseconds;
|
||||
|
||||
// The chance is small, but it's better to check
|
||||
if (delay <= 0)
|
||||
delay = 1;
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_timer = new Timer(ExecuteTask, null, delay > int.MaxValue ? int.MaxValue : (int)delay, Timeout.Infinite);
|
||||
}
|
||||
|
||||
private async void ExecuteTask(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var syncService = ActivatorUtilities.GetServiceOrCreateInstance<ScheduleSynchronizer>(scope.ServiceProvider);
|
||||
await syncService.StartSync(_cancellationTokenSource.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occurred during schedule synchronization.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ScheduleNextRun();
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ScheduleNextRun();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
_timer?.Change(Timeout.Infinite, 0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
_timer?.Dispose();
|
||||
ScheduleSyncManager.OnForceSyncRequested -= OnForceSyncRequested;
|
||||
_onChangeUpdateCron?.Dispose();
|
||||
_cancellationTokenSource.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
using Cronos;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Mirea.Api.Endpoint.Common.Attributes;
|
||||
using Mirea.Api.Endpoint.Configuration.Model;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.Middleware;
|
||||
|
||||
public class CacheMaxAgeMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (!context.Response.StatusCode.ToString().StartsWith('2'))
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var endpoint = context.GetEndpoint();
|
||||
|
||||
var actionDescriptor = endpoint?.Metadata.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>();
|
||||
|
||||
if (actionDescriptor == null)
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var controllerType = actionDescriptor.ControllerTypeInfo;
|
||||
var methodInfo = actionDescriptor.MethodInfo;
|
||||
|
||||
var maxAgeAttribute = methodInfo.GetCustomAttribute<CacheMaxAgeAttribute>() ?? controllerType.GetCustomAttribute<CacheMaxAgeAttribute>();
|
||||
|
||||
if (maxAgeAttribute == null)
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (maxAgeAttribute.MaxAge)
|
||||
{
|
||||
case < 0:
|
||||
{
|
||||
DateTime? nextDate;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
using (var scope = serviceProvider.CreateScope())
|
||||
{
|
||||
var updateCronString = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<GeneralConfig>>().Value.ScheduleSettings?.CronUpdateSchedule;
|
||||
|
||||
if (string.IsNullOrEmpty(updateCronString) ||
|
||||
!CronExpression.TryParse(updateCronString, CronFormat.Standard, out var updateCron))
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
nextDate = updateCron.GetNextOccurrence(now);
|
||||
}
|
||||
|
||||
if (!nextDate.HasValue)
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
context.Response.Headers.CacheControl = "max-age=" + (int)(nextDate.Value - now).TotalSeconds;
|
||||
break;
|
||||
}
|
||||
case > 0:
|
||||
context.Response.Headers.CacheControl = "max-age=" + maxAgeAttribute.MaxAge;
|
||||
break;
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Mirea.Api.Security.Common;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.Middleware;
|
||||
|
||||
public class CookieAuthorizationMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (context.Request.Cookies.ContainsKey(CookieNames.AccessToken))
|
||||
context.Request.Headers.Authorization = "Bearer " + context.Request.Cookies[CookieNames.AccessToken];
|
||||
|
||||
await next(context);
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Mirea.Api.DataAccess.Application.Common.Exceptions;
|
||||
using Mirea.Api.Endpoint.Common.Exceptions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.Middleware;
|
||||
|
||||
public class CustomExceptionHandlerMiddleware(RequestDelegate next, ILogger<CustomExceptionHandlerMiddleware> logger)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
await HandleExceptionAsync(context, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||
{
|
||||
var traceId = Activity.Current?.Id ?? context.TraceIdentifier;
|
||||
|
||||
var problemDetails = new ProblemDetails
|
||||
{
|
||||
Type = "https://tools.ietf.org/html/rfc9110#section-15.6.1",
|
||||
Title = "An unexpected error occurred.",
|
||||
Status = StatusCodes.Status500InternalServerError,
|
||||
Detail = "Please provide this traceId to the administrator for further investigation.",
|
||||
Extensions = new Dictionary<string, object?>()
|
||||
{
|
||||
{ "traceId", traceId }
|
||||
}
|
||||
};
|
||||
|
||||
switch (exception)
|
||||
{
|
||||
case ValidationException validationException:
|
||||
problemDetails.Status = StatusCodes.Status400BadRequest;
|
||||
problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1";
|
||||
problemDetails.Title = "Validation errors occurred.";
|
||||
problemDetails.Extensions = new Dictionary<string, object?>
|
||||
{
|
||||
{ "errors", validationException.Errors.Select(e => e.ErrorMessage).ToArray() },
|
||||
{ "traceId", traceId }
|
||||
};
|
||||
break;
|
||||
case NotFoundException:
|
||||
problemDetails.Status = StatusCodes.Status404NotFound;
|
||||
problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.4";
|
||||
problemDetails.Title = "Resource not found.";
|
||||
break;
|
||||
case ControllerArgumentException:
|
||||
problemDetails.Status = StatusCodes.Status400BadRequest;
|
||||
problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1";
|
||||
problemDetails.Title = "Invalid arguments provided.";
|
||||
problemDetails.Detail = exception.Message;
|
||||
break;
|
||||
case SecurityException:
|
||||
problemDetails.Status = StatusCodes.Status401Unauthorized;
|
||||
problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.2";
|
||||
problemDetails.Title = "Unauthorized access.";
|
||||
problemDetails.Detail = exception.Message;
|
||||
break;
|
||||
case ServerUnavailableException unavailableException:
|
||||
problemDetails.Status = StatusCodes.Status503ServiceUnavailable;
|
||||
problemDetails.Type = "https://datatracker.ietf.org/doc/html/rfc9110#section-15.6.4";
|
||||
problemDetails.Title = "Server unavailable.";
|
||||
problemDetails.Detail = unavailableException.Message;
|
||||
if (unavailableException.AddRetryAfter)
|
||||
context.Response.Headers.RetryAfter = "600";
|
||||
break;
|
||||
}
|
||||
|
||||
if (problemDetails.Status == StatusCodes.Status500InternalServerError)
|
||||
logger.LogError(exception, "Internal server error when processing the request");
|
||||
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = problemDetails.Status.Value;
|
||||
|
||||
return context.Response.WriteAsync(JsonSerializer.Serialize(problemDetails));
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Mirea.Api.Security.Common.Interfaces;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.Middleware;
|
||||
|
||||
public class JwtRevocationMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context, IRevokedToken revokedTokenStore)
|
||||
{
|
||||
if (context.Request.Headers.ContainsKey("Authorization"))
|
||||
{
|
||||
var token = context.Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
||||
if (await revokedTokenStore.IsTokenRevokedAsync(token))
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Mirea.Api.Endpoint.Common.Attributes;
|
||||
using Mirea.Api.Endpoint.Common.Exceptions;
|
||||
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.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 InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (!maintenanceModeService.IsMaintenanceMode && !maintenanceModeNotConfigureService.IsMaintenanceMode || IsIgnoreMaintenanceMode(context))
|
||||
await next(context);
|
||||
else
|
||||
{
|
||||
|
||||
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||
context.Response.ContentType = "plain/text";
|
||||
|
||||
if (maintenanceModeService.IsMaintenanceMode)
|
||||
throw new ServerUnavailableException("The service is currently undergoing maintenance. Please try again later.", true);
|
||||
|
||||
throw new ServerUnavailableException(
|
||||
"The service is currently not configured. Go to the setup page if you are an administrator or try again later.", false);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||
|
||||
public static class ApiVersioningConfiguration
|
||||
{
|
||||
public static IApiVersioningBuilder AddCustomApiVersioning(this IServiceCollection services)
|
||||
{
|
||||
return services.AddApiVersioning(options =>
|
||||
{
|
||||
options.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
options.ReportApiVersions = true;
|
||||
options.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||
}).AddApiExplorer(options =>
|
||||
{
|
||||
options.GroupNameFormat = "'v'VVV";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
}
|
||||
}
|
26
Endpoint/Configuration/Core/Startup/CacheConfiguration.cs
Normal file
26
Endpoint/Configuration/Core/Startup/CacheConfiguration.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Mirea.Api.Dto.Common;
|
||||
using Mirea.Api.Endpoint.Configuration.Model;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||
|
||||
public static class CacheConfiguration
|
||||
{
|
||||
public static IServiceCollection AddCustomRedis(this IServiceCollection services, IConfiguration configuration, IHealthChecksBuilder? healthChecksBuilder = null)
|
||||
{
|
||||
var cache = configuration.Get<GeneralConfig>()?.CacheSettings;
|
||||
if (cache?.TypeDatabase != CacheType.Redis)
|
||||
return services;
|
||||
|
||||
services.AddStackExchangeRedisCache(options =>
|
||||
{
|
||||
options.Configuration = cache.ConnectionString;
|
||||
options.InstanceName = "mirea_";
|
||||
});
|
||||
|
||||
healthChecksBuilder?.AddRedis(cache.ConnectionString!, name: "Redis");
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||
|
||||
public static class EnvironmentConfiguration
|
||||
{
|
||||
private static Dictionary<string, string> LoadEnvironment(string envFile)
|
||||
{
|
||||
Dictionary<string, string> environment = [];
|
||||
|
||||
if (!File.Exists(envFile)) return environment;
|
||||
|
||||
foreach (var line in File.ReadAllLines(envFile))
|
||||
{
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
|
||||
var commentIndex = line.IndexOf('#', StringComparison.Ordinal);
|
||||
|
||||
var arg = line;
|
||||
|
||||
if (commentIndex != -1)
|
||||
arg = arg.Remove(commentIndex, arg.Length - commentIndex);
|
||||
|
||||
var parts = arg.Split(
|
||||
'=',
|
||||
StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length > 2)
|
||||
parts = [parts[0], string.Join("=", parts[1..])];
|
||||
|
||||
if (parts.Length != 2)
|
||||
continue;
|
||||
|
||||
environment.Add(parts[0].Trim(), parts[1].Trim());
|
||||
}
|
||||
|
||||
return environment;
|
||||
}
|
||||
|
||||
public static IConfigurationRoot GetEnvironment()
|
||||
{
|
||||
var variablesFromFile = LoadEnvironment(".env");
|
||||
|
||||
#if DEBUG
|
||||
LoadEnvironment(".env.develop").ToList().ForEach(x => variablesFromFile.Add(x.Key, x.Value));
|
||||
#endif
|
||||
|
||||
var environmentVariables = Environment.GetEnvironmentVariables()
|
||||
.OfType<DictionaryEntry>()
|
||||
.ToDictionary(
|
||||
entry => entry.Key.ToString() ?? string.Empty,
|
||||
entry => entry.Value?.ToString() ?? string.Empty
|
||||
);
|
||||
|
||||
var result = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(environmentVariables!)
|
||||
.AddInMemoryCollection(variablesFromFile!);
|
||||
|
||||
if (variablesFromFile.TryGetValue("PATH_TO_SAVE", out var pathToSave))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("PATH_TO_SAVE", pathToSave);
|
||||
if (!Directory.Exists(pathToSave))
|
||||
Directory.CreateDirectory(pathToSave);
|
||||
}
|
||||
|
||||
if (variablesFromFile.TryGetValue("ACTUAL_SUB_PATH", out var actualSubPath))
|
||||
Environment.SetEnvironmentVariable("ACTUAL_SUB_PATH", actualSubPath);
|
||||
|
||||
if (variablesFromFile.TryGetValue("SWAGGER_SUB_PATH", out var swaggerSubPath))
|
||||
Environment.SetEnvironmentVariable("SWAGGER_SUB_PATH", swaggerSubPath);
|
||||
|
||||
return result.Build();
|
||||
}
|
||||
}
|
67
Endpoint/Configuration/Core/Startup/JwtConfiguration.cs
Normal file
67
Endpoint/Configuration/Core/Startup/JwtConfiguration.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Mirea.Api.Endpoint.Common.Services.Security;
|
||||
using Mirea.Api.Security.Common.Interfaces;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||
|
||||
public static class JwtConfiguration
|
||||
{
|
||||
public static AuthenticationBuilder AddJwtToken(this 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
|
||||
});
|
||||
|
||||
return 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)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
102
Endpoint/Configuration/Core/Startup/LoggerConfiguration.cs
Normal file
102
Endpoint/Configuration/Core/Startup/LoggerConfiguration.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Mirea.Api.Endpoint.Common.Services;
|
||||
using Mirea.Api.Endpoint.Configuration.Model;
|
||||
using Serilog;
|
||||
using Serilog.Context;
|
||||
using Serilog.Events;
|
||||
using Serilog.Filters;
|
||||
using Serilog.Formatting.Compact;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||
|
||||
public static class LoggerConfiguration
|
||||
{
|
||||
public static IHostBuilder AddCustomSerilog(this IHostBuilder hostBuilder)
|
||||
{
|
||||
return hostBuilder.UseSerilog((context, _, configuration) =>
|
||||
{
|
||||
var generalConfig = context.Configuration.Get<GeneralConfig>()?.LogSettings;
|
||||
configuration
|
||||
.MinimumLevel.Debug()
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console(
|
||||
outputTemplate:
|
||||
"[{Level:u3}] [{Timestamp:dd.MM.yyyy HH:mm:ss}] {Message:lj}{NewLine}{Exception}");
|
||||
|
||||
if (generalConfig?.EnableLogToFile == true)
|
||||
{
|
||||
generalConfig.LogFilePath = PathBuilder.Combine(generalConfig.LogFilePath ?? string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(generalConfig.LogFilePath) && Directory.Exists(generalConfig.LogFilePath))
|
||||
Directory.CreateDirectory(generalConfig.LogFilePath);
|
||||
|
||||
configuration.WriteTo.File(
|
||||
new CompactJsonFormatter(),
|
||||
PathBuilder.Combine(
|
||||
generalConfig.LogFilePath!,
|
||||
generalConfig.LogFileName + ".json"
|
||||
),
|
||||
LogEventLevel.Debug,
|
||||
rollingInterval: RollingInterval.Day);
|
||||
}
|
||||
|
||||
if (generalConfig != null && !string.IsNullOrEmpty(generalConfig.ApiServerSeq) &&
|
||||
Uri.TryCreate(generalConfig.ApiServerSeq, UriKind.Absolute, out var _))
|
||||
configuration.WriteTo.Seq(generalConfig.ApiServerSeq, apiKey: generalConfig.ApiKeySeq);
|
||||
|
||||
configuration
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Authorization", LogEventLevel.Warning);
|
||||
|
||||
configuration.Filter.ByExcluding(Matching.WithProperty<string>("SourceContext", sc =>
|
||||
sc.Contains("Microsoft.EntityFrameworkCore.Database.Command")));
|
||||
});
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UseCustomSerilog(this IApplicationBuilder app)
|
||||
{
|
||||
return app.Use(async (context, next) =>
|
||||
{
|
||||
var traceId = Activity.Current?.Id ?? context.TraceIdentifier;
|
||||
|
||||
using (LogContext.PushProperty("TraceId", traceId))
|
||||
using (LogContext.PushProperty("UserAgent", context.Request.Headers.UserAgent.ToString()))
|
||||
using (LogContext.PushProperty("RemoteIPAddress", context.Connection.RemoteIpAddress?.ToString()))
|
||||
{
|
||||
await next();
|
||||
}
|
||||
}).UseSerilogRequestLogging(options =>
|
||||
{
|
||||
options.MessageTemplate = "[{RequestMethod}] {RequestPath} [Client {RemoteIPAddress}] [{StatusCode}] in {Elapsed:0.0000} ms";
|
||||
|
||||
options.GetLevel = (httpContext, elapsed, ex) =>
|
||||
{
|
||||
if (httpContext.Request.Path.StartsWithSegments("/health"))
|
||||
return LogEventLevel.Verbose;
|
||||
|
||||
return elapsed >= 2500 || ex != null
|
||||
? LogEventLevel.Warning
|
||||
: elapsed >= 1000
|
||||
? LogEventLevel.Information
|
||||
: LogEventLevel.Debug;
|
||||
};
|
||||
|
||||
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
|
||||
{
|
||||
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
|
||||
diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
|
||||
diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent);
|
||||
diagnosticContext.Set("RemoteIPAddress", httpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
26
Endpoint/Configuration/Core/Startup/SecureConfiguration.cs
Normal file
26
Endpoint/Configuration/Core/Startup/SecureConfiguration.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Mirea.Api.Dto.Common;
|
||||
using Mirea.Api.Endpoint.Common.Services.Security;
|
||||
using Mirea.Api.Endpoint.Configuration.Model;
|
||||
using Mirea.Api.Security;
|
||||
using Mirea.Api.Security.Common.Interfaces;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||
|
||||
public static class SecureConfiguration
|
||||
{
|
||||
public static IServiceCollection AddSecurity(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddSecurityServices(configuration);
|
||||
|
||||
services.AddSingleton<IRevokedToken, MemoryRevokedTokenService>();
|
||||
|
||||
if (configuration.Get<GeneralConfig>()?.CacheSettings?.TypeDatabase == CacheType.Redis)
|
||||
services.AddSingleton<ICacheService, DistributedCacheService>();
|
||||
else
|
||||
services.AddSingleton<ICacheService, MemoryCacheService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
77
Endpoint/Configuration/Core/Startup/SwaggerConfiguration.cs
Normal file
77
Endpoint/Configuration/Core/Startup/SwaggerConfiguration.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using Asp.Versioning.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Mirea.Api.Endpoint.Common.Services;
|
||||
using Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
|
||||
|
||||
public static class SwaggerConfiguration
|
||||
{
|
||||
public static IServiceCollection AddCustomSwagger(this IServiceCollection services)
|
||||
{
|
||||
services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.OperationFilter<TagSchemeFilter>();
|
||||
options.SchemaFilter<ExampleFilter>();
|
||||
options.OperationFilter<DefaultValues>();
|
||||
options.OperationFilter<ActionResultSchemaFilter>();
|
||||
options.SchemaFilter<EnumSchemaFilter>();
|
||||
var basePath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Description = "Keep the JWT token in the field (Bearer token)",
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.ApiKey
|
||||
});
|
||||
|
||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
}
|
||||
},
|
||||
[]
|
||||
}
|
||||
});
|
||||
|
||||
if (File.Exists(Path.Combine(basePath, "docs.xml")))
|
||||
options.IncludeXmlComments(Path.Combine(basePath, "docs.xml"));
|
||||
|
||||
if (File.Exists(Path.Combine(basePath, "ApiDtoDocs.xml")))
|
||||
options.IncludeXmlComments(Path.Combine(basePath, "ApiDtoDocs.xml"));
|
||||
});
|
||||
|
||||
return services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UseCustomSwagger(this IApplicationBuilder app, IServiceProvider services)
|
||||
{
|
||||
app.UseSwagger();
|
||||
return app.UseSwaggerUI(options =>
|
||||
{
|
||||
options.InjectStylesheet($"{UrlHelper.GetSubPath}css/swagger/SwaggerDark.css");
|
||||
var provider = services.GetService<IApiVersionDescriptionProvider>();
|
||||
|
||||
foreach (var description in provider!.ApiVersionDescriptions)
|
||||
{
|
||||
var url = $"/swagger/{description.GroupName}/swagger.json";
|
||||
var name = description.GroupName.ToUpperInvariant();
|
||||
options.SwaggerEndpoint(url, name);
|
||||
options.RoutePrefix = UrlHelper.GetSubPathSwagger.Trim('/');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
5
Endpoint/Configuration/ISaveSettings.cs
Normal file
5
Endpoint/Configuration/ISaveSettings.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace Mirea.Api.Endpoint.Configuration;
|
||||
public interface ISaveSettings
|
||||
{
|
||||
void SaveSetting();
|
||||
}
|
41
Endpoint/Configuration/Model/Admin.cs
Normal file
41
Endpoint/Configuration/Model/Admin.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Mirea.Api.Endpoint.Common.Services;
|
||||
using Mirea.Api.Security.Common.Domain;
|
||||
using Mirea.Api.Security.Common.Model;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Model;
|
||||
|
||||
public class Admin : ISaveSettings
|
||||
{
|
||||
[JsonIgnore] private const string FileName = "admin.json";
|
||||
private string _username = string.Empty;
|
||||
private string _email = string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public static string FilePath => PathBuilder.Combine(FileName);
|
||||
|
||||
public required string Username
|
||||
{
|
||||
get => _username;
|
||||
set => _username = value.Trim();
|
||||
}
|
||||
public required string Email
|
||||
{
|
||||
get => _email;
|
||||
set => _email = value.Trim();
|
||||
}
|
||||
public required string PasswordHash { get; set; }
|
||||
public required string Salt { get; set; }
|
||||
public TwoFactorAuthenticator TwoFactorAuthenticator { get; set; } = TwoFactorAuthenticator.None;
|
||||
public string? Secret { get; set; }
|
||||
|
||||
public Dictionary<OAuthProvider, OAuthUser>? OAuthProviders { get; set; }
|
||||
|
||||
public void SaveSetting()
|
||||
{
|
||||
File.WriteAllText(FilePath, JsonSerializer.Serialize(this));
|
||||
}
|
||||
}
|
36
Endpoint/Configuration/Model/GeneralConfig.cs
Normal file
36
Endpoint/Configuration/Model/GeneralConfig.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Mirea.Api.Endpoint.Common.Services;
|
||||
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||
using Mirea.Api.Security.Common.Model;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Model;
|
||||
|
||||
public class GeneralConfig : ISaveSettings
|
||||
{
|
||||
[JsonIgnore] private const string FileName = "Settings.json";
|
||||
|
||||
[JsonIgnore]
|
||||
public static string FilePath => PathBuilder.Combine(FileName);
|
||||
|
||||
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; }
|
||||
public PasswordPolicy PasswordPolicy { get; set; } = new();
|
||||
|
||||
public string? SecretForwardToken { get; set; }
|
||||
|
||||
public void SaveSetting()
|
||||
{
|
||||
File.WriteAllText(
|
||||
FilePath,
|
||||
JsonSerializer.Serialize(this, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
using Mirea.Api.Dto.Common;
|
||||
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||
|
||||
[RequiredSettings]
|
||||
public class CacheSettings : IIsConfigured
|
||||
{
|
||||
public CacheType TypeDatabase { get; set; }
|
||||
public string? ConnectionString { get; set; }
|
||||
|
||||
public bool IsConfigured()
|
||||
{
|
||||
return TypeDatabase == CacheType.Memcached ||
|
||||
!string.IsNullOrEmpty(ConnectionString);
|
||||
}
|
||||
}
|
28
Endpoint/Configuration/Model/GeneralSettings/DbSettings.cs
Normal file
28
Endpoint/Configuration/Model/GeneralSettings/DbSettings.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using Mirea.Api.DataAccess.Persistence.Common;
|
||||
using Mirea.Api.Dto.Common;
|
||||
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||
|
||||
[RequiredSettings]
|
||||
public class DbSettings : IIsConfigured
|
||||
{
|
||||
public DatabaseType TypeDatabase { get; set; }
|
||||
public required string ConnectionStringSql { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public DatabaseProvider DatabaseProvider =>
|
||||
TypeDatabase switch
|
||||
{
|
||||
DatabaseType.PostgresSql => DatabaseProvider.Postgresql,
|
||||
DatabaseType.Mysql => DatabaseProvider.Mysql,
|
||||
DatabaseType.Sqlite => DatabaseProvider.Sqlite,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
public bool IsConfigured() =>
|
||||
!string.IsNullOrEmpty(ConnectionStringSql);
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
21
Endpoint/Configuration/Model/GeneralSettings/LogSettings.cs
Normal file
21
Endpoint/Configuration/Model/GeneralSettings/LogSettings.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||
|
||||
[RequiredSettings]
|
||||
public class LogSettings : IIsConfigured
|
||||
{
|
||||
public bool EnableLogToFile { get; set; }
|
||||
public string? LogFilePath { get; set; }
|
||||
public string? LogFileName { get; set; }
|
||||
public string? ApiKeySeq { get; set; }
|
||||
public string? ApiServerSeq { get; set; }
|
||||
|
||||
public bool IsConfigured()
|
||||
{
|
||||
return !EnableLogToFile ||
|
||||
!string.IsNullOrEmpty(LogFilePath) &&
|
||||
!string.IsNullOrEmpty(LogFileName);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||
|
||||
[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 record struct CronUpdateSkip
|
||||
{
|
||||
public DateOnly? Start { get; set; }
|
||||
public DateOnly? End { get; set; }
|
||||
public DateOnly? Date { get; set; }
|
||||
|
||||
public CronUpdateSkip(DateOnly d1, DateOnly d2)
|
||||
{
|
||||
if (d1 > d2)
|
||||
{
|
||||
Start = d2;
|
||||
End = d1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Start = d1;
|
||||
End = d2;
|
||||
}
|
||||
}
|
||||
|
||||
public CronUpdateSkip(DateOnly d1) => Date = d1;
|
||||
}
|
||||
|
||||
public required string CronUpdateSchedule { get; set; }
|
||||
public DateOnly StartTerm { get; set; }
|
||||
public required IDictionary<int, PairPeriodTime> PairPeriod { get; set; }
|
||||
public List<CronUpdateSkip> CronUpdateSkipDateList { get; set; } = [];
|
||||
|
||||
public bool IsConfigured()
|
||||
{
|
||||
return !string.IsNullOrEmpty(CronUpdateSchedule) &&
|
||||
StartTerm != default &&
|
||||
PairPeriod.Count != 0 &&
|
||||
PairPeriod.Any();
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||
|
||||
public class ActionResultSchemaFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var returnType = context.MethodInfo.ReturnType;
|
||||
if (!returnType.IsEquivalentTo(typeof(ActionResult)) &&
|
||||
!returnType.IsEquivalentTo(typeof(ContentResult)) &&
|
||||
!returnType.IsEquivalentTo(typeof(FileStreamResult)) &&
|
||||
!returnType.IsGenericType)
|
||||
return;
|
||||
|
||||
if (returnType.IsGenericType &&
|
||||
!returnType.GetGenericTypeDefinition().IsEquivalentTo(typeof(ActionResult<>)) &&
|
||||
!returnType.GetGenericTypeDefinition().IsEquivalentTo(typeof(Task<>)))
|
||||
return;
|
||||
|
||||
var genericType = returnType.IsGenericType ? returnType.GetGenericArguments().FirstOrDefault() : returnType;
|
||||
if (genericType == null)
|
||||
return;
|
||||
|
||||
var responseTypeAttributes = context.MethodInfo.GetCustomAttributes(typeof(ProducesResponseTypeAttribute), false)
|
||||
.Cast<ProducesResponseTypeAttribute>()
|
||||
.Where(attr => attr.StatusCode == 200)
|
||||
.ToList();
|
||||
|
||||
var contentType = "application/json";
|
||||
|
||||
if (context.MethodInfo.GetCustomAttributes(typeof(ProducesAttribute), false)
|
||||
.FirstOrDefault() is ProducesAttribute producesAttribute)
|
||||
contentType = producesAttribute.ContentTypes.FirstOrDefault() ?? "application/json";
|
||||
|
||||
if (responseTypeAttributes.Count != 0)
|
||||
{
|
||||
var responseType = responseTypeAttributes.First().Type;
|
||||
genericType = responseType;
|
||||
}
|
||||
|
||||
if (genericType.IsEquivalentTo(typeof(ContentResult)) || genericType.IsEquivalentTo(typeof(FileStreamResult)))
|
||||
{
|
||||
operation.Responses["200"] = new OpenApiResponse
|
||||
{
|
||||
Description = "OK",
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
[contentType] = new()
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (genericType == typeof(ActionResult))
|
||||
{
|
||||
operation.Responses["200"] = new OpenApiResponse { Description = "OK" };
|
||||
}
|
||||
else
|
||||
{
|
||||
OpenApiSchema schema;
|
||||
if (genericType.IsGenericType && genericType.GetGenericTypeDefinition() == typeof(ActionResult<>))
|
||||
schema = context.SchemaGenerator.GenerateSchema(genericType.GetGenericArguments().FirstOrDefault(),
|
||||
context.SchemaRepository);
|
||||
else
|
||||
schema = context.SchemaGenerator.GenerateSchema(genericType, context.SchemaRepository);
|
||||
|
||||
operation.Responses["200"] = new OpenApiResponse
|
||||
{
|
||||
Description = "OK",
|
||||
Content = new Dictionary<string, OpenApiMediaType>
|
||||
{
|
||||
[contentType] = new() { Schema = schema }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using Asp.Versioning.ApiExplorer;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||
|
||||
public class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
public void Configure(SwaggerGenOptions options)
|
||||
{
|
||||
foreach (var description in provider.ApiVersionDescriptions)
|
||||
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
|
||||
}
|
||||
|
||||
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
|
||||
{
|
||||
var info = new OpenApiInfo()
|
||||
{
|
||||
Title = $"MIREA Schedule Web API ({FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion})",
|
||||
Version = description.ApiVersion.ToString(),
|
||||
Description = "This API provides a convenient interface for retrieving data stored in the database. " +
|
||||
"Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.",
|
||||
Contact = new OpenApiContact { Name = "Author name", Email = "support@winsomnia.net" },
|
||||
License = new OpenApiLicense { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") }
|
||||
};
|
||||
|
||||
if (description.IsDeprecated)
|
||||
info.Description += " This API version has been deprecated.";
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
51
Endpoint/Configuration/SwaggerOptions/DefaultValues.cs
Normal file
51
Endpoint/Configuration/SwaggerOptions/DefaultValues.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||
|
||||
public class DefaultValues : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var apiDescription = context.ApiDescription;
|
||||
operation.Deprecated |= apiDescription.IsDeprecated();
|
||||
|
||||
foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
|
||||
{
|
||||
var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
|
||||
var response = operation.Responses[responseKey];
|
||||
|
||||
foreach (var contentType in response.Content.Keys)
|
||||
{
|
||||
if (responseType.ApiResponseFormats.All(x => x.MediaType != contentType))
|
||||
response.Content.Remove(contentType);
|
||||
}
|
||||
}
|
||||
|
||||
if (operation.Parameters == null)
|
||||
return;
|
||||
|
||||
foreach (var parameter in operation.Parameters)
|
||||
{
|
||||
var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
|
||||
|
||||
parameter.Description ??= description.ModelMetadata.Description;
|
||||
|
||||
if (parameter.Schema.Default == null &&
|
||||
description.DefaultValue != null &&
|
||||
description.DefaultValue is not DBNull &&
|
||||
description.ModelMetadata is ModelMetadata modelMetadata)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(description.DefaultValue, modelMetadata.ModelType);
|
||||
parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
|
||||
}
|
||||
|
||||
parameter.Required |= description.IsRequired;
|
||||
}
|
||||
}
|
||||
}
|
28
Endpoint/Configuration/SwaggerOptions/EnumSchemaFilter.cs
Normal file
28
Endpoint/Configuration/SwaggerOptions/EnumSchemaFilter.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||
|
||||
public class EnumSchemaFilter : ISchemaFilter
|
||||
{
|
||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||
{
|
||||
if (!context.Type.IsEnum)
|
||||
return;
|
||||
|
||||
schema.Enum.Clear();
|
||||
|
||||
var enumValues = Enum.GetNames(context.Type)
|
||||
.Select(name => new OpenApiString(name))
|
||||
.ToList();
|
||||
|
||||
foreach (var value in enumValues)
|
||||
schema.Enum.Add(value);
|
||||
|
||||
schema.Type = "string";
|
||||
schema.Format = null;
|
||||
}
|
||||
}
|
16
Endpoint/Configuration/SwaggerOptions/ExampleFilter.cs
Normal file
16
Endpoint/Configuration/SwaggerOptions/ExampleFilter.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Mirea.Api.Endpoint.Common.Attributes;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||
|
||||
public class ExampleFilter : ISchemaFilter
|
||||
{
|
||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||
{
|
||||
var att = context.ParameterInfo?.GetCustomAttribute<SwaggerDefaultAttribute>();
|
||||
if (att != null)
|
||||
schema.Example = new Microsoft.OpenApi.Any.OpenApiString(att.Value);
|
||||
}
|
||||
}
|
40
Endpoint/Configuration/SwaggerOptions/TagSchemeFilter.cs
Normal file
40
Endpoint/Configuration/SwaggerOptions/TagSchemeFilter.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||
|
||||
public class TagSchemeFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (context.ApiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
|
||||
return;
|
||||
|
||||
var controllerType = controllerActionDescriptor.ControllerTypeInfo;
|
||||
|
||||
var tagsAttribute = controllerType.GetCustomAttributes<TagsAttribute>(inherit: true).FirstOrDefault();
|
||||
|
||||
if (tagsAttribute == null)
|
||||
{
|
||||
var baseType = controllerType.BaseType;
|
||||
while (baseType != null)
|
||||
{
|
||||
tagsAttribute = baseType.GetCustomAttributes<TagsAttribute>(inherit: true).FirstOrDefault();
|
||||
if (tagsAttribute != null)
|
||||
break;
|
||||
|
||||
baseType = baseType.BaseType;
|
||||
}
|
||||
}
|
||||
|
||||
if (tagsAttribute == null)
|
||||
return;
|
||||
|
||||
operation.Tags ??= [];
|
||||
operation.Tags.Add(new OpenApiTag { Name = tagsAttribute.Tags[0] });
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class RequiredSettingsAttribute : Attribute;
|
||||
|
||||
// todo: only with IIsConfigured. If possible add Roslyn Analyzer later
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user