Compare commits
292 Commits
8a103831eb
...
release/v1
Author | SHA1 | Date | |
---|---|---|---|
46bbc34956 | |||
047ccfa754 | |||
b0d9a67c1c | |||
3eb043b24c | |||
4cd476764d | |||
90b4662dda | |||
e7edc79ebc | |||
aabeed0aa5 | |||
e79ec360ea | |||
31c1d2804d | |||
ea4c8b61e0 | |||
b40e394bcf | |||
885b937b0b | |||
dc08285ec8 | |||
b3a0964aac | |||
7d6b21c5bb | |||
93912caf01 | |||
c725cfed32 | |||
7c7707b1e2 | |||
1687e9d89b | |||
8d1b709b43 | |||
ce6b0f2673 | |||
16afc0bc69 | |||
c9bc6a3565 | |||
ad8f356fc1 | |||
dda0a29300 | |||
369901db78 | |||
a67b72b7fb | |||
2453b2bd51 | |||
5870eef552 | |||
52de98969d | |||
bc86e077bd | |||
03b6560bc4 | |||
5bcb7bfbc1 | |||
38fba5556f | |||
fd26178a24 | |||
7eb307b65e | |||
56c7196100 | |||
92081156cf | |||
6358410f18 | |||
e79ddf220f | |||
c3c9844e2f | |||
206720cd63 | |||
d9f4176aca | |||
1de344ac25 | |||
61a11ea223 | |||
07111b9b61 | |||
538f1d67c8 | |||
233458ed89 | |||
7f87b4d856 | |||
0c6d1c9bfb | |||
516ba5bb8e | |||
9d5007ef3a | |||
c75ac60b0b | |||
5b7412f20f | |||
c4a4478b8c | |||
05166188be | |||
157708d00f | |||
36026b3afb | |||
43edab2912 | |||
dcdd43469b | |||
17fd260068 | |||
97187a8e45 | |||
cfe08dcf9b | |||
ae4d2073c4 | |||
269d976ad4 | |||
5fa545e981 | |||
2ab5dea8ba | |||
5e65aded79 | |||
dfac9ddca8 | |||
c66f3355ec | |||
c12323dc29 | |||
71c31c0bbb | |||
8c51ba83a4 | |||
9ff0f51e19 | |||
408a95e4b3 | |||
2a33ecbf07 | |||
97e50b5331 | |||
d505041c72 | |||
5ff8744a55 | |||
053f01eec1 | |||
e8e94e45a5 | |||
55562a9f00 | |||
57b9819d13 | |||
78254ed23d | |||
202d20bb25 | |||
3e05863aea | |||
b82fbc491f | |||
85722f8552 | |||
9231c4d5ca | |||
7b94f9cc1f | |||
7bafbb95c4 | |||
544ad6e791 | |||
e4b942d062 | |||
f2e79e51f2 | |||
5cc54eac44 | |||
e9ff1cabe8 | |||
fd578aa61e | |||
cff42d0a31 | |||
8250957b85 | |||
39208037f0 | |||
5e072d88c2 | |||
25eddbe776 | |||
74ba4e901a | |||
e760ddae0a | |||
598ebabc5c | |||
08aeb7ea3c | |||
182235c4cd | |||
5437623a20 | |||
2c09122971 | |||
503f5792fb | |||
95627003e5 | |||
a96073d44d | |||
5f36e0f75b | |||
e977de3e4f | |||
65d928ec2d | |||
713bbfa16f | |||
6b5eda7756 | |||
dbd9e1a070 | |||
0dda336de1 | |||
727f5c276e | |||
db70e4dd96 | |||
6831d9c708 | |||
1b24954c3e | |||
c5ba1cfcca | |||
3811d879ab | |||
61dc0a8bc4 | |||
b3b00aa9e1 | |||
6c9af942f4 | |||
23f74b3bdf | |||
eb272baa38 | |||
a0ff624481 | |||
cd6f25deba | |||
0f47a98ad9 | |||
3279ef594b | |||
5bc729eb66 | |||
5317b7b563 | |||
665544236f | |||
f203ee71f0 | |||
d8dbf1562f | |||
dead9f89bb | |||
8c932cf0be | |||
80e74b34c1 | |||
b095ca9749 | |||
8fad070a9c | |||
6c20713d81 | |||
fc5ec1fd54 | |||
ed99fce9b8 | |||
2ccc476686 | |||
84d7b095f0 | |||
4605c81895 | |||
0788c36bd2 | |||
f5dbc46856 | |||
ebec0a2d2b | |||
4fc28378c5 | |||
98ee3c389c | |||
428c2dc3ba | |||
4970dd782a | |||
2e48b0067f | |||
71e8eca5f4 | |||
1f3aaca3cf | |||
b49df925d4 | |||
c9ebddf27a | |||
f5739647b2 | |||
26dbf608b9 | |||
2b89dd07a9 | |||
1c981fb7bf | |||
de5dc274d7 | |||
c5ecf00932 | |||
412751e30f | |||
076d6498a1 | |||
88d78dfab3 | |||
332e5a013b | |||
e8450400c7 | |||
65709e1f83 | |||
1e204c948c | |||
0ced152fc9 | |||
6f9bfd3880 | |||
ae0f437e2c | |||
592e8a1b42 | |||
a27549092b | |||
f27d07fb5a | |||
535bafa73a | |||
fba842acc3 | |||
31087a57c9 | |||
24c75e4306 | |||
dee89b278b | |||
565252382c | |||
80dc2e412c | |||
b1250616a7 | |||
c51a9cecc9 | |||
c189cc6955 | |||
c7b401eae7 | |||
837205f66e | |||
c6ca717b89 | |||
497b7f146b | |||
3326b17d74 | |||
80b46754ad | |||
3898463bc4 | |||
279ca9647b | |||
ab660f69c8 | |||
1c27bffa73 | |||
76fd1347ce | |||
820828276e | |||
ac4804e864 | |||
21055176ac | |||
f42caa3a45 | |||
57f4d1b822 | |||
cdb738ca42 | |||
d45c865f4e | |||
d87654a355 | |||
75c1aebea6 | |||
17e20fee2e | |||
e8ca2c42a6 | |||
9133b57a1b | |||
05ca45db49 | |||
7e2016080f | |||
fe24dfcd6a | |||
2041a187e7 | |||
098fca5df8 | |||
e0cff050de | |||
2f7c77e764 | |||
7e82d4a520 | |||
07edf0e5ad | |||
5b8d9e1f4a | |||
de2f909ed6 | |||
2e2cee2ca7 | |||
8c340e2a97 | |||
9abdb1ac43 | |||
42f0b8ee0e | |||
41b5bb571b | |||
2c112d00df | |||
f89136669d | |||
612efcb91c | |||
8d4c482bbd | |||
160c7505f0 | |||
c62ec33130 | |||
55371cb675 | |||
a36e0694ec | |||
1a0d539e76 | |||
79151e7da8 | |||
039d323643 | |||
a5f9e67647 | |||
21866d54cb | |||
eba11f515d | |||
70780d620a | |||
7d3952c373 | |||
0ecb796d54 | |||
2e389b252c | |||
984791f193 | |||
b640902777 | |||
4222e4702f | |||
993e66a084 | |||
6797adac4f | |||
8e58c83526 | |||
2e64caf6ea | |||
9c56fa582b | |||
b2a0a6dd7c | |||
78f589bb18 | |||
ca02509b97 | |||
f3a757d33d | |||
34addd930f | |||
054d319f7c | |||
2addd2aa78 | |||
bba9431733 | |||
6fb5a83183 | |||
ea538ac340 | |||
63216f3b66 | |||
e088374b14 | |||
ded577f40a | |||
32621515db | |||
fdf0ecc9ef | |||
5400e0c873 | |||
1fd6c8657a | |||
d09011d25a | |||
a902d9eb81 | |||
827cdaf9f9 | |||
d2ba2d982c | |||
79118c5283 | |||
8cd8277c22 | |||
ae46823685 | |||
7aa37618e0 | |||
04b6687181 | |||
7a741d7783 | |||
f17ee43805 | |||
b67f0a82ed | |||
815c860dc0 | |||
f0544ff42e | |||
c9c6a99fe9 | |||
aa1e1000fa | |||
a353b4c3f8 | |||
b81fe6d8c1 |
.editorconfig.envPersistence.csproj
.gitea/workflows
.github/workflows
.gitignoreApiDto
ApiDto.csproj
Backend.slnDockerfileCommon
AuthRoles.csCacheType.csCronUpdateSkip.csDatabaseType.csOAuthAction.csOAuthProvider.csPasswordPolicy.csTwoFactorAuthentication.cs
Requests
Responses
AvailableOAuthProvidersResponse.csCampusDetailsResponse.cs
Configuration
ErrorResponse.csFacultyDetailsResponse.csFacultyResponse.csLessonTypeResponse.csSchedule
DisciplineScheduleResponse.csGroupScheduleResponse.csLectureHallScheduleResponse.csProfessorScheduleResponse.cs
ScheduleResponse.csTotpKeyResponse.csUserResponse.csEndpoint
Backend.httpEnvironmentManager.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
General
ISaveSettings.csModel
SwaggerOptions
ActionResultSchemaFilter.csConfigureSwaggerOptions.csDefaultValues.csEnumSchemaFilter.csExampleFilter.csTagSchemeFilter.cs
Validation
Controllers
Endpoint.csprojMiddleware
Program.csSync
wwwroot
css
swagger
Security
Common
CookieNames.cs
DependencyInjection.csDomain
AuthToken.cs
Caching
OAuthPayload.csOAuthProvider.csOAuthProviderUrisData.csOAuthUser.csPreAuthToken.csRequestContextInfo.csDto
Interfaces
Model
OAuth2
ViewModel
Properties
Security.csprojServices
SqlData
Application
Application.csprojDependencyInjection.cs
Common
Exceptions
Cqrs
Campus
Queries
Discipline
Queries
Faculty
Queries
Group
Queries
GetGroupList
LectureHall
Queries
GetLectureHallList
Professor
Queries
GetProfessorDetails
GetProfessorDetailsBySearch
GetProfessorList
Schedule
Queries
TypeOfOccupation
Domain
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
DbInitializer.csDependencyInjection.csEntityTypeConfigurations
Mysql
Schedule
Postgresql
Schedule
Sqlite
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
|
64
.env
64
.env
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
# General
|
# General
|
||||||
|
|
||||||
# The path to save the data.
|
# The path to save the data
|
||||||
# string
|
# string
|
||||||
# (optional)
|
# (optional)
|
||||||
# Saving logs (if the full path is not specified),
|
# Saving logs (if the full path is not specified),
|
||||||
@ -18,6 +18,24 @@
|
|||||||
# 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.
|
# 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=
|
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
|
# Security
|
||||||
|
|
||||||
# JWT signature token
|
# JWT signature token
|
||||||
@ -99,3 +117,47 @@ SECURITY_HASH_TOKEN=
|
|||||||
# integer
|
# integer
|
||||||
# The salt is a random value added to the password before hashing to prevent the use of rainbow hash tables and other attacks.
|
# 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
|
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=
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
name: .NET Test Pipeline
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [master, 'release/*']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up .NET Core
|
|
||||||
uses: actions/setup-dotnet@v3
|
|
||||||
with:
|
|
||||||
dotnet-version: 8.0.x
|
|
||||||
|
|
||||||
- name: Restore dependencies
|
|
||||||
run: dotnet restore
|
|
||||||
|
|
||||||
- name: Build the solution
|
|
||||||
run: dotnet build --configuration Release
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: dotnet test --configuration Release --no-build --no-restore --verbosity normal
|
|
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
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
/ApiDto/ApiDtoDocs.xml
|
||||||
|
/Endpoint/docs.xml
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
<ImplicitUsings>disable</ImplicitUsings>
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Company>Winsomnia</Company>
|
<Company>Winsomnia</Company>
|
||||||
<Version>1.0.0-a0</Version>
|
<Version>1.0.0</Version>
|
||||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
<AssemblyVersion>1.0.3.0</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.0</FileVersion>
|
<FileVersion>1.0.3.0</FileVersion>
|
||||||
<AssemblyName>Mirea.Api.Dto</AssemblyName>
|
<AssemblyName>Mirea.Api.Dto</AssemblyName>
|
||||||
<RootNamespace>$(AssemblyName)</RootNamespace>
|
<RootNamespace>$(AssemblyName)</RootNamespace>
|
||||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||||
@ -20,4 +20,23 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</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>
|
</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
|
||||||
|
}
|
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,
|
||||||
|
}
|
@ -22,4 +22,17 @@ public class LoggingRequest
|
|||||||
/// Gets or sets the log file path.
|
/// Gets or sets the log file path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? LogFilePath { get; set; }
|
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; }
|
||||||
}
|
}
|
@ -1,6 +1,4 @@
|
|||||||
using Mirea.Api.Dto.Common;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Mirea.Api.Dto.Requests.Configuration;
|
namespace Mirea.Api.Dto.Requests.Configuration;
|
||||||
@ -20,10 +18,4 @@ public class ScheduleConfigurationRequest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public DateOnly StartTerm { get; set; }
|
public DateOnly StartTerm { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the pair period times, keyed by pair number.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required IDictionary<int, PairPeriodTime> PairPeriod { get; set; }
|
|
||||||
}
|
}
|
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; }
|
||||||
|
}
|
@ -8,30 +8,30 @@ public class ScheduleRequest
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets an array of group IDs.
|
/// Gets or sets an array of group IDs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This array can contain null values.</remarks>
|
|
||||||
public int[]? Groups { get; set; } = null;
|
public int[]? Groups { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether to retrieve schedules for even weeks.
|
/// Gets or sets a value indicating whether to retrieve schedules for even weeks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This property can contain null.</remarks>
|
|
||||||
public bool? IsEven { get; set; } = null;
|
public bool? IsEven { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets an array of discipline IDs.
|
/// Gets or sets an array of discipline IDs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This array can contain null values.</remarks>
|
|
||||||
public int[]? Disciplines { get; set; } = null;
|
public int[]? Disciplines { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets an array of professor IDs.
|
/// Gets or sets an array of professor IDs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This array can contain null values.</remarks>
|
|
||||||
public int[]? Professors { get; set; } = null;
|
public int[]? Professors { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets an array of lecture hall IDs.
|
/// Gets or sets an array of lecture hall IDs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This array can contain null values.</remarks>
|
|
||||||
public int[]? LectureHalls { get; set; } = null;
|
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; }
|
||||||
|
}
|
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; }
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Dto.Responses;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A class for providing information about an error
|
|
||||||
/// </summary>
|
|
||||||
public class ErrorResponse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The text or translation code of the error. This field may not contain information in specific scenarios.
|
|
||||||
/// For example, it might be empty for HTTP 204 responses where no content is returned or if the validation texts have not been configured.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required string Error { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// In addition to returning the response code in the header, it is also duplicated in this field.
|
|
||||||
/// Represents the HTTP response code.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required int Code { get; set; }
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Dto.Responses;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents detailed information about a faculty.
|
|
||||||
/// </summary>
|
|
||||||
public class FacultyDetailsResponse
|
|
||||||
{
|
|
||||||
/// <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; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the unique identifier of the campus to which the faculty belongs (optional).
|
|
||||||
/// </summary>
|
|
||||||
public int? CampusId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the name of the campus to which the faculty belongs (optional).
|
|
||||||
/// </summary>
|
|
||||||
public string? CampusName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the code name of the campus to which the faculty belongs (optional).
|
|
||||||
/// </summary>
|
|
||||||
public string? CampusCode { get; set; }
|
|
||||||
}
|
|
@ -3,7 +3,7 @@
|
|||||||
namespace Mirea.Api.Dto.Responses;
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents basic information about a faculty.
|
/// Represents information about a faculty.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FacultyResponse
|
public class FacultyResponse
|
||||||
{
|
{
|
||||||
@ -18,9 +18,4 @@ public class FacultyResponse
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the unique identifier of the campus to which the faculty belongs (optional).
|
|
||||||
/// </summary>
|
|
||||||
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; }
|
||||||
|
}
|
@ -1,106 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Dto.Responses.Schedule;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents information about a specific schedule entry for a professor.
|
|
||||||
/// </summary>
|
|
||||||
public class DisciplineScheduleInfo
|
|
||||||
{
|
|
||||||
/// <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 type of occupation for the schedule entry.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required IEnumerable<string> TypeOfOccupation { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the names of the group for the schedule entry.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
|
|
||||||
public required string Group { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the IDs 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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a response containing schedule information for a professor.
|
|
||||||
/// </summary>
|
|
||||||
public class DisciplineScheduleResponse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the name of the discipline.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required string Discipline { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the ID of the discipline.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required int DisciplineId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the schedules for the professor.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required IEnumerable<DisciplineScheduleInfo> Schedules { get; set; }
|
|
||||||
}
|
|
@ -1,129 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Dto.Responses.Schedule;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents information about a specific schedule entry for a group.
|
|
||||||
/// </summary>
|
|
||||||
public class GroupScheduleInfo
|
|
||||||
{
|
|
||||||
/// <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 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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a response containing schedule information for a group.
|
|
||||||
/// </summary>
|
|
||||||
public class GroupScheduleResponse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the name of the group.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required string Group { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the ID of the group.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required int GroupId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the schedules for the group.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required IEnumerable<GroupScheduleInfo> Schedules { get; set; }
|
|
||||||
}
|
|
@ -1,128 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Dto.Responses.Schedule;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents information about a specific schedule entry for a lecture hall.
|
|
||||||
/// </summary>
|
|
||||||
public class LectureHallScheduleInfo
|
|
||||||
{
|
|
||||||
/// <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 names of the group for the schedule entry.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required string Group { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the IDs of the group for the schedule entry.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required int GroupId { 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 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 links to online meetings for the schedule entry.
|
|
||||||
/// </summary>
|
|
||||||
public required IEnumerable<string?> LinkToMeet { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a response containing schedule information for a lecture hall.
|
|
||||||
/// </summary>
|
|
||||||
public class LectureHallScheduleResponse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the names of the lecture halls.
|
|
||||||
/// </summary>
|
|
||||||
public required string LectureHalls { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the IDs of the lecture halls.
|
|
||||||
/// </summary>
|
|
||||||
public required int LectureHallsId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the schedules for the lecture hall.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required IEnumerable<LectureHallScheduleInfo> Schedules { get; set; }
|
|
||||||
}
|
|
@ -1,131 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Dto.Responses.Schedule;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents information about a specific schedule entry for a professor.
|
|
||||||
/// </summary>
|
|
||||||
public class ProfessorScheduleInfo
|
|
||||||
{
|
|
||||||
/// <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 names of the group for the schedule entry.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
|
|
||||||
public required string Group { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the IDs 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 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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a response containing schedule information for a professor.
|
|
||||||
/// </summary>
|
|
||||||
public class ProfessorScheduleResponse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the name of the professor.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required string Professor { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the ID of the professor.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required int ProfessorId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the schedules for the professor.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public required IEnumerable<ProfessorScheduleInfo> Schedules { get; set; }
|
|
||||||
}
|
|
@ -2,7 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Mirea.Api.Dto.Responses.Schedule;
|
namespace Mirea.Api.Dto.Responses;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a response object containing schedule information.
|
/// Represents a response object containing schedule information.
|
||||||
@ -50,8 +50,8 @@ public class ScheduleResponse
|
|||||||
/// If is <see langword="null"/>, then there are no specific <see cref="Weeks"/>
|
/// If is <see langword="null"/>, then there are no specific <see cref="Weeks"/>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
///
|
///
|
||||||
|
|
||||||
public bool? IsExcludedWeeks { get; set; }
|
public bool? IsExcludedWeeks { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The week numbers required for the correct display of the schedule.
|
/// The week numbers required for the correct display of the schedule.
|
||||||
/// <br/>
|
/// <br/>
|
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; }
|
||||||
|
}
|
37
Backend.sln
37
Backend.sln
@ -8,13 +8,15 @@ EndProject
|
|||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Elements of the solution", "Elements of the solution", "{3E087889-A4A0-4A55-A07D-7D149A5BC928}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Elements of the solution", "Elements of the solution", "{3E087889-A4A0-4A55-A07D-7D149A5BC928}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
.dockerignore = .dockerignore
|
.dockerignore = .dockerignore
|
||||||
|
.editorconfig = .editorconfig
|
||||||
.env = .env
|
.env = .env
|
||||||
.gitattributes = .gitattributes
|
.gitattributes = .gitattributes
|
||||||
.gitignore = .gitignore
|
.gitignore = .gitignore
|
||||||
|
.github\workflows\code-analyze.yaml = .github\workflows\code-analyze.yaml
|
||||||
Dockerfile = Dockerfile
|
Dockerfile = Dockerfile
|
||||||
LICENSE.txt = LICENSE.txt
|
LICENSE.txt = LICENSE.txt
|
||||||
README.md = README.md
|
README.md = README.md
|
||||||
.gitea\workflows\test.yaml = .gitea\workflows\test.yaml
|
.github\workflows\release-version.yml = .github\workflows\release-version.yml
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiDto", "ApiDto\ApiDto.csproj", "{0335FA36-E137-453F-853B-916674C168FE}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiDto", "ApiDto\ApiDto.csproj", "{0335FA36-E137-453F-853B-916674C168FE}"
|
||||||
@ -29,6 +31,23 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "SqlData\Appl
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence", "SqlData\Persistence\Persistence.csproj", "{48C9998C-ECE2-407F-835F-1A7255A5C99E}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence", "SqlData\Persistence\Persistence.csproj", "{48C9998C-ECE2-407F-835F-1A7255A5C99E}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -59,6 +78,18 @@ Global
|
|||||||
{48C9998C-ECE2-407F-835F-1A7255A5C99E}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{48C9998C-ECE2-407F-835F-1A7255A5C99E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{48C9998C-ECE2-407F-835F-1A7255A5C99E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EF5530BD-4BF4-4DD8-80BB-04C6B6623DA7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5861915B-9574-4D5D-872F-D54A09651697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5861915B-9574-4D5D-872F-D54A09651697}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5861915B-9574-4D5D-872F-D54A09651697}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5861915B-9574-4D5D-872F-D54A09651697}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E9E238CD-6DD8-4B29-8C36-C61F1168FCCD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -67,6 +98,10 @@ Global
|
|||||||
{3BFD6180-7CA7-4E85-A379-225B872439A1} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
|
{3BFD6180-7CA7-4E85-A379-225B872439A1} = {7E7A63CD-547B-4FB4-A383-EB75298020A1}
|
||||||
{0B1F3656-E5B3-440C-961F-A7D004FBE9A8} = {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}
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E80A1224-87F5-4FEB-82AE-89006BE98B12}
|
SolutionGuid = {E80A1224-87F5-4FEB-82AE-89006BE98B12}
|
||||||
|
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
|
||||||
|
LABEL company="Winsomnia"
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
LABEL maintainer.name="Wesser" maintainer.email="support@winsomnia.net"
|
||||||
USER app
|
|
||||||
WORKDIR /app
|
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
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["Backend.csproj", "."]
|
|
||||||
RUN dotnet restore "./././Backend.csproj"
|
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR "/src/."
|
|
||||||
RUN dotnet build "./Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
|
||||||
|
|
||||||
FROM build AS publish
|
ARG NUGET_USERNAME
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG NUGET_PASSWORD
|
||||||
RUN dotnet publish "./Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
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
|
FROM base AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=publish /app/publish .
|
COPY --from=build /app .
|
||||||
ENTRYPOINT ["dotnet", "Backend.dll"]
|
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
|
|
||||||
|
|
||||||
###
|
|
@ -1,9 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Mirea.Api.Dto.Responses;
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Common.Attributes;
|
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||||
public class BadRequestResponseAttribute() : ProducesResponseTypeAttribute(typeof(ErrorResponse), StatusCodes.Status400BadRequest);
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using System;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Common.Attributes;
|
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||||
@ -9,11 +10,21 @@ public class LocalhostAttribute : ActionFilterAttribute
|
|||||||
public override void OnActionExecuting(ActionExecutingContext context)
|
public override void OnActionExecuting(ActionExecutingContext context)
|
||||||
{
|
{
|
||||||
var ip = context.HttpContext.Connection.RemoteIpAddress;
|
var ip = context.HttpContext.Connection.RemoteIpAddress;
|
||||||
if (ip == null || !IPAddress.IsLoopback(ip))
|
|
||||||
|
if (ip == null)
|
||||||
{
|
{
|
||||||
context.Result = new UnauthorizedResult();
|
context.Result = new UnauthorizedResult();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isRunningInContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER")?.ToLower() == "true";
|
||||||
|
|
||||||
|
if (IPAddress.IsLoopback(ip) || (isRunningInContainer && ip.ToString().StartsWith("172.")))
|
||||||
|
{
|
||||||
base.OnActionExecuting(context);
|
base.OnActionExecuting(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Result = new UnauthorizedResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Common.Attributes;
|
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
|
||||||
public class MaintenanceModeIgnoreAttribute : Attribute;
|
public class MaintenanceModeIgnoreAttribute : Attribute;
|
@ -1,9 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Mirea.Api.Dto.Responses;
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Common.Attributes;
|
namespace Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||||
public class NotFoundResponseAttribute() : ProducesResponseTypeAttribute(typeof(ErrorResponse), StatusCodes.Status404NotFound);
|
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;
|
||||||
|
}
|
@ -9,10 +9,11 @@ namespace Mirea.Api.Endpoint.Common.Attributes;
|
|||||||
[AttributeUsage(AttributeTargets.Method)]
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
public class TokenAuthenticationAttribute : Attribute, IActionFilter
|
public class TokenAuthenticationAttribute : Attribute, IActionFilter
|
||||||
{
|
{
|
||||||
|
public const string AuthToken = "AuthToken";
|
||||||
public void OnActionExecuting(ActionExecutingContext context)
|
public void OnActionExecuting(ActionExecutingContext context)
|
||||||
{
|
{
|
||||||
var setupToken = context.HttpContext.RequestServices.GetRequiredService<ISetupToken>();
|
var setupToken = context.HttpContext.RequestServices.GetRequiredService<ISetupToken>();
|
||||||
if (!context.HttpContext.Request.Cookies.TryGetValue("AuthToken", out string? tokenFromCookie))
|
if (!context.HttpContext.Request.Cookies.TryGetValue(AuthToken, out var tokenFromCookie))
|
||||||
{
|
{
|
||||||
context.Result = new UnauthorizedResult();
|
context.Result = new UnauthorizedResult();
|
||||||
return;
|
return;
|
||||||
|
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;
|
||||||
|
}
|
@ -3,6 +3,5 @@
|
|||||||
public interface IMaintenanceModeNotConfigureService
|
public interface IMaintenanceModeNotConfigureService
|
||||||
{
|
{
|
||||||
bool IsMaintenanceMode { get; }
|
bool IsMaintenanceMode { get; }
|
||||||
|
|
||||||
void DisableMaintenanceMode();
|
void DisableMaintenanceMode();
|
||||||
}
|
}
|
@ -3,8 +3,6 @@
|
|||||||
public interface IMaintenanceModeService
|
public interface IMaintenanceModeService
|
||||||
{
|
{
|
||||||
bool IsMaintenanceMode { get; }
|
bool IsMaintenanceMode { get; }
|
||||||
|
|
||||||
void EnableMaintenanceMode();
|
void EnableMaintenanceMode();
|
||||||
|
|
||||||
void DisableMaintenanceMode();
|
void DisableMaintenanceMode();
|
||||||
}
|
}
|
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();
|
||||||
|
}
|
@ -1,13 +1,14 @@
|
|||||||
using Mirea.Api.Endpoint.Configuration.General.Settings;
|
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Common.Services;
|
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
||||||
|
|
||||||
public static class PairPeriodTimeConverter
|
public static class PairPeriodTimeConverter
|
||||||
{
|
{
|
||||||
public static Dictionary<int, Dto.Common.PairPeriodTime> ConvertToDto(this IDictionary<int, ScheduleSettings.PairPeriodTime> pairPeriod) =>
|
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 });
|
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));
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ namespace Mirea.Api.Endpoint.Common.Services;
|
|||||||
|
|
||||||
public static class PathBuilder
|
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 PathToSave => Environment.GetEnvironmentVariable("PATH_TO_SAVE") ?? Directory.GetCurrentDirectory();
|
||||||
public static string Combine(params string[] paths) => Path.Combine([.. paths.Prepend(PathToSave)]);
|
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();
|
||||||
|
}
|
@ -9,7 +9,8 @@ namespace Mirea.Api.Endpoint.Common.Services.Security;
|
|||||||
|
|
||||||
public class DistributedCacheService(IDistributedCache cache) : ICacheService
|
public class DistributedCacheService(IDistributedCache cache) : ICacheService
|
||||||
{
|
{
|
||||||
public async Task SetAsync<T>(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null, CancellationToken cancellationToken = default)
|
public async Task SetAsync<T>(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var options = new DistributedCacheEntryOptions
|
var options = new DistributedCacheEntryOptions
|
||||||
{
|
{
|
||||||
@ -17,12 +18,43 @@ public class DistributedCacheService(IDistributedCache cache) : ICacheService
|
|||||||
SlidingExpiration = slidingExpiration
|
SlidingExpiration = slidingExpiration
|
||||||
};
|
};
|
||||||
|
|
||||||
var serializedValue = JsonSerializer.SerializeToUtf8Bytes(value);
|
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);
|
await cache.SetAsync(key, serializedValue, options, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
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);
|
var cachedValue = await cache.GetAsync(key, cancellationToken);
|
||||||
return cachedValue == null ? default : JsonSerializer.Deserialize<T>(cachedValue);
|
return cachedValue == null ? default : JsonSerializer.Deserialize<T>(cachedValue);
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ public class JwtTokenService : IAccessToken
|
|||||||
public required string Audience { private get; init; }
|
public required string Audience { private get; init; }
|
||||||
public TimeSpan Lifetime { private get; init; }
|
public TimeSpan Lifetime { private get; init; }
|
||||||
|
|
||||||
public ReadOnlyMemory<byte> EncryptionKey { get; init; }
|
public ReadOnlyMemory<byte> EncryptionKey { private get; init; }
|
||||||
public ReadOnlyMemory<byte> SigningKey { private get; init; }
|
public ReadOnlyMemory<byte> SigningKey { private get; init; }
|
||||||
|
|
||||||
public (string Token, DateTime ExpireIn) GenerateToken(string userId)
|
public (string Token, DateTime ExpireIn) GenerateToken(string userId)
|
||||||
@ -33,7 +33,7 @@ public class JwtTokenService : IAccessToken
|
|||||||
SigningCredentials = signingCredentials,
|
SigningCredentials = signingCredentials,
|
||||||
Subject = new ClaimsIdentity(
|
Subject = new ClaimsIdentity(
|
||||||
[
|
[
|
||||||
new Claim(ClaimTypes.Name, userId),
|
new Claim(ClaimTypes.NameIdentifier, userId),
|
||||||
// todo: get role by userId
|
// todo: get role by userId
|
||||||
new Claim(ClaimTypes.Role, "")
|
new Claim(ClaimTypes.Role, "")
|
||||||
]),
|
]),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Mirea.Api.Security.Common.Interfaces;
|
using Mirea.Api.Security.Common.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -8,7 +9,8 @@ namespace Mirea.Api.Endpoint.Common.Services.Security;
|
|||||||
|
|
||||||
public class MemoryCacheService(IMemoryCache cache) : ICacheService
|
public class MemoryCacheService(IMemoryCache cache) : ICacheService
|
||||||
{
|
{
|
||||||
public Task SetAsync<T>(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null, CancellationToken cancellationToken = default)
|
public Task SetAsync<T>(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var options = new MemoryCacheEntryOptions
|
var options = new MemoryCacheEntryOptions
|
||||||
{
|
{
|
||||||
@ -16,14 +18,41 @@ public class MemoryCacheService(IMemoryCache cache) : ICacheService
|
|||||||
SlidingExpiration = slidingExpiration
|
SlidingExpiration = slidingExpiration
|
||||||
};
|
};
|
||||||
|
|
||||||
cache.Set(key, value, options);
|
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;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
public Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
cache.TryGetValue(key, out T? value);
|
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
return Task.FromResult(value);
|
|
||||||
|
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)
|
public Task RemoveAsync(string key, CancellationToken cancellationToken = default)
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Mirea.Api.Endpoint.Common.Attributes;
|
using Mirea.Api.Endpoint.Common.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Common.Exceptions;
|
||||||
using Mirea.Api.Endpoint.Common.Interfaces;
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Middleware;
|
namespace Mirea.Api.Endpoint.Configuration.Core.Middleware;
|
||||||
|
|
||||||
public class MaintenanceModeMiddleware(RequestDelegate next, IMaintenanceModeService maintenanceModeService, IMaintenanceModeNotConfigureService maintenanceModeNotConfigureService)
|
public class MaintenanceModeMiddleware(RequestDelegate next, IMaintenanceModeService maintenanceModeService, IMaintenanceModeNotConfigureService maintenanceModeNotConfigureService)
|
||||||
{
|
{
|
||||||
@ -13,7 +14,7 @@ public class MaintenanceModeMiddleware(RequestDelegate next, IMaintenanceModeSer
|
|||||||
return endpoint?.Metadata.GetMetadata<MaintenanceModeIgnoreAttribute>() != null;
|
return endpoint?.Metadata.GetMetadata<MaintenanceModeIgnoreAttribute>() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Invoke(HttpContext context)
|
public async Task InvokeAsync(HttpContext context)
|
||||||
{
|
{
|
||||||
if (!maintenanceModeService.IsMaintenanceMode && !maintenanceModeNotConfigureService.IsMaintenanceMode || IsIgnoreMaintenanceMode(context))
|
if (!maintenanceModeService.IsMaintenanceMode && !maintenanceModeNotConfigureService.IsMaintenanceMode || IsIgnoreMaintenanceMode(context))
|
||||||
await next(context);
|
await next(context);
|
||||||
@ -23,17 +24,11 @@ public class MaintenanceModeMiddleware(RequestDelegate next, IMaintenanceModeSer
|
|||||||
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||||
context.Response.ContentType = "plain/text";
|
context.Response.ContentType = "plain/text";
|
||||||
|
|
||||||
string error;
|
|
||||||
if (maintenanceModeService.IsMaintenanceMode)
|
if (maintenanceModeService.IsMaintenanceMode)
|
||||||
{
|
throw new ServerUnavailableException("The service is currently undergoing maintenance. Please try again later.", true);
|
||||||
context.Response.Headers.RetryAfter = "600";
|
|
||||||
error = "The service is currently undergoing maintenance. Please try again later.";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
error =
|
|
||||||
"The service is currently not configured. Go to the setup page if you are an administrator or try again later.";
|
|
||||||
|
|
||||||
await context.Response.WriteAsync(error);
|
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('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,36 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration;
|
|
||||||
|
|
||||||
internal static class EnvironmentManager
|
|
||||||
{
|
|
||||||
public static void LoadEnvironment(string envFile)
|
|
||||||
{
|
|
||||||
if (!File.Exists(envFile)) return;
|
|
||||||
|
|
||||||
foreach (var line in File.ReadAllLines(envFile))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(line)) continue;
|
|
||||||
|
|
||||||
var commentIndex = line.IndexOf('#', StringComparison.Ordinal);
|
|
||||||
|
|
||||||
string arg = line;
|
|
||||||
|
|
||||||
if (commentIndex != -1)
|
|
||||||
arg = arg.Remove(commentIndex, arg.Length - commentIndex);
|
|
||||||
|
|
||||||
var parts = arg.Split(
|
|
||||||
'=',
|
|
||||||
StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
if (parts.Length > 2)
|
|
||||||
parts = [parts[0], string.Join("=", parts[1..])];
|
|
||||||
|
|
||||||
if (parts.Length != 2)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Environment.SetEnvironmentVariable(parts[0].Trim(), parts[1].Trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
using Mirea.Api.Endpoint.Configuration.General.Settings;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration.General;
|
|
||||||
|
|
||||||
public class GeneralConfig
|
|
||||||
{
|
|
||||||
public const string FilePath = "Settings.json";
|
|
||||||
|
|
||||||
public DbSettings? DbSettings { get; set; }
|
|
||||||
public CacheSettings? CacheSettings { get; set; }
|
|
||||||
public ScheduleSettings? ScheduleSettings { get; set; }
|
|
||||||
public EmailSettings? EmailSettings { get; set; }
|
|
||||||
public LogSettings? LogSettings { get; set; }
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
namespace Mirea.Api.Endpoint.Configuration.General.Interfaces;
|
|
||||||
|
|
||||||
public interface IIsConfigured
|
|
||||||
{
|
|
||||||
bool IsConfigured();
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
using Mirea.Api.Endpoint.Configuration.General.Attributes;
|
|
||||||
using Mirea.Api.Endpoint.Configuration.General.Interfaces;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration.General.Settings;
|
|
||||||
|
|
||||||
[RequiredSettings]
|
|
||||||
public class CacheSettings : IIsConfigured
|
|
||||||
{
|
|
||||||
public enum CacheEnum
|
|
||||||
{
|
|
||||||
Memcached,
|
|
||||||
Redis
|
|
||||||
}
|
|
||||||
|
|
||||||
public CacheEnum TypeDatabase { get; set; }
|
|
||||||
public string? ConnectionString { get; set; }
|
|
||||||
|
|
||||||
public bool IsConfigured()
|
|
||||||
{
|
|
||||||
return TypeDatabase == CacheEnum.Memcached ||
|
|
||||||
!string.IsNullOrEmpty(ConnectionString);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Mirea.Api.DataAccess.Persistence.Common;
|
|
||||||
using Mirea.Api.Endpoint.Configuration.General.Attributes;
|
|
||||||
using Mirea.Api.Endpoint.Configuration.General.Interfaces;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration.General.Settings;
|
|
||||||
|
|
||||||
[RequiredSettings]
|
|
||||||
public class DbSettings : IIsConfigured
|
|
||||||
{
|
|
||||||
public enum DatabaseEnum
|
|
||||||
{
|
|
||||||
Mysql,
|
|
||||||
Sqlite,
|
|
||||||
PostgresSql
|
|
||||||
}
|
|
||||||
public DatabaseEnum TypeDatabase { get; set; }
|
|
||||||
public required string ConnectionStringSql { get; set; }
|
|
||||||
|
|
||||||
public DatabaseProvider DatabaseProvider =>
|
|
||||||
TypeDatabase switch
|
|
||||||
{
|
|
||||||
DatabaseEnum.PostgresSql => DatabaseProvider.Postgresql,
|
|
||||||
DatabaseEnum.Mysql => DatabaseProvider.Mysql,
|
|
||||||
DatabaseEnum.Sqlite => DatabaseProvider.Sqlite,
|
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
|
||||||
};
|
|
||||||
|
|
||||||
public bool IsConfigured() =>
|
|
||||||
!string.IsNullOrEmpty(ConnectionStringSql);
|
|
||||||
}
|
|
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);
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
using Mirea.Api.Endpoint.Configuration.General.Interfaces;
|
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration.General.Settings;
|
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
|
||||||
public class EmailSettings : IIsConfigured
|
public class EmailSettings : IIsConfigured
|
||||||
{
|
{
|
@ -1,7 +1,7 @@
|
|||||||
using Mirea.Api.Endpoint.Configuration.General.Attributes;
|
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||||
using Mirea.Api.Endpoint.Configuration.General.Interfaces;
|
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration.General.Settings;
|
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
|
||||||
[RequiredSettings]
|
[RequiredSettings]
|
||||||
public class LogSettings : IIsConfigured
|
public class LogSettings : IIsConfigured
|
||||||
@ -9,6 +9,8 @@ public class LogSettings : IIsConfigured
|
|||||||
public bool EnableLogToFile { get; set; }
|
public bool EnableLogToFile { get; set; }
|
||||||
public string? LogFilePath { get; set; }
|
public string? LogFilePath { get; set; }
|
||||||
public string? LogFileName { get; set; }
|
public string? LogFileName { get; set; }
|
||||||
|
public string? ApiKeySeq { get; set; }
|
||||||
|
public string? ApiServerSeq { get; set; }
|
||||||
|
|
||||||
public bool IsConfigured()
|
public bool IsConfigured()
|
||||||
{
|
{
|
@ -1,10 +1,10 @@
|
|||||||
using Mirea.Api.Endpoint.Configuration.General.Attributes;
|
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||||
using Mirea.Api.Endpoint.Configuration.General.Interfaces;
|
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration.General.Settings;
|
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
|
||||||
|
|
||||||
[RequiredSettings]
|
[RequiredSettings]
|
||||||
public class ScheduleSettings : IIsConfigured
|
public class ScheduleSettings : IIsConfigured
|
||||||
@ -31,9 +31,33 @@ public class ScheduleSettings : IIsConfigured
|
|||||||
public PairPeriodTime(Dto.Common.PairPeriodTime time) : this(time.Start, time.End) { }
|
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 required string CronUpdateSchedule { get; set; }
|
||||||
public DateOnly StartTerm { get; set; }
|
public DateOnly StartTerm { get; set; }
|
||||||
public required IDictionary<int, PairPeriodTime> PairPeriod { get; set; }
|
public required IDictionary<int, PairPeriodTime> PairPeriod { get; set; }
|
||||||
|
public List<CronUpdateSkip> CronUpdateSkipDateList { get; set; } = [];
|
||||||
|
|
||||||
public bool IsConfigured()
|
public bool IsConfigured()
|
||||||
{
|
{
|
@ -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 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +1,30 @@
|
|||||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
using Asp.Versioning.ApiExplorer;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration.Swagger;
|
namespace Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||||
|
|
||||||
public class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions<SwaggerGenOptions>
|
public class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions<SwaggerGenOptions>
|
||||||
{
|
{
|
||||||
public void Configure(SwaggerGenOptions options)
|
public void Configure(SwaggerGenOptions options)
|
||||||
{
|
{
|
||||||
foreach (var description in provider.ApiVersionDescriptions)
|
foreach (var description in provider.ApiVersionDescriptions)
|
||||||
{
|
|
||||||
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
|
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
|
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
|
||||||
{
|
{
|
||||||
var info = new OpenApiInfo()
|
var info = new OpenApiInfo()
|
||||||
{
|
{
|
||||||
Title = "MIREA Schedule Web API",
|
Title = $"MIREA Schedule Web API ({FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion})",
|
||||||
Version = description.ApiVersion.ToString(),
|
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.",
|
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" },
|
Contact = new OpenApiContact { Name = "Author name", Email = "support@winsomnia.net" },
|
||||||
License = new OpenApiLicense { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") }
|
License = new OpenApiLicense { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") }
|
||||||
};
|
};
|
@ -6,9 +6,9 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration.Swagger;
|
namespace Mirea.Api.Endpoint.Configuration.SwaggerOptions;
|
||||||
|
|
||||||
public class SwaggerDefaultValues : IOperationFilter
|
public class DefaultValues : IOperationFilter
|
||||||
{
|
{
|
||||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||||
{
|
{
|
||||||
@ -23,16 +23,12 @@ public class SwaggerDefaultValues : IOperationFilter
|
|||||||
foreach (var contentType in response.Content.Keys)
|
foreach (var contentType in response.Content.Keys)
|
||||||
{
|
{
|
||||||
if (responseType.ApiResponseFormats.All(x => x.MediaType != contentType))
|
if (responseType.ApiResponseFormats.All(x => x.MediaType != contentType))
|
||||||
{
|
|
||||||
response.Content.Remove(contentType);
|
response.Content.Remove(contentType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (operation.Parameters == null)
|
if (operation.Parameters == null)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var parameter in operation.Parameters)
|
foreach (var parameter in operation.Parameters)
|
||||||
{
|
{
|
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] });
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration.General.Attributes;
|
namespace Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
public class RequiredSettingsAttribute : Attribute;
|
public class RequiredSettingsAttribute : Attribute;
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
|
|
||||||
|
public interface IIsConfigured
|
||||||
|
{
|
||||||
|
bool IsConfigured();
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
using Mirea.Api.Endpoint.Common.Interfaces;
|
using Mirea.Api.Endpoint.Common.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration.General;
|
namespace Mirea.Api.Endpoint.Configuration.Validation;
|
||||||
|
|
||||||
public class SetupTokenService : ISetupToken
|
public class SetupTokenService : ISetupToken
|
||||||
{
|
{
|
||||||
@ -14,8 +14,8 @@ public class SetupTokenService : ISetupToken
|
|||||||
|
|
||||||
var token2 = Token.Value.Span;
|
var token2 = Token.Value.Span;
|
||||||
|
|
||||||
int result = 0;
|
var result = 0;
|
||||||
for (int i = 0; i < Token.Value.Length; i++)
|
for (var i = 0; i < Token.Value.Length; i++)
|
||||||
result |= token2[i] ^ token[i];
|
result |= token2[i] ^ token[i];
|
||||||
|
|
||||||
return result == 0;
|
return result == 0;
|
@ -1,10 +1,11 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Mirea.Api.Endpoint.Configuration.General.Attributes;
|
using Mirea.Api.Endpoint.Configuration.Model;
|
||||||
using Mirea.Api.Endpoint.Configuration.General.Interfaces;
|
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
|
||||||
|
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Configuration.General.Validators;
|
namespace Mirea.Api.Endpoint.Configuration.Validation.Validators;
|
||||||
|
|
||||||
public class SettingsRequiredValidator
|
public class SettingsRequiredValidator
|
||||||
{
|
{
|
@ -1,270 +0,0 @@
|
|||||||
using Cronos;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Mirea.Api.Dto.Requests;
|
|
||||||
using Mirea.Api.Dto.Requests.Configuration;
|
|
||||||
using Mirea.Api.Endpoint.Common.Attributes;
|
|
||||||
using Mirea.Api.Endpoint.Common.Exceptions;
|
|
||||||
using Mirea.Api.Endpoint.Common.Interfaces;
|
|
||||||
using Mirea.Api.Endpoint.Common.Services;
|
|
||||||
using Mirea.Api.Endpoint.Configuration.General;
|
|
||||||
using Mirea.Api.Endpoint.Configuration.General.Settings;
|
|
||||||
using MySqlConnector;
|
|
||||||
using Npgsql;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
using System;
|
|
||||||
using System.Data;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Mirea.Api.Endpoint.Controllers.Configuration;
|
|
||||||
|
|
||||||
[ApiVersion("1.0")]
|
|
||||||
[ApiController]
|
|
||||||
[MaintenanceModeIgnore]
|
|
||||||
public class SetupController(ISetupToken setupToken, IMaintenanceModeNotConfigureService notConfigureService, IMemoryCache cache) : BaseController
|
|
||||||
{
|
|
||||||
private const string CacheGeneralKey = "config_part";
|
|
||||||
private GeneralConfig GeneralConfig
|
|
||||||
{
|
|
||||||
get => cache.Get<GeneralConfig>(CacheGeneralKey) ?? new GeneralConfig();
|
|
||||||
set => cache.Set(CacheGeneralKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("GenerateToken")]
|
|
||||||
[Localhost]
|
|
||||||
public ActionResult<string> GenerateToken()
|
|
||||||
{
|
|
||||||
if (!notConfigureService.IsMaintenanceMode)
|
|
||||||
throw new ControllerArgumentException(
|
|
||||||
"The token cannot be generated because the server has been configured. " +
|
|
||||||
$"If you need to restart the configuration, then delete the \"{PathBuilder.Combine(GeneralConfig.FilePath)}\" file and restart the application.");
|
|
||||||
|
|
||||||
var token = new byte[32];
|
|
||||||
RandomNumberGenerator.Create().GetBytes(token);
|
|
||||||
setupToken.SetToken(token);
|
|
||||||
|
|
||||||
return Ok(Convert.ToBase64String(token));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("CheckToken")]
|
|
||||||
public ActionResult<bool> CheckToken([FromQuery] string token)
|
|
||||||
{
|
|
||||||
if (!setupToken.MatchToken(Convert.FromBase64String(token))) return Unauthorized("The token is not valid");
|
|
||||||
|
|
||||||
Response.Cookies.Append("AuthToken", token, new CookieOptions
|
|
||||||
{
|
|
||||||
HttpOnly = false,
|
|
||||||
Secure = false,
|
|
||||||
Path = "/"
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ActionResult<bool> SetDatabase<TConnection, TException>(string connectionString, DbSettings.DatabaseEnum databaseType)
|
|
||||||
where TConnection : class, IDbConnection, new()
|
|
||||||
where TException : Exception
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var connection = new TConnection();
|
|
||||||
connection.ConnectionString = connectionString;
|
|
||||||
connection.Open();
|
|
||||||
connection.Close();
|
|
||||||
|
|
||||||
var general = GeneralConfig;
|
|
||||||
general.DbSettings = new DbSettings
|
|
||||||
{
|
|
||||||
ConnectionStringSql = connectionString,
|
|
||||||
TypeDatabase = databaseType
|
|
||||||
};
|
|
||||||
GeneralConfig = general;
|
|
||||||
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
catch (TException ex)
|
|
||||||
{
|
|
||||||
throw new ControllerArgumentException($"Error when connecting: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("SetPsql")]
|
|
||||||
[TokenAuthentication]
|
|
||||||
[BadRequestResponse]
|
|
||||||
public ActionResult<bool> SetPsql([FromBody] DatabaseRequest request)
|
|
||||||
{
|
|
||||||
string connectionString = $"Host={request.Server}:{request.Port};Username={request.User};Database={request.Database}";
|
|
||||||
if (request.Password != null)
|
|
||||||
connectionString += $";Password={request.Password}";
|
|
||||||
if (request.Ssl)
|
|
||||||
connectionString += ";SSL Mode=Require;";
|
|
||||||
|
|
||||||
return SetDatabase<NpgsqlConnection, NpgsqlException>(connectionString, DbSettings.DatabaseEnum.PostgresSql);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("SetMysql")]
|
|
||||||
[TokenAuthentication]
|
|
||||||
[BadRequestResponse]
|
|
||||||
public ActionResult<bool> SetMysql([FromBody] DatabaseRequest request)
|
|
||||||
{
|
|
||||||
string connectionString = $"Server={request.Server}:{request.Port};Uid={request.User};Database={request.Database}";
|
|
||||||
if (request.Password != null)
|
|
||||||
connectionString += $";Pwd={request.Password}";
|
|
||||||
if (request.Ssl)
|
|
||||||
connectionString += ";SslMode=Require;";
|
|
||||||
|
|
||||||
return SetDatabase<MySqlConnection, MySqlException>(connectionString, DbSettings.DatabaseEnum.Mysql);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("SetSqlite")]
|
|
||||||
[TokenAuthentication]
|
|
||||||
public ActionResult<bool> SetSqlite([FromQuery] string? path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path)) path = "database";
|
|
||||||
|
|
||||||
path = PathBuilder.Combine(path);
|
|
||||||
|
|
||||||
if (!Directory.Exists(path))
|
|
||||||
{
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
Directory.CreateDirectory(path);
|
|
||||||
else
|
|
||||||
Directory.CreateDirectory(path, UnixFileMode.UserRead | UnixFileMode.UserWrite);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
throw new ControllerArgumentException("Such a folder exists. Enter a different name");
|
|
||||||
|
|
||||||
string connectionString = $"Data Source={PathBuilder.Combine(path, "database.db3")}";
|
|
||||||
|
|
||||||
return SetDatabase<SqliteConnection, SqliteException>(connectionString, DbSettings.DatabaseEnum.Sqlite);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("SetRedis")]
|
|
||||||
[TokenAuthentication]
|
|
||||||
[BadRequestResponse]
|
|
||||||
public ActionResult<bool> SetRedis([FromBody] CacheRequest request)
|
|
||||||
{
|
|
||||||
string connectionString = $"{request.Server}:{request.Port},ssl=false";
|
|
||||||
if (request.Password != null)
|
|
||||||
connectionString += $",password={request.Password}";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var redis = ConnectionMultiplexer.Connect(connectionString);
|
|
||||||
redis.Close();
|
|
||||||
|
|
||||||
var general = GeneralConfig;
|
|
||||||
general.CacheSettings = new CacheSettings
|
|
||||||
{
|
|
||||||
ConnectionString = connectionString,
|
|
||||||
TypeDatabase = CacheSettings.CacheEnum.Redis
|
|
||||||
};
|
|
||||||
GeneralConfig = general;
|
|
||||||
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new ControllerArgumentException("Error when connecting to Redis: " + ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("SetMemcached")]
|
|
||||||
[TokenAuthentication]
|
|
||||||
[BadRequestResponse]
|
|
||||||
public ActionResult<bool> SetMemcached()
|
|
||||||
{
|
|
||||||
var general = GeneralConfig;
|
|
||||||
general.CacheSettings = new CacheSettings
|
|
||||||
{
|
|
||||||
ConnectionString = null,
|
|
||||||
TypeDatabase = CacheSettings.CacheEnum.Memcached
|
|
||||||
};
|
|
||||||
GeneralConfig = general;
|
|
||||||
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("SetLogging")]
|
|
||||||
[TokenAuthentication]
|
|
||||||
[BadRequestResponse]
|
|
||||||
public ActionResult<bool> SetLogging([FromBody] LoggingRequest? request)
|
|
||||||
{
|
|
||||||
var settings = (request == null) switch
|
|
||||||
{
|
|
||||||
true => new LogSettings
|
|
||||||
{
|
|
||||||
EnableLogToFile = true,
|
|
||||||
LogFileName = "logging-",
|
|
||||||
LogFilePath = "logs"
|
|
||||||
},
|
|
||||||
false => new LogSettings
|
|
||||||
{
|
|
||||||
EnableLogToFile = request.EnableLogToFile,
|
|
||||||
LogFileName = request.LogFileName,
|
|
||||||
LogFilePath = request.LogFilePath
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var general = GeneralConfig;
|
|
||||||
general.LogSettings = settings;
|
|
||||||
GeneralConfig = general;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("SetEmail")]
|
|
||||||
[TokenAuthentication]
|
|
||||||
[BadRequestResponse]
|
|
||||||
public ActionResult<bool> SetEmail([FromBody] EmailRequest? request)
|
|
||||||
{
|
|
||||||
var settings = (request == null) switch
|
|
||||||
{
|
|
||||||
true => new EmailSettings(),
|
|
||||||
false => new EmailSettings
|
|
||||||
{
|
|
||||||
Server = request.Server,
|
|
||||||
From = request.From,
|
|
||||||
Password = request.Password,
|
|
||||||
Port = request.Port,
|
|
||||||
Ssl = request.Ssl,
|
|
||||||
User = request.User
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var general = GeneralConfig;
|
|
||||||
general.EmailSettings = settings;
|
|
||||||
GeneralConfig = general;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("SetSchedule")]
|
|
||||||
[TokenAuthentication]
|
|
||||||
[BadRequestResponse]
|
|
||||||
public ActionResult<bool> SetSchedule([FromBody] ScheduleConfigurationRequest request)
|
|
||||||
{
|
|
||||||
var general = GeneralConfig;
|
|
||||||
general.ScheduleSettings = new ScheduleSettings
|
|
||||||
{
|
|
||||||
// every 6 hours
|
|
||||||
CronUpdateSchedule = request.CronUpdateSchedule ?? "0 */6 * * *",
|
|
||||||
StartTerm = request.StartTerm,
|
|
||||||
PairPeriod = request.PairPeriod.ConvertFromDto()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!CronExpression.TryParse(general.ScheduleSettings.CronUpdateSchedule, CronFormat.Standard, out _))
|
|
||||||
throw new ControllerArgumentException("The Cron task could not be parsed. Check the format of the entered data.");
|
|
||||||
|
|
||||||
GeneralConfig = general;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
11
Endpoint/Controllers/ConfigurationBaseController.cs
Normal file
11
Endpoint/Controllers/ConfigurationBaseController.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Mirea.Api.Endpoint.Controllers;
|
||||||
|
|
||||||
|
[Route("api/v{version:apiVersion}/Configuration/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[Tags("Configuration")]
|
||||||
|
public class ConfigurationBaseController : BaseController;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user