├── .vscode └── settings.json ├── LICENSE ├── README.md ├── backend ├── dotnet │ ├── .dockerignore │ ├── .editorconfig │ ├── .gitignore │ ├── Directory.Build.props │ ├── Twitter.sln │ ├── framework │ │ ├── Core │ │ │ ├── Caching │ │ │ │ └── ICacheService.cs │ │ │ ├── Database │ │ │ │ └── IRepository.cs │ │ │ ├── Domain │ │ │ │ ├── BaseEntity.cs │ │ │ │ └── IBaseEntity.cs │ │ │ ├── Events │ │ │ │ ├── DomainEvent.cs │ │ │ │ ├── IDomainEvent.cs │ │ │ │ ├── IEvent.cs │ │ │ │ ├── IEventPublisher.cs │ │ │ │ ├── IIntegrationEvent.cs │ │ │ │ └── IntegrationEvent.cs │ │ │ ├── Exceptions │ │ │ │ ├── ConfigurationMissingException.cs │ │ │ │ ├── CustomException.cs │ │ │ │ ├── ForbiddenException.cs │ │ │ │ ├── NotFoundException.cs │ │ │ │ └── UnauthorizedException.cs │ │ │ ├── FSH.Framework.Core.csproj │ │ │ ├── GlobalUsings.cs │ │ │ ├── Identity │ │ │ │ └── Constants.cs │ │ │ ├── Pagination │ │ │ │ ├── PagedList.cs │ │ │ │ └── PaginationParameters.cs │ │ │ ├── Serializers │ │ │ │ └── ISerializerService.cs │ │ │ ├── Services │ │ │ │ ├── IDateTimeService.cs │ │ │ │ ├── IScopedService.cs │ │ │ │ └── ITransientService.cs │ │ │ └── Validation │ │ │ │ └── CustomValidator.cs │ │ ├── Infrastructure │ │ │ ├── Auth │ │ │ │ ├── OpenId │ │ │ │ │ ├── Extensions.cs │ │ │ │ │ ├── HasScopeHandler.cs │ │ │ │ │ ├── HasScopeRequirement.cs │ │ │ │ │ └── OpenIdOptions.cs │ │ │ │ └── OpenIddict │ │ │ │ │ ├── Extensions.cs │ │ │ │ │ └── OpenIddictOptions.cs │ │ │ ├── Behaviors │ │ │ │ ├── Extensions.cs │ │ │ │ └── ValidationBehavior.cs │ │ │ ├── Caching │ │ │ │ ├── CachingOptions.cs │ │ │ │ ├── DistributedCacheService.cs │ │ │ │ ├── Extensions.cs │ │ │ │ └── InMemoryCacheService.cs │ │ │ ├── Controllers │ │ │ │ └── BaseApiController.cs │ │ │ ├── Extensions.cs │ │ │ ├── FSH.Framework.Infrastructure.csproj │ │ │ ├── Logging │ │ │ │ └── Serilog │ │ │ │ │ ├── Extensions.cs │ │ │ │ │ └── SerilogOptions.cs │ │ │ ├── Mapping │ │ │ │ └── Mapster │ │ │ │ │ └── Extensions.cs │ │ │ ├── Messaging │ │ │ │ ├── EventPublisher.cs │ │ │ │ └── Extensions.cs │ │ │ ├── Middlewares │ │ │ │ ├── ExceptionDetails.cs │ │ │ │ ├── ExceptionMiddleware.cs │ │ │ │ └── Extensions.cs │ │ │ ├── Options │ │ │ │ ├── AppOptions.cs │ │ │ │ ├── Extensions.cs │ │ │ │ └── IOptionsRoot.cs │ │ │ ├── Serializers │ │ │ │ └── NewtonSoftService.cs │ │ │ ├── Services │ │ │ │ ├── DateTimeService.cs │ │ │ │ └── Extensions.cs │ │ │ └── Swagger │ │ │ │ ├── Extensions.cs │ │ │ │ └── SwaggerOptions.cs │ │ ├── Persistence.EntityFrameworkCore │ │ │ └── FSH.Framework.Persistence.EntityFrameworkCore.csproj │ │ └── Persistence.NoSQL │ │ │ ├── FSH.Framework.Persistence.Mongo.csproj │ │ │ ├── IMongoDbContext.cs │ │ │ ├── MongoDbContext.cs │ │ │ ├── MongoOptions.cs │ │ │ ├── MongoRepository.cs │ │ │ ├── QueryableExtensions.cs │ │ │ └── ServiceCollectionExtensions.cs │ └── gateway │ │ ├── Dockerfile │ │ ├── Gateway.csproj │ │ ├── Program.cs │ │ ├── Properties │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.docker.json │ │ └── appsettings.json └── go │ ├── .gitignore │ └── services │ └── storage │ ├── .env │ ├── Dockerfile │ ├── README.md │ ├── cmd │ └── app │ │ └── main.go │ ├── controllers │ ├── controllers.go │ └── mediacontroller.go │ ├── go.mod │ ├── go.sum │ ├── handlers │ ├── handler.go │ └── upload.go │ ├── pkg │ └── aws │ │ ├── aws.go │ │ ├── constants.go │ │ ├── ddb.go │ │ └── s3.go │ └── types │ ├── category.go │ ├── metadata.go │ └── uploadrequest.go ├── docker-compose.yaml ├── frontend └── nextjs │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── next.svg │ └── vercel.svg │ ├── tailwind.config.js │ └── tsconfig.json └── misc └── twitter.postman_collection.json /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "backend\\dotnet\\Twitter.sln" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 fluentstack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitter Clone 2 | A Twitter Clone built with .NET 8, GoLang, and Next.js 13 🔥 3 | 4 | ![twitter clone](https://github.com/fluentstack/twitter-clone/assets/31455818/eb39cbec-e080-4e54-a04c-2f5ca41a0bda) 5 | 6 | ## System Design 7 | The Clone should meet the following requirments. 8 | 9 | ### Functional Requirements 10 | - Authentication using email/password & OAuth Providers. 11 | - Ability to Login, Register and Logout. 12 | - Should be able to post new tweets (upto 250 characters) 13 | - Should be able to follow other users. 14 | - Should be able to like and comment on other tweets 15 | - Should have a newsfeed containing tweets from the following users. 16 | 17 | ### Non-Functional Requirments 18 | - Super Clean UI. 19 | - Do Not Have the Same UI Components as Twitter. 20 | - Scalable Backend. 21 | - Super Fast API Responses. 22 | 23 | ### Nice to Have Features 24 | - Metrics and Dashboards. 25 | - Retweet functionality. 26 | -------------------------------------------------------------------------------- /backend/dotnet/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /backend/dotnet/.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | # C# files 5 | [*.cs] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Indentation and spacing 10 | indent_size = 4 11 | indent_style = space 12 | tab_width = 4 13 | 14 | # New line preferences 15 | end_of_line = crlf 16 | insert_final_newline = false 17 | 18 | #### .NET Coding Conventions #### 19 | 20 | # Organize usings 21 | dotnet_separate_import_directive_groups = false 22 | dotnet_sort_system_directives_first = false 23 | file_header_template = unset 24 | 25 | # this. and Me. preferences 26 | dotnet_style_qualification_for_event = false 27 | dotnet_style_qualification_for_field = false 28 | dotnet_style_qualification_for_method = false 29 | dotnet_style_qualification_for_property = false 30 | 31 | # Language keywords vs BCL types preferences 32 | dotnet_style_predefined_type_for_locals_parameters_members = true 33 | dotnet_style_predefined_type_for_member_access = true 34 | 35 | # Parentheses preferences 36 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity 37 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity 38 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary 39 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity 40 | 41 | # Modifier preferences 42 | dotnet_style_require_accessibility_modifiers = for_non_interface_members 43 | 44 | # Expression-level preferences 45 | dotnet_style_coalesce_expression = true 46 | dotnet_style_collection_initializer = true 47 | dotnet_style_explicit_tuple_names = true 48 | dotnet_style_namespace_match_folder = true 49 | dotnet_style_null_propagation = true 50 | dotnet_style_object_initializer = true 51 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 52 | dotnet_style_prefer_auto_properties = true 53 | dotnet_style_prefer_compound_assignment = true 54 | dotnet_style_prefer_conditional_expression_over_assignment = true 55 | dotnet_style_prefer_conditional_expression_over_return = true 56 | dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed 57 | dotnet_style_prefer_inferred_anonymous_type_member_names = true 58 | dotnet_style_prefer_inferred_tuple_names = true 59 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true 60 | dotnet_style_prefer_simplified_boolean_expressions = true 61 | dotnet_style_prefer_simplified_interpolation = true 62 | 63 | # Field preferences 64 | dotnet_style_readonly_field = true 65 | 66 | # Parameter preferences 67 | dotnet_code_quality_unused_parameters = all 68 | 69 | # Suppression preferences 70 | dotnet_remove_unnecessary_suppression_exclusions = none 71 | 72 | # New line preferences 73 | dotnet_style_allow_multiple_blank_lines_experimental = true 74 | dotnet_style_allow_statement_immediately_after_block_experimental = true 75 | 76 | #### C# Coding Conventions #### 77 | 78 | # var preferences 79 | csharp_style_var_elsewhere = false 80 | csharp_style_var_for_built_in_types = false 81 | csharp_style_var_when_type_is_apparent = false 82 | 83 | # Expression-bodied members 84 | csharp_style_expression_bodied_accessors = true:silent 85 | csharp_style_expression_bodied_constructors = false:silent 86 | csharp_style_expression_bodied_indexers = true:silent 87 | csharp_style_expression_bodied_lambdas = true:silent 88 | csharp_style_expression_bodied_local_functions = false:silent 89 | csharp_style_expression_bodied_methods = false:silent 90 | csharp_style_expression_bodied_operators = false:silent 91 | csharp_style_expression_bodied_properties = true:silent 92 | 93 | # Pattern matching preferences 94 | csharp_style_pattern_matching_over_as_with_null_check = true 95 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 96 | csharp_style_prefer_extended_property_pattern = true 97 | csharp_style_prefer_not_pattern = true 98 | csharp_style_prefer_pattern_matching = true 99 | csharp_style_prefer_switch_expression = true 100 | 101 | # Null-checking preferences 102 | csharp_style_conditional_delegate_call = true:suggestion 103 | 104 | # Modifier preferences 105 | csharp_prefer_static_local_function = true:suggestion 106 | csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async 107 | csharp_style_prefer_readonly_struct = true 108 | csharp_style_prefer_readonly_struct_member = true 109 | 110 | # Code-block preferences 111 | csharp_prefer_braces = true:silent 112 | csharp_prefer_simple_using_statement = true:suggestion 113 | csharp_style_namespace_declarations = block_scoped:silent 114 | csharp_style_prefer_method_group_conversion = true:silent 115 | csharp_style_prefer_top_level_statements = true:silent 116 | 117 | # Expression-level preferences 118 | csharp_prefer_simple_default_expression = true 119 | csharp_style_deconstructed_variable_declaration = true 120 | csharp_style_implicit_object_creation_when_type_is_apparent = true 121 | csharp_style_inlined_variable_declaration = true 122 | csharp_style_prefer_index_operator = true 123 | csharp_style_prefer_local_over_anonymous_function = true:suggestion 124 | csharp_style_prefer_null_check_over_type_check = true 125 | csharp_style_prefer_range_operator = true 126 | csharp_style_prefer_tuple_swap = true 127 | csharp_style_prefer_utf8_string_literals = true 128 | csharp_style_throw_expression = true 129 | csharp_style_unused_value_assignment_preference = discard_variable 130 | csharp_style_unused_value_expression_statement_preference = discard_variable 131 | 132 | # 'using' directive preferences 133 | csharp_using_directive_placement = outside_namespace:silent 134 | 135 | # New line preferences 136 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true 137 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true 138 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true 139 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true 140 | csharp_style_allow_embedded_statements_on_same_line_experimental = true 141 | 142 | #### C# Formatting Rules #### 143 | 144 | # New line preferences 145 | csharp_new_line_before_catch = true 146 | csharp_new_line_before_else = true 147 | csharp_new_line_before_finally = true 148 | csharp_new_line_before_members_in_anonymous_types = true 149 | csharp_new_line_before_members_in_object_initializers = true 150 | csharp_new_line_before_open_brace = all 151 | csharp_new_line_between_query_expression_clauses = true 152 | 153 | # Indentation preferences 154 | csharp_indent_block_contents = true 155 | csharp_indent_braces = false 156 | csharp_indent_case_contents = true 157 | csharp_indent_case_contents_when_block = true 158 | csharp_indent_labels = one_less_than_current 159 | csharp_indent_switch_labels = true 160 | 161 | # Space preferences 162 | csharp_space_after_cast = false 163 | csharp_space_after_colon_in_inheritance_clause = true 164 | csharp_space_after_comma = true 165 | csharp_space_after_dot = false 166 | csharp_space_after_keywords_in_control_flow_statements = true 167 | csharp_space_after_semicolon_in_for_statement = true 168 | csharp_space_around_binary_operators = before_and_after 169 | csharp_space_around_declaration_statements = false 170 | csharp_space_before_colon_in_inheritance_clause = true 171 | csharp_space_before_comma = false 172 | csharp_space_before_dot = false 173 | csharp_space_before_open_square_brackets = false 174 | csharp_space_before_semicolon_in_for_statement = false 175 | csharp_space_between_empty_square_brackets = false 176 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 177 | csharp_space_between_method_call_name_and_opening_parenthesis = false 178 | csharp_space_between_method_call_parameter_list_parentheses = false 179 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 180 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 181 | csharp_space_between_method_declaration_parameter_list_parentheses = false 182 | csharp_space_between_parentheses = false 183 | csharp_space_between_square_brackets = false 184 | 185 | # Wrapping preferences 186 | csharp_preserve_single_line_blocks = true 187 | csharp_preserve_single_line_statements = true 188 | 189 | #### Naming styles #### 190 | 191 | # Naming rules 192 | 193 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 194 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 195 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 196 | 197 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 198 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 199 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 200 | 201 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 202 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 203 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 204 | 205 | # Symbol specifications 206 | 207 | dotnet_naming_symbols.interface.applicable_kinds = interface 208 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 209 | dotnet_naming_symbols.interface.required_modifiers = 210 | 211 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 212 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 213 | dotnet_naming_symbols.types.required_modifiers = 214 | 215 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 216 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 217 | dotnet_naming_symbols.non_field_members.required_modifiers = 218 | 219 | # Naming styles 220 | 221 | dotnet_naming_style.pascal_case.required_prefix = 222 | dotnet_naming_style.pascal_case.required_suffix = 223 | dotnet_naming_style.pascal_case.word_separator = 224 | dotnet_naming_style.pascal_case.capitalization = pascal_case 225 | 226 | dotnet_naming_style.begins_with_i.required_prefix = I 227 | dotnet_naming_style.begins_with_i.required_suffix = 228 | dotnet_naming_style.begins_with_i.word_separator = 229 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 230 | dotnet_diagnostic.S2094.severity = silent 231 | dotnet_diagnostic.CA1032.severity = silent 232 | dotnet_diagnostic.S3925.severity = silent 233 | 234 | [*.{cs,vb}] 235 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 236 | tab_width = 4 237 | indent_size = 4 238 | end_of_line = crlf 239 | dotnet_style_coalesce_expression = true:suggestion 240 | dotnet_style_null_propagation = true:suggestion 241 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 242 | dotnet_style_prefer_auto_properties = true:silent 243 | dotnet_style_object_initializer = true:suggestion 244 | dotnet_style_collection_initializer = true:suggestion 245 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 246 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 247 | dotnet_style_prefer_conditional_expression_over_return = true:silent 248 | dotnet_style_explicit_tuple_names = true:suggestion 249 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 250 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 251 | dotnet_diagnostic.CA1040.severity = silent -------------------------------------------------------------------------------- /backend/dotnet/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml 402 | 403 | ## 404 | ## Visual studio for Mac 405 | ## 406 | 407 | 408 | # globs 409 | Makefile.in 410 | *.userprefs 411 | *.usertasks 412 | config.make 413 | config.status 414 | aclocal.m4 415 | install-sh 416 | autom4te.cache/ 417 | *.tar.gz 418 | tarballs/ 419 | test-results/ 420 | 421 | # Mac bundle stuff 422 | *.dmg 423 | *.app 424 | 425 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 426 | # General 427 | .DS_Store 428 | .AppleDouble 429 | .LSOverride 430 | 431 | # Icon must end with two \r 432 | Icon 433 | 434 | 435 | # Thumbnails 436 | ._* 437 | 438 | # Files that might appear in the root of a volume 439 | .DocumentRevisions-V100 440 | .fseventsd 441 | .Spotlight-V100 442 | .TemporaryItems 443 | .Trashes 444 | .VolumeIcon.icns 445 | .com.apple.timemachine.donotpresent 446 | 447 | # Directories potentially created on remote AFP share 448 | .AppleDB 449 | .AppleDesktop 450 | Network Trash Folder 451 | Temporary Items 452 | .apdisk 453 | 454 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 455 | # Windows thumbnail cache files 456 | Thumbs.db 457 | ehthumbs.db 458 | ehthumbs_vista.db 459 | 460 | # Dump file 461 | *.stackdump 462 | 463 | # Folder config file 464 | [Dd]esktop.ini 465 | 466 | # Recycle Bin used on file shares 467 | $RECYCLE.BIN/ 468 | 469 | # Windows Installer files 470 | *.cab 471 | *.msi 472 | *.msix 473 | *.msm 474 | *.msp 475 | 476 | # Windows shortcuts 477 | *.lnk 478 | 479 | # Vim temporary swap files 480 | *.swp 481 | -------------------------------------------------------------------------------- /backend/dotnet/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | latest 4 | All 5 | false 6 | false 7 | true 8 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /backend/dotnet/Twitter.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{62D17C0C-340D-4531-BBD0-D0F655A6A3C7}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | Directory.Build.props = Directory.Build.props 10 | EndProjectSection 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "framework", "framework", "{EA94AC04-92A6-4559-97ED-0161986DD788}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FSH.Framework.Core", "framework\Core\FSH.Framework.Core.csproj", "{A08EBDAC-2C20-4CF8-A00A-3A6479A89665}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FSH.Framework.Infrastructure", "framework\Infrastructure\FSH.Framework.Infrastructure.csproj", "{C6D52CBF-E885-45BE-AD9C-08DF1B37B35E}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FSH.Framework.Persistence.EntityFrameworkCore", "framework\Persistence.EntityFrameworkCore\FSH.Framework.Persistence.EntityFrameworkCore.csproj", "{DB806F34-1F47-48E3-A3EB-8B30A1BE2335}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FSH.Framework.Persistence.Mongo", "framework\Persistence.NoSQL\FSH.Framework.Persistence.Mongo.csproj", "{2819E6CC-1E78-40AE-8C5F-3F8D2C9FA275}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "services", "services", "{E1F7929A-3776-4B3D-9202-E19FDD0AFA91}" 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gateway", "gateway", "{B122A407-CD54-4FC0-BAB5-91CB675CAE8D}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gateway", "gateway\Gateway.csproj", "{F6B588D9-8469-46BF-ADA6-7B6588FF377D}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {A08EBDAC-2C20-4CF8-A00A-3A6479A89665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {A08EBDAC-2C20-4CF8-A00A-3A6479A89665}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {A08EBDAC-2C20-4CF8-A00A-3A6479A89665}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {A08EBDAC-2C20-4CF8-A00A-3A6479A89665}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {C6D52CBF-E885-45BE-AD9C-08DF1B37B35E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {C6D52CBF-E885-45BE-AD9C-08DF1B37B35E}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {C6D52CBF-E885-45BE-AD9C-08DF1B37B35E}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {C6D52CBF-E885-45BE-AD9C-08DF1B37B35E}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {DB806F34-1F47-48E3-A3EB-8B30A1BE2335}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {DB806F34-1F47-48E3-A3EB-8B30A1BE2335}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {DB806F34-1F47-48E3-A3EB-8B30A1BE2335}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {DB806F34-1F47-48E3-A3EB-8B30A1BE2335}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {2819E6CC-1E78-40AE-8C5F-3F8D2C9FA275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {2819E6CC-1E78-40AE-8C5F-3F8D2C9FA275}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {2819E6CC-1E78-40AE-8C5F-3F8D2C9FA275}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {2819E6CC-1E78-40AE-8C5F-3F8D2C9FA275}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {F6B588D9-8469-46BF-ADA6-7B6588FF377D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {F6B588D9-8469-46BF-ADA6-7B6588FF377D}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {F6B588D9-8469-46BF-ADA6-7B6588FF377D}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {F6B588D9-8469-46BF-ADA6-7B6588FF377D}.Release|Any CPU.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(NestedProjects) = preSolution 59 | {A08EBDAC-2C20-4CF8-A00A-3A6479A89665} = {EA94AC04-92A6-4559-97ED-0161986DD788} 60 | {C6D52CBF-E885-45BE-AD9C-08DF1B37B35E} = {EA94AC04-92A6-4559-97ED-0161986DD788} 61 | {DB806F34-1F47-48E3-A3EB-8B30A1BE2335} = {EA94AC04-92A6-4559-97ED-0161986DD788} 62 | {2819E6CC-1E78-40AE-8C5F-3F8D2C9FA275} = {EA94AC04-92A6-4559-97ED-0161986DD788} 63 | {F6B588D9-8469-46BF-ADA6-7B6588FF377D} = {B122A407-CD54-4FC0-BAB5-91CB675CAE8D} 64 | EndGlobalSection 65 | GlobalSection(ExtensibilityGlobals) = postSolution 66 | SolutionGuid = {175A3E4E-171F-4086-9386-93CEC940F4BD} 67 | EndGlobalSection 68 | EndGlobal 69 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Caching/ICacheService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Caching; 2 | 3 | public interface ICacheService 4 | { 5 | T GetCache(string key); 6 | Task GetCacheAsync(string key, CancellationToken token = default); 7 | 8 | void RefreshCache(string key); 9 | Task RefreshCacheAsync(string key, CancellationToken token = default); 10 | 11 | void RemoveCache(string key); 12 | Task RemoveCacheAsync(string key, CancellationToken token = default); 13 | 14 | void SetCache(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null); 15 | Task SetCacheAsync(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null, CancellationToken cancellationToken = default); 16 | } 17 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Database/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace FSH.Framework.Core.Database; 4 | 5 | public interface IRepository : IReadRepository, IWriteRepository, IDisposable where TDocument : class 6 | { 7 | } 8 | 9 | public interface IReadRepository where TDocument : class 10 | { 11 | Task FindByIdAsync(TId id, CancellationToken cancellationToken = default); 12 | 13 | Task FindOneAsync(Expression> predicate, CancellationToken cancellationToken = default); 14 | 15 | Task> FindAsync(Expression> predicate, CancellationToken cancellationToken = default); 16 | 17 | Task> GetAllAsync(CancellationToken cancellationToken = default); 18 | Task ExistsAsync(Expression> predicate, CancellationToken cancellationToken = default); 19 | } 20 | 21 | public interface IWriteRepository where TDocument : class 22 | { 23 | Task AddAsync(TDocument entity, CancellationToken cancellationToken = default); 24 | Task UpdateAsync(TDocument entity, CancellationToken cancellationToken = default); 25 | Task DeleteRangeAsync(IReadOnlyList entities, CancellationToken cancellationToken = default); 26 | Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default); 27 | Task DeleteAsync(TDocument entity, CancellationToken cancellationToken = default); 28 | Task DeleteByIdAsync(TId id, CancellationToken cancellationToken = default); 29 | } 30 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Domain/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Events; 2 | using MassTransit; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace FSH.Framework.Core.Domain; 6 | public abstract class BaseEntity : BaseEntity 7 | { 8 | protected BaseEntity() => Id = NewId.Next().ToGuid(); 9 | } 10 | 11 | public abstract class BaseEntity : IBaseEntity 12 | { 13 | [JsonPropertyOrder(-1)] 14 | public TId Id { get; protected set; } = default!; 15 | public DateTime CreatedOn { get; private set; } = DateTime.UtcNow; 16 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 17 | public string? CreatedBy { get; private set; } 18 | public DateTime? LastModifiedOn { get; private set; } = DateTime.UtcNow; 19 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 20 | public string? LastModifiedBy { get; private set; } 21 | [JsonIgnore] 22 | public bool IsDeleted { get; private set; } 23 | [JsonIgnore] 24 | private readonly List _domainEvents = new(); 25 | [JsonIgnore] 26 | public IReadOnlyList DomainEvents => _domainEvents.AsReadOnly(); 27 | public void UpdateIsDeleted(bool isDeleted) 28 | { 29 | IsDeleted = isDeleted; 30 | } 31 | public void UpdateModifiedProperties(DateTime? lastModifiedOn, string lastModifiedBy) 32 | { 33 | LastModifiedOn = lastModifiedOn; 34 | LastModifiedBy = lastModifiedBy; 35 | } 36 | public void AddDomainEvent(IDomainEvent @event) 37 | { 38 | _domainEvents.Add(@event); 39 | } 40 | 41 | public IDomainEvent[] ClearDomainEvents() 42 | { 43 | var dequeuedEvents = _domainEvents.ToArray(); 44 | _domainEvents.Clear(); 45 | return dequeuedEvents; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Domain/IBaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Domain; 2 | 3 | public interface IBaseEntity 4 | { 5 | TId Id { get; } 6 | string? CreatedBy { get; } 7 | DateTime? LastModifiedOn { get; } 8 | string? LastModifiedBy { get; } 9 | bool IsDeleted { get; } 10 | void UpdateIsDeleted(bool isDeleted); 11 | void UpdateModifiedProperties(DateTime? lastModifiedOn, string lastModifiedBy); 12 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Events/DomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Events; 2 | 3 | public abstract class DomainEvent : IDomainEvent 4 | { 5 | public DefaultIdType Id { get; } 6 | public DateTime CreationDate { get; } 7 | 8 | public IDictionary MetaData { get; } 9 | 10 | protected DomainEvent() 11 | { 12 | Id = DefaultIdType.NewGuid(); 13 | CreationDate = DateTime.UtcNow; 14 | MetaData = new Dictionary(); 15 | } 16 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Events/IDomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Events; 2 | public interface IDomainEvent : IEvent 3 | { 4 | } 5 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Events/IEvent.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace FSH.Framework.Core.Events; 4 | public interface IEvent : INotification 5 | { 6 | DefaultIdType Id { get; } 7 | DateTime CreationDate { get; } 8 | IDictionary MetaData { get; } 9 | } 10 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Events/IEventPublisher.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Events; 2 | public interface IEventPublisher 3 | { 4 | Task PublishAsync(TEvent @event, CancellationToken token = default) where TEvent : IEvent; 5 | } 6 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Events/IIntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Events; 2 | public interface IIntegrationEvent : IEvent 3 | { 4 | } 5 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Events/IntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Events; 2 | 3 | public class IntegrationEvent : IIntegrationEvent 4 | { 5 | public Guid Id { get; } 6 | public DateTime CreationDate { get; } 7 | 8 | public IDictionary MetaData { get; } 9 | 10 | protected IntegrationEvent() 11 | { 12 | Id = Guid.NewGuid(); 13 | CreationDate = DateTime.UtcNow; 14 | MetaData = new Dictionary(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Exceptions/ConfigurationMissingException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | 5 | public class ConfigurationMissingException : CustomException 6 | { 7 | public ConfigurationMissingException(string sectionName) : base($"{sectionName} Missing in Configurations", HttpStatusCode.NotFound) 8 | { 9 | } 10 | 11 | public ConfigurationMissingException(string message, HttpStatusCode statusCode = HttpStatusCode.NotFound) : base(message, statusCode) 12 | { 13 | } 14 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Exceptions/CustomException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | 5 | public class CustomException : Exception 6 | { 7 | public HttpStatusCode StatusCode { get; } 8 | 9 | public CustomException(string message, HttpStatusCode statusCode = HttpStatusCode.InternalServerError) 10 | : base(message) 11 | { 12 | StatusCode = statusCode; 13 | } 14 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Exceptions/ForbiddenException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | public class ForbiddenException : CustomException 5 | { 6 | public ForbiddenException() : base("You do not have permissions to access this resource.", HttpStatusCode.Forbidden) 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | public class NotFoundException : CustomException 5 | { 6 | public NotFoundException(string message) : base(message, HttpStatusCode.NotFound) 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Exceptions/UnauthorizedException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | 5 | public class UnauthorizedException : CustomException 6 | { 7 | public string Error { get; set; } 8 | public string Description { get; set; } 9 | public UnauthorizedException(string error = default!, string description = default!) : base(error, HttpStatusCode.Unauthorized) 10 | { 11 | Error = error; 12 | Description = description; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/FSH.Framework.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using DefaultIdType = global::System.Guid; -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Identity/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Identity; 2 | public static class RoleConstants 3 | { 4 | public const string AdministratorRole = "Administrator"; 5 | public const string BasicRole = "Basic"; 6 | } 7 | 8 | public static class UserConstants 9 | { 10 | public const string DefaultPassword = "123Pa$$word!"; 11 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Pagination/PagedList.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Pagination; 2 | public class PagedList 3 | { 4 | public IList Data { get; } 5 | public PagedList(IEnumerable items, int totalItems, int pageNumber, int pageSize) 6 | { 7 | PageNumber = pageNumber; 8 | PageSize = pageSize; 9 | TotalItems = totalItems; 10 | if (totalItems > 0) 11 | { 12 | TotalPages = (int)Math.Ceiling(totalItems / (double)pageSize); 13 | } 14 | Data = items as IList ?? new List(items); 15 | } 16 | public int PageNumber { get; } 17 | public int PageSize { get; } 18 | public int TotalPages { get; } 19 | public int TotalItems { get; } 20 | public bool IsFirstPage => PageNumber == 1; 21 | public bool IsLastPage => PageNumber == TotalPages && TotalPages > 0; 22 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Pagination/PaginationParameters.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Pagination; 2 | public abstract class PaginationParameters 3 | { 4 | internal virtual int MaxPageSize { get; } = 20; 5 | internal virtual int DefaultPageSize { get; set; } = 10; 6 | public virtual int PageNumber { get; set; } = 1; 7 | public int PageSize 8 | { 9 | get 10 | { 11 | return DefaultPageSize; 12 | } 13 | set 14 | { 15 | DefaultPageSize = value > MaxPageSize ? MaxPageSize : value; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Serializers/ISerializerService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Services; 2 | 3 | namespace FSH.Framework.Core.Serializers; 4 | 5 | public interface ISerializerService : ITransientService 6 | { 7 | string Serialize(T obj); 8 | 9 | string Serialize(T obj, Type type); 10 | 11 | T Deserialize(string text); 12 | } 13 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Services/IDateTimeService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Services; 2 | 3 | public interface IDateTimeService : IScopedService 4 | { 5 | public DateTime DateTimeUtcNow { get; } 6 | public DateOnly DateOnlyUtcNow { get; } 7 | } 8 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Services/IScopedService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Services; 2 | 3 | public interface IScopedService 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Services/ITransientService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Services; 2 | 3 | public interface ITransientService 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Core/Validation/CustomValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace FSH.Framework.Core.Validation; 4 | public class CustomValidator : AbstractValidator 5 | { 6 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Auth/OpenId/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Exceptions; 2 | using FSH.Framework.Infrastructure.Options; 3 | using Microsoft.AspNetCore.Authentication.JwtBearer; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace FSH.Framework.Infrastructure.Auth.OpenId; 9 | public static class Extensions 10 | { 11 | public static IServiceCollection AddOpenIdAuth(this IServiceCollection services, IConfiguration config, List policyNames) 12 | { 13 | var authOptions = services.BindValidateReturn(config); 14 | 15 | services.AddAuthentication(options => 16 | { 17 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 18 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 19 | }).AddJwtBearer(options => 20 | { 21 | options.Authority = authOptions.Authority; 22 | options.Audience = authOptions.Audience; 23 | options.RequireHttpsMetadata = false; 24 | options.SaveToken = true; 25 | options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters 26 | { 27 | RequireAudience = true, 28 | ValidateAudience = true, 29 | }; 30 | options.Events = new JwtBearerEvents 31 | { 32 | OnChallenge = context => 33 | { 34 | context.HandleResponse(); 35 | if (!context.Response.HasStarted) 36 | { 37 | throw new UnauthorizedException(context.Error!, context.ErrorDescription!); 38 | } 39 | 40 | return Task.CompletedTask; 41 | }, 42 | OnForbidden = _ => throw new ForbiddenException() 43 | }; 44 | }); 45 | 46 | if (policyNames?.Count > 0) 47 | { 48 | services.AddAuthorization(options => 49 | { 50 | foreach (string policyName in policyNames) 51 | { 52 | options.AddPolicy(policyName, policy => policy.Requirements.Add(new HasScopeRequirement(policyName, authOptions.Authority!))); 53 | } 54 | }); 55 | } 56 | 57 | services.AddSingleton(); 58 | return services; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Auth/OpenId/HasScopeHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace FSH.Framework.Infrastructure.Auth.OpenId; 4 | public class HasScopeHandler : AuthorizationHandler 5 | { 6 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement) 7 | { 8 | // If user does not have the scope claim, get out of here 9 | if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer)) 10 | return Task.CompletedTask; 11 | 12 | // Split the scopes string into an array 13 | string[] scopes = context.User.FindFirst(c => c.Type == "scope" && c.Issuer == requirement.Issuer)!.Value.Split(' '); 14 | 15 | // Succeed if the scope array contains the required scope 16 | if (scopes.Any(s => s == requirement.Scope)) 17 | context.Succeed(requirement); 18 | 19 | return Task.CompletedTask; 20 | } 21 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Auth/OpenId/HasScopeRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace FSH.Framework.Infrastructure.Auth.OpenId; 4 | public class HasScopeRequirement : IAuthorizationRequirement 5 | { 6 | public string Issuer { get; } 7 | public string Scope { get; } 8 | 9 | public HasScopeRequirement(string scope, string issuer) 10 | { 11 | Scope = scope ?? throw new ArgumentNullException(nameof(scope)); 12 | Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer)); 13 | } 14 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Auth/OpenId/OpenIdOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace FSH.Framework.Infrastructure.Auth.OpenId; 5 | public class OpenIdOptions : IOptionsRoot 6 | { 7 | [Required(AllowEmptyStrings = false)] 8 | public string? Authority { get; set; } = string.Empty; 9 | [Required(AllowEmptyStrings = false)] 10 | public string? Audience { get; set; } = string.Empty; 11 | } 12 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Auth/OpenIddict/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using FSH.Framework.Infrastructure.Options; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using OpenIddict.Validation.AspNetCore; 9 | using static OpenIddict.Abstractions.OpenIddictConstants; 10 | 11 | namespace FSH.Framework.Infrastructure.Auth.OpenIddict; 12 | 13 | public static class Extensions 14 | { 15 | public static IServiceCollection AddAuthValidation(this IServiceCollection services, IConfiguration config) 16 | { 17 | var authOptions = services.BindValidateReturn(config); 18 | 19 | services.AddOpenIddict() 20 | .AddValidation(options => 21 | { 22 | options.SetIssuer(authOptions.IssuerUrl!); 23 | options.UseIntrospection() 24 | .SetClientId(authOptions.ClientId!) 25 | .SetClientSecret(authOptions.ClientSecret!); 26 | options.UseSystemNetHttp(); 27 | options.UseAspNetCore(); 28 | }); 29 | 30 | services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); 31 | services.AddAuthorization(); 32 | return services; 33 | } 34 | 35 | public static void ConfigureAuthServer(this WebApplicationBuilder builder, Assembly dbContextAssembly, string connectionName = "DefaultConnection") where T : DbContext 36 | { 37 | builder.Services.AddOpenIddict() 38 | .AddCore(options => options.UseEntityFrameworkCore().UseDbContext()) 39 | .AddServer(options => 40 | { 41 | options.SetAuthorizationEndpointUris("/connect/authorize") 42 | .SetIntrospectionEndpointUris("/connect/introspect") 43 | .SetUserinfoEndpointUris("connect/userinfo") 44 | .SetTokenEndpointUris("/connect/token"); 45 | options.AllowClientCredentialsFlow(); 46 | options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles); 47 | options.DisableAccessTokenEncryption(); 48 | options.AddDevelopmentEncryptionCertificate().AddDevelopmentSigningCertificate(); 49 | options.UseAspNetCore().EnableTokenEndpointPassthrough().DisableTransportSecurityRequirement(); 50 | }) 51 | .AddValidation(options => 52 | { 53 | options.UseLocalServer(); 54 | options.UseAspNetCore(); 55 | }); 56 | 57 | builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); 58 | builder.Services.AddAuthorization(); 59 | 60 | string? connectionString = builder.Configuration.GetConnectionString(connectionName); 61 | if (!builder.Environment.IsDevelopment() && connectionString == null) 62 | throw new ArgumentNullException(nameof(connectionString)); 63 | 64 | builder.Services.AddDbContext(options => 65 | { 66 | if (builder.Environment.IsDevelopment()) 67 | { 68 | options.UseInMemoryDatabase("authDb"); 69 | } 70 | else 71 | { 72 | options.UseNpgsql(connectionString, m => m.MigrationsAssembly(dbContextAssembly.FullName)); 73 | } 74 | options.UseOpenIddict(); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Auth/OpenIddict/OpenIddictOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace FSH.Framework.Infrastructure.Auth.OpenIddict; 5 | 6 | public class OpenIddictOptions : IOptionsRoot 7 | { 8 | [Required(AllowEmptyStrings = false)] 9 | public string? ClientId { get; set; } = string.Empty; 10 | [Required(AllowEmptyStrings = false)] 11 | public string? ClientSecret { get; set; } = string.Empty; 12 | [Required(AllowEmptyStrings = false)] 13 | public string? IssuerUrl { get; set; } = string.Empty; 14 | } 15 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Behaviors/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using MediatR; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace FSH.Framework.Infrastructure.Behaviors; 6 | public static class Extensions 7 | { 8 | public static IServiceCollection AddBehaviors(this IServiceCollection services) 9 | { 10 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); 11 | return services; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Behaviors/ValidationBehavior.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using MediatR; 3 | 4 | namespace FSH.Framework.Infrastructure.Behaviors; 5 | public class ValidationBehavior : IPipelineBehavior 6 | where TRequest : IRequest 7 | { 8 | private readonly IEnumerable> _validators; 9 | 10 | public ValidationBehavior(IEnumerable> validators) 11 | { 12 | _validators = validators; 13 | } 14 | 15 | public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) 16 | { 17 | if (_validators.Any()) 18 | { 19 | var context = new ValidationContext(request); 20 | var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken))); 21 | var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList(); 22 | 23 | if (failures.Count != 0) 24 | throw new ValidationException(failures); 25 | } 26 | 27 | return await next(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Caching/CachingOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | 3 | namespace FSH.Framework.Infrastructure.Caching; 4 | public class CachingOptions : IOptionsRoot 5 | { 6 | public bool EnableDistributedCaching { get; set; } = false; 7 | public int SlidingExpirationInMinutes { get; set; } = 2; 8 | public int AbsoluteExpirationInMinutes { get; set; } = 5; 9 | public bool PreferRedis { get; set; } = false; 10 | public string? RedisURL { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Caching/DistributedCacheService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Caching; 2 | using FSH.Framework.Core.Serializers; 3 | using Microsoft.Extensions.Caching.Distributed; 4 | using Microsoft.Extensions.Logging; 5 | using System.Text; 6 | 7 | namespace FSH.Framework.Infrastructure.Caching 8 | { 9 | internal class DistributedCacheService : ICacheService 10 | { 11 | private readonly IDistributedCache _cache; 12 | private readonly ILogger _logger; 13 | private readonly ISerializerService _serializer; 14 | 15 | public DistributedCacheService(IDistributedCache cache, ISerializerService serializer, ILogger logger) => 16 | (_cache, _serializer, _logger) = (cache, serializer, logger); 17 | 18 | public T GetCache(string key) => 19 | Get(key) is { } data 20 | ? Deserialize(data) 21 | : default!; 22 | 23 | private byte[] Get(string key) 24 | { 25 | ArgumentNullException.ThrowIfNull(key); 26 | return _cache.Get(key)!; 27 | } 28 | 29 | public async Task GetCacheAsync(string key, CancellationToken token = default) => 30 | await GetAsync(key, token).ConfigureAwait(true) is { } data 31 | ? Deserialize(data) 32 | : default!; 33 | 34 | private async Task GetAsync(string key, CancellationToken token = default) 35 | { 36 | byte[]? data = await _cache.GetAsync(key, token)!.ConfigureAwait(true); 37 | return data!; 38 | } 39 | 40 | public void RefreshCache(string key) 41 | { 42 | _cache.Refresh(key); 43 | } 44 | 45 | public async Task RefreshCacheAsync(string key, CancellationToken token = default) 46 | { 47 | await _cache.RefreshAsync(key, token).ConfigureAwait(true); 48 | _logger.LogDebug("Cache Refreshed : {key}", key); 49 | } 50 | 51 | public void RemoveCache(string key) 52 | { 53 | _cache.Remove(key); 54 | } 55 | 56 | public async Task RemoveCacheAsync(string key, CancellationToken token = default) 57 | { 58 | await _cache.RemoveAsync(key, token).ConfigureAwait(true); 59 | } 60 | 61 | public void SetCache(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null) => 62 | Set(key, Serialize(value), slidingExpiration); 63 | 64 | private void Set(string key, byte[] value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null) 65 | { 66 | _cache.Set(key, value, GetOptions(slidingExpiration, absoluteExpiration)); 67 | _logger.LogDebug("Added to Cache : {key}", key); 68 | } 69 | 70 | public Task SetCacheAsync(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null, CancellationToken cancellationToken = default) => 71 | SetAsync(key, Serialize(value), slidingExpiration, absoluteExpiration, cancellationToken); 72 | 73 | private async Task SetAsync(string key, byte[] value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null, CancellationToken token = default) 74 | { 75 | await _cache.SetAsync(key, value, GetOptions(slidingExpiration, absoluteExpiration), token).ConfigureAwait(true); 76 | _logger.LogDebug("Added to Cache : {key}", key); 77 | } 78 | 79 | private byte[] Serialize(T item) 80 | { 81 | return Encoding.Default.GetBytes(_serializer.Serialize(item)); 82 | } 83 | 84 | private T Deserialize(byte[] cachedData) => 85 | _serializer.Deserialize(Encoding.Default.GetString(cachedData)); 86 | 87 | private static DistributedCacheEntryOptions GetOptions(TimeSpan? slidingExpiration, DateTimeOffset? absoluteExpiration) 88 | { 89 | var options = new DistributedCacheEntryOptions(); 90 | if (slidingExpiration.HasValue) 91 | { 92 | options.SetSlidingExpiration(slidingExpiration.Value); 93 | } 94 | else 95 | { 96 | options.SetSlidingExpiration(TimeSpan.FromMinutes(10)); // Default expiration time of 10 minutes. 97 | } 98 | 99 | if (absoluteExpiration.HasValue) 100 | { 101 | options.SetAbsoluteExpiration(absoluteExpiration.Value); 102 | } 103 | else 104 | { 105 | options.SetAbsoluteExpiration(TimeSpan.FromMinutes(15)); // Default expiration time of 10 minutes. 106 | } 107 | 108 | return options; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Caching/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Caching; 2 | using FSH.Framework.Infrastructure.Options; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace FSH.Framework.Infrastructure.Caching; 7 | public static class Extensions 8 | { 9 | public static IServiceCollection AddCachingService(this IServiceCollection services, IConfiguration configuration) 10 | { 11 | var cacheOptions = services.BindValidateReturn(configuration); 12 | if (cacheOptions.EnableDistributedCaching) 13 | { 14 | if (cacheOptions.PreferRedis) 15 | { 16 | services.AddStackExchangeRedisCache(options => 17 | { 18 | options.Configuration = cacheOptions.RedisURL; 19 | options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions() 20 | { 21 | AbortOnConnectFail = true, 22 | EndPoints = { cacheOptions.RedisURL } 23 | }; 24 | }); 25 | } 26 | else 27 | { 28 | services.AddDistributedMemoryCache(); 29 | } 30 | 31 | services.AddTransient(); 32 | } 33 | else 34 | { 35 | services.AddTransient(); 36 | } 37 | services.AddMemoryCache(); 38 | 39 | return services; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Caching/InMemoryCacheService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Caching; 2 | using Microsoft.Extensions.Caching.Memory; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace FSH.Framework.Infrastructure.Caching; 7 | 8 | public class InMemoryCacheService : ICacheService 9 | { 10 | private readonly ILogger _logger; 11 | private readonly IMemoryCache _cache; 12 | private readonly CachingOptions _cacheOptions; 13 | public InMemoryCacheService(IMemoryCache cache, ILogger logger, IOptions cacheOptions) 14 | { 15 | _cache = cache; 16 | _logger = logger; 17 | _cacheOptions = cacheOptions.Value; 18 | } 19 | 20 | public T GetCache(string key) => _cache.Get(key)!; 21 | 22 | public Task GetCacheAsync(string key, CancellationToken token = default) 23 | { 24 | var data = GetCache(key)!; 25 | if (data != null) 26 | { 27 | _logger.LogDebug("Get From Cache : {Key}", key); 28 | } 29 | else 30 | { 31 | _logger.LogDebug("Key Not Found in Cache : {Key}", key); 32 | } 33 | return Task.FromResult(data); 34 | } 35 | 36 | public void RefreshCache(string key) => _cache.TryGetValue(key, out _); 37 | 38 | public Task RefreshCacheAsync(string key, CancellationToken token = default) 39 | { 40 | RefreshCache(key); 41 | return Task.CompletedTask; 42 | } 43 | 44 | public void RemoveCache(string key) => _cache.Remove(key); 45 | 46 | public Task RemoveCacheAsync(string key, CancellationToken token = default) 47 | { 48 | RemoveCache(key); 49 | return Task.CompletedTask; 50 | } 51 | 52 | public void SetCache(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null) 53 | { 54 | slidingExpiration ??= TimeSpan.FromMinutes(_cacheOptions.SlidingExpirationInMinutes); 55 | absoluteExpiration ??= DateTime.UtcNow.AddMinutes(_cacheOptions.AbsoluteExpirationInMinutes); 56 | _cache.Set(key, value, new MemoryCacheEntryOptions { SlidingExpiration = slidingExpiration, AbsoluteExpiration = absoluteExpiration }); 57 | _logger.LogDebug("Added to Cache : {Key}", key); 58 | } 59 | 60 | public Task SetCacheAsync(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null, CancellationToken token = default) 61 | { 62 | SetCache(key, value, slidingExpiration); 63 | return Task.CompletedTask; 64 | } 65 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Controllers/BaseApiController.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace FSH.Framework.Infrastructure.Controllers; 8 | 9 | [ApiController] 10 | [Authorize] 11 | [Route("[controller]")] 12 | public class BaseApiController : ControllerBase 13 | { 14 | private ISender _mediator = null!; 15 | 16 | protected ISender Mediator => _mediator ??= HttpContext.RequestServices.GetRequiredService(); 17 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Framework.Infrastructure.Behaviors; 3 | using FSH.Framework.Infrastructure.Caching; 4 | using FSH.Framework.Infrastructure.Logging.Serilog; 5 | using FSH.Framework.Infrastructure.Mapping.Mapster; 6 | using FSH.Framework.Infrastructure.Middlewares; 7 | using FSH.Framework.Infrastructure.Options; 8 | using FSH.Framework.Infrastructure.Services; 9 | using FSH.Framework.Infrastructure.Swagger; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Hosting; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using System.Reflection; 14 | 15 | namespace FSH.Framework.Infrastructure; 16 | 17 | public static class Extensions 18 | { 19 | public const string AllowAllOrigins = "AllowAll"; 20 | public static void AddInfrastructure(this WebApplicationBuilder builder, Assembly? applicationAssembly = null, bool enableSwagger = true) 21 | { 22 | var config = builder.Configuration; 23 | var appOptions = builder.Services.BindValidateReturn(config); 24 | 25 | builder.Services.AddCors(options => 26 | { 27 | options.AddPolicy(name: AllowAllOrigins, builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); 28 | }); 29 | builder.Services.AddExceptionMiddleware(); 30 | builder.Services.AddControllers(); 31 | builder.Services.AddEndpointsApiExplorer(); 32 | builder.ConfigureSerilog(appOptions.Name); 33 | builder.Services.AddRouting(options => options.LowercaseUrls = true); 34 | if (applicationAssembly != null) 35 | { 36 | builder.Services.AddMapsterExtension(applicationAssembly); 37 | builder.Services.AddBehaviors(); 38 | builder.Services.AddValidatorsFromAssembly(applicationAssembly); 39 | builder.Services.AddMediatR(o => o.RegisterServicesFromAssembly(applicationAssembly)); 40 | } 41 | 42 | if (enableSwagger) builder.Services.AddSwaggerExtension(config); 43 | builder.Services.AddCachingService(config); 44 | builder.Services.AddInternalServices(); 45 | } 46 | 47 | public static void UseInfrastructure(this WebApplication app, IWebHostEnvironment env, bool enableSwagger = true) 48 | { 49 | //Preserve Order 50 | app.UseCors(AllowAllOrigins); 51 | app.UseExceptionMiddleware(); 52 | app.UseAuthentication(); 53 | app.UseAuthorization(); 54 | app.MapControllers(); 55 | if (enableSwagger) app.UseSwaggerExtension(env); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/FSH.Framework.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Logging/Serilog/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | using Microsoft.AspNetCore.Builder; 3 | using Serilog; 4 | using Serilog.Core; 5 | using Serilog.Events; 6 | using Serilog.Exceptions; 7 | using Serilog.Formatting.Compact; 8 | 9 | namespace FSH.Framework.Infrastructure.Logging.Serilog; 10 | 11 | public static class Extensions 12 | { 13 | public static void ConfigureSerilog(this WebApplicationBuilder builder, string appName) 14 | { 15 | var config = builder.Configuration; 16 | var serilogOptions = builder.Services.BindValidateReturn(config); 17 | _ = builder.Host.UseSerilog((_, _, serilogConfig) => 18 | { 19 | if (serilogOptions.EnableErichers) ConfigureEnrichers(serilogConfig, appName); 20 | ConfigureConsoleLogging(serilogConfig, serilogOptions.StructuredConsoleLogging); 21 | ConfigureWriteToFile(serilogConfig, serilogOptions.WriteToFile, serilogOptions.RetentionFileCount, appName); 22 | SetMinimumLogLevel(serilogConfig, serilogOptions.MinimumLogLevel); 23 | if (serilogOptions.OverideMinimumLogLevel) OverideMinimumLogLevel(serilogConfig); 24 | }); 25 | } 26 | 27 | private static void ConfigureEnrichers(LoggerConfiguration config, string appName) 28 | { 29 | config 30 | .Enrich.FromLogContext() 31 | .Enrich.WithProperty("Application", appName) 32 | .Enrich.WithExceptionDetails() 33 | .Enrich.WithMachineName() 34 | .Enrich.WithProcessId() 35 | .Enrich.WithThreadId(); 36 | } 37 | 38 | private static void ConfigureConsoleLogging(LoggerConfiguration serilogConfig, bool structuredConsoleLogging) 39 | { 40 | if (structuredConsoleLogging) 41 | { 42 | serilogConfig.WriteTo.Async(wt => wt.Console(new CompactJsonFormatter())); 43 | } 44 | else 45 | { 46 | serilogConfig.WriteTo.Async(wt => wt.Console()); 47 | } 48 | } 49 | 50 | private static void ConfigureWriteToFile(LoggerConfiguration serilogConfig, bool writeToFile, int retainedFileCount, string appName) 51 | { 52 | if (writeToFile) 53 | { 54 | serilogConfig.WriteTo.File( 55 | new CompactJsonFormatter(), 56 | $"Logs/{appName.ToLower()}.logs.json", 57 | restrictedToMinimumLevel: LogEventLevel.Information, 58 | rollingInterval: RollingInterval.Day, 59 | retainedFileCountLimit: retainedFileCount); 60 | } 61 | } 62 | 63 | private static void SetMinimumLogLevel(LoggerConfiguration serilogConfig, string minLogLevel) 64 | { 65 | var loggingLevelSwitch = new LoggingLevelSwitch 66 | { 67 | MinimumLevel = minLogLevel.ToLower() switch 68 | { 69 | "debug" => LogEventLevel.Debug, 70 | "information" => LogEventLevel.Information, 71 | "warning" => LogEventLevel.Warning, 72 | _ => LogEventLevel.Information, 73 | } 74 | }; 75 | serilogConfig.MinimumLevel.ControlledBy(loggingLevelSwitch); 76 | } 77 | 78 | private static void OverideMinimumLogLevel(LoggerConfiguration serilogConfig) 79 | { 80 | serilogConfig 81 | .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 82 | .MinimumLevel.Override("Hangfire", LogEventLevel.Warning) 83 | .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) 84 | .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Error) 85 | .MinimumLevel.Override("OpenIddict.Validation", LogEventLevel.Error) 86 | .MinimumLevel.Override("System.Net.Http.HttpClient.OpenIddict", LogEventLevel.Error); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Logging/Serilog/SerilogOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | 3 | namespace FSH.Framework.Infrastructure.Logging.Serilog; 4 | 5 | public class SerilogOptions : IOptionsRoot 6 | { 7 | public string ElasticSearchUrl { get; set; } = string.Empty; 8 | public bool WriteToFile { get; set; } = false; 9 | public int RetentionFileCount { get; set; } = 5; 10 | public bool StructuredConsoleLogging { get; set; } = false; 11 | public string MinimumLogLevel { get; set; } = "Information"; 12 | public bool EnableErichers { get; set; } = true; 13 | public bool OverideMinimumLogLevel { get; set; } = true; 14 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Mapping/Mapster/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Mapster; 2 | using MapsterMapper; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System.Reflection; 5 | 6 | namespace FSH.Framework.Infrastructure.Mapping.Mapster; 7 | public static class Extensions 8 | { 9 | public static IServiceCollection AddMapsterExtension(this IServiceCollection services, Assembly coreAssembly) 10 | { 11 | var typeAdapterConfig = TypeAdapterConfig.GlobalSettings; 12 | typeAdapterConfig.Scan(coreAssembly); 13 | var mapperConfig = new Mapper(typeAdapterConfig); 14 | services.AddSingleton(mapperConfig); 15 | return services; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Messaging/EventPublisher.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Events; 2 | using MassTransit; 3 | 4 | namespace FSH.Framework.Infrastructure.Messaging; 5 | 6 | public class EventPublisher : IEventPublisher 7 | { 8 | private readonly IPublishEndpoint _publisher; 9 | 10 | public EventPublisher(IPublishEndpoint publisher) 11 | { 12 | _publisher = publisher; 13 | } 14 | 15 | public Task PublishAsync(TEvent @event, CancellationToken token = default) where TEvent : IEvent 16 | { 17 | return _publisher.Publish(@event, token); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Messaging/Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Infrastructure.Messaging 2 | { 3 | internal sealed class Extensions 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Middlewares/ExceptionDetails.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Exceptions; 2 | using Microsoft.AspNetCore.WebUtilities; 3 | using System.Collections.ObjectModel; 4 | using System.Net; 5 | 6 | namespace FSH.Framework.Infrastructure.Middlewares; 7 | 8 | public class ExceptionDetails 9 | { 10 | public string? Title { get; set; } 11 | public string? Detail { get; set; } 12 | public Guid TraceId { get; set; } = Guid.NewGuid(); 13 | public Collection? Errors { get; private set; } 14 | public int? Status { get; set; } 15 | public string? StackTrace { get; set; } 16 | 17 | internal static ExceptionDetails HandleFluentValidationException(FluentValidation.ValidationException exception) 18 | { 19 | var errorResult = new ExceptionDetails() 20 | { 21 | Title = "Validation Failed", 22 | Detail = "One or More Validations failed", 23 | Status = (int)HttpStatusCode.BadRequest, 24 | Errors = new(), 25 | }; 26 | if (exception.Errors.Count() == 1) 27 | { 28 | errorResult.Detail = exception.Errors.FirstOrDefault()!.ErrorMessage; 29 | } 30 | foreach (var error in exception.Errors) 31 | { 32 | errorResult.Errors.Add(error.ErrorMessage); 33 | } 34 | return errorResult; 35 | } 36 | 37 | internal static ExceptionDetails HandleDefaultException(Exception exception) 38 | { 39 | var errorResult = new ExceptionDetails() 40 | { 41 | Title = ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.InternalServerError), 42 | Detail = exception.Message.Trim(), 43 | Status = (int)HttpStatusCode.InternalServerError, 44 | }; 45 | return errorResult; 46 | } 47 | 48 | internal static ExceptionDetails HandleNotFoundException(NotFoundException exception) 49 | { 50 | var errorResult = new ExceptionDetails() 51 | { 52 | Title = ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.NotFound), 53 | Detail = exception.Message.Trim(), 54 | Status = (int)HttpStatusCode.NotFound 55 | }; 56 | return errorResult; 57 | } 58 | 59 | internal static ExceptionDetails HandleUnauthorizedException(UnauthorizedException unauthorizedException) 60 | { 61 | return new ExceptionDetails() 62 | { 63 | Title = string.IsNullOrEmpty(unauthorizedException.Error) ? ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.Unauthorized) : unauthorizedException.Error, 64 | Detail = string.IsNullOrEmpty(unauthorizedException.Description) ? unauthorizedException.Message.Trim() : unauthorizedException.Description, 65 | Status = (int)HttpStatusCode.Unauthorized 66 | }; 67 | } 68 | 69 | internal static ExceptionDetails HandleForbiddenException(ForbiddenException forbiddenException) 70 | { 71 | return new ExceptionDetails() 72 | { 73 | Title = ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.Forbidden), 74 | Detail = forbiddenException.Message.Trim(), 75 | Status = ((int)HttpStatusCode.Forbidden) 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Middlewares/ExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Exceptions; 2 | using FSH.Framework.Core.Serializers; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace FSH.Framework.Infrastructure.Middlewares; 9 | 10 | internal class ExceptionMiddleware : IMiddleware 11 | { 12 | private readonly ILogger _logger; 13 | private readonly ISerializerService _serializer; 14 | private readonly IWebHostEnvironment _env; 15 | 16 | public ExceptionMiddleware(ILogger logger, ISerializerService serializer, IWebHostEnvironment env) 17 | { 18 | _logger = logger; 19 | _serializer = serializer; 20 | _env = env; 21 | } 22 | 23 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 24 | { 25 | try 26 | { 27 | await next(context); 28 | } 29 | catch (Exception exception) 30 | { 31 | var errorResult = exception switch 32 | { 33 | FluentValidation.ValidationException fluentException => ExceptionDetails.HandleFluentValidationException(fluentException), 34 | UnauthorizedException unauthorizedException => ExceptionDetails.HandleUnauthorizedException(unauthorizedException), 35 | ForbiddenException forbiddenException => ExceptionDetails.HandleForbiddenException(forbiddenException), 36 | NotFoundException notFoundException => ExceptionDetails.HandleNotFoundException(notFoundException), 37 | _ => ExceptionDetails.HandleDefaultException(exception), 38 | }; 39 | 40 | var errorLogLevel = exception switch 41 | { 42 | FluentValidation.ValidationException or UnauthorizedException => LogLevel.Warning, 43 | _ => LogLevel.Error 44 | }; 45 | 46 | LogErrorMessage(errorLogLevel, exception, errorResult); 47 | 48 | var response = context.Response; 49 | if (!response.HasStarted) 50 | { 51 | response.ContentType = "application/json"; 52 | response.StatusCode = errorResult.Status!.Value; 53 | await response.WriteAsync(_serializer.Serialize(errorResult)); 54 | } 55 | else 56 | { 57 | _logger.LogWarning("Can't write error response. Response has already started."); 58 | } 59 | } 60 | } 61 | 62 | private void LogErrorMessage(LogLevel errorLogLevel, Exception exception, ExceptionDetails details) 63 | { 64 | var properties = new Dictionary 65 | { 66 | { "TraceId", details.TraceId } 67 | }; 68 | 69 | if (details.Errors != null) 70 | { 71 | properties.Add("Errors", details.Errors); 72 | } 73 | 74 | if (_env.IsDevelopment()) 75 | { 76 | properties.Add("StackTrace", exception.StackTrace!.Trim()); 77 | } 78 | 79 | using (_logger.BeginScope(properties)) 80 | { 81 | _logger.Log(errorLogLevel, "{title} | {details} | {traceId}", details.Title, details.Detail, details.TraceId); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Middlewares/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace FSH.Framework.Infrastructure.Middlewares; 5 | 6 | public static class Extensions 7 | { 8 | public static IServiceCollection AddExceptionMiddleware(this IServiceCollection services) 9 | { 10 | return services.AddScoped(); 11 | } 12 | 13 | public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder app) 14 | { 15 | return app.UseMiddleware(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Options/AppOptions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace FSH.Framework.Infrastructure.Options 4 | { 5 | public class AppOptions : IOptionsRoot 6 | { 7 | [Required(AllowEmptyStrings = false)] 8 | public string Name { get; set; } = "FSH.WebAPI"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Options/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Exceptions; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace FSH.Framework.Infrastructure.Options; 6 | 7 | public static class Extensions 8 | { 9 | public static T LoadOptions(this IConfiguration configuration, string sectionName) where T : IOptionsRoot 10 | { 11 | var options = configuration.GetSection(sectionName).Get() ?? throw new ConfigurationMissingException(sectionName); 12 | return options; 13 | } 14 | 15 | public static T BindValidateReturn(this IServiceCollection services, IConfiguration configuration) where T : class, IOptionsRoot 16 | { 17 | services.AddOptions() 18 | .BindConfiguration(typeof(T).Name) 19 | .ValidateDataAnnotations() 20 | .ValidateOnStart(); 21 | return configuration.LoadOptions(typeof(T).Name); 22 | } 23 | public static void BindValidate(this IServiceCollection services) where T : class, IOptionsRoot 24 | { 25 | services.AddOptions() 26 | .BindConfiguration(typeof(T).Name) 27 | .ValidateDataAnnotations() 28 | .ValidateOnStart(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Options/IOptionsRoot.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Infrastructure.Options; 2 | 3 | public interface IOptionsRoot 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Serializers/NewtonSoftService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Serializers; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | using Newtonsoft.Json.Serialization; 5 | 6 | namespace FSH.Framework.Infrastructure.Serializers; 7 | 8 | public class NewtonSoftService : ISerializerService 9 | { 10 | public T Deserialize(string text) 11 | { 12 | return JsonConvert.DeserializeObject(text)!; 13 | } 14 | 15 | public string Serialize(T obj) 16 | { 17 | return JsonConvert.SerializeObject(obj, new JsonSerializerSettings 18 | { 19 | ContractResolver = new CamelCasePropertyNamesContractResolver(), 20 | NullValueHandling = NullValueHandling.Ignore, 21 | 22 | Converters = new List 23 | { 24 | new StringEnumConverter(new CamelCaseNamingStrategy()) 25 | } 26 | }); 27 | } 28 | 29 | public string Serialize(T obj, Type type) 30 | { 31 | return JsonConvert.SerializeObject(obj, type, new()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Services/DateTimeService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Services; 2 | 3 | namespace FSH.Framework.Infrastructure.Services; 4 | 5 | public class DateTimeService : IDateTimeService 6 | { 7 | public DateTime DateTimeUtcNow => DateTime.UtcNow; 8 | public DateOnly DateOnlyUtcNow => DateOnly.FromDateTime(DateTimeUtcNow); 9 | } 10 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Services/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Services; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace FSH.Framework.Infrastructure.Services; 5 | 6 | internal static class Extensions 7 | { 8 | internal static IServiceCollection AddInternalServices(this IServiceCollection services) => 9 | services 10 | .AddServices(typeof(ITransientService), ServiceLifetime.Transient) 11 | .AddServices(typeof(IScopedService), ServiceLifetime.Scoped); 12 | 13 | internal static IServiceCollection AddServices(this IServiceCollection services, Type interfaceType, ServiceLifetime lifetime) 14 | { 15 | var interfaceTypes = 16 | AppDomain.CurrentDomain.GetAssemblies() 17 | .SelectMany(s => s.GetTypes()) 18 | .Where(t => interfaceType.IsAssignableFrom(t) 19 | && t.IsClass && !t.IsAbstract) 20 | .Select(t => new 21 | { 22 | Service = t.GetInterfaces().FirstOrDefault(), 23 | Implementation = t 24 | }) 25 | .Where(t => t.Service is not null 26 | && interfaceType.IsAssignableFrom(t.Service)); 27 | 28 | foreach (var type in interfaceTypes) 29 | { 30 | services.AddService(type.Service!, type.Implementation, lifetime); 31 | } 32 | 33 | return services; 34 | } 35 | 36 | internal static IServiceCollection AddService(this IServiceCollection services, Type serviceType, Type implementationType, ServiceLifetime lifetime) => 37 | lifetime switch 38 | { 39 | ServiceLifetime.Transient => services.AddTransient(serviceType, implementationType), 40 | ServiceLifetime.Scoped => services.AddScoped(serviceType, implementationType), 41 | ServiceLifetime.Singleton => services.AddSingleton(serviceType, implementationType), 42 | _ => throw new ArgumentException("Invalid lifeTime", nameof(lifetime)) 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Swagger/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | using Microsoft.AspNetCore.Authentication.JwtBearer; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.OpenApi.Models; 9 | using Swashbuckle.AspNetCore.SwaggerUI; 10 | 11 | namespace FSH.Framework.Infrastructure.Swagger 12 | { 13 | internal static class Extensions 14 | { 15 | public static void UseSwaggerExtension(this IApplicationBuilder app, IWebHostEnvironment env) 16 | { 17 | if (!env.IsProduction()) 18 | { 19 | app.UseSwagger(c => 20 | { 21 | c.RouteTemplate = "docs/{documentName}/openapi.json"; 22 | c.PreSerializeFilters.Add((swagger, httpReq) => swagger.Servers = new List { new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}{httpReq.PathBase.Value}" } }); 23 | }); 24 | app.UseSwaggerUI(config => 25 | { 26 | config.SwaggerEndpoint("v1/openapi.json", "Version 1"); 27 | config.RoutePrefix = "docs"; 28 | config.DocExpansion(DocExpansion.List); 29 | config.DisplayRequestDuration(); 30 | config.DefaultModelsExpandDepth(-1); 31 | }); 32 | } 33 | } 34 | internal static void AddSwaggerExtension(this IServiceCollection services, IConfiguration configuration) 35 | { 36 | var swaggerOptions = services.BindValidateReturn(configuration); 37 | _ = services.AddSwaggerGen(config => 38 | { 39 | config.CustomSchemaIds(type => type.ToString()); 40 | config.MapType(() => new OpenApiSchema 41 | { 42 | Type = "string", 43 | Format = "date" 44 | }); 45 | 46 | config.SwaggerDoc( 47 | "v1", 48 | new OpenApiInfo 49 | { 50 | Version = "v1", 51 | Title = swaggerOptions.Title, 52 | Description = swaggerOptions.Description, 53 | Contact = new OpenApiContact 54 | { 55 | Name = swaggerOptions.Name, 56 | Email = swaggerOptions.Email, 57 | }, 58 | }); 59 | 60 | config.AddSecurityRequirement(new OpenApiSecurityRequirement { 61 | { 62 | new OpenApiSecurityScheme { 63 | Reference = new OpenApiReference { 64 | Type = ReferenceType.SecurityScheme, 65 | Id = JwtBearerDefaults.AuthenticationScheme 66 | } 67 | }, 68 | Array.Empty() 69 | }}); 70 | 71 | config.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme 72 | { 73 | Name = "Authorization", 74 | Description = "Input your Bearer token to access this API", 75 | In = ParameterLocation.Header, 76 | Type = SecuritySchemeType.Http, 77 | Scheme = JwtBearerDefaults.AuthenticationScheme, 78 | BearerFormat = "JWT", 79 | }); 80 | }); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Infrastructure/Swagger/SwaggerOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | 3 | namespace FSH.Framework.Infrastructure.Swagger 4 | { 5 | public class SwaggerOptions : IOptionsRoot 6 | { 7 | public string? Title { get; set; } 8 | public string? Description { get; set; } 9 | public string? Name { get; set; } 10 | public string? Email { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Persistence.EntityFrameworkCore/FSH.Framework.Persistence.EntityFrameworkCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Persistence.NoSQL/FSH.Framework.Persistence.Mongo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Persistence.NoSQL/IMongoDbContext.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Driver; 2 | 3 | namespace FSH.Framework.Persistence.Mongo; 4 | public interface IMongoDbContext : IDisposable 5 | { 6 | IMongoCollection GetCollection(string? name = null); 7 | } -------------------------------------------------------------------------------- /backend/dotnet/framework/Persistence.NoSQL/MongoDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using MongoDB.Bson; 3 | using MongoDB.Bson.Serialization.Conventions; 4 | using MongoDB.Driver; 5 | 6 | namespace FSH.Framework.Persistence.Mongo; 7 | public class MongoDbContext : IMongoDbContext 8 | { 9 | public IMongoDatabase Database { get; } 10 | public IMongoClient MongoClient { get; } 11 | 12 | public MongoDbContext(IOptions options) 13 | { 14 | RegisterConventions(); 15 | 16 | MongoClient = new MongoClient(options.Value.ConnectionString); 17 | string databaseName = options.Value.DatabaseName; 18 | Database = MongoClient.GetDatabase(databaseName); 19 | } 20 | 21 | private static void RegisterConventions() 22 | { 23 | ConventionRegistry.Register( 24 | "conventions", 25 | new ConventionPack 26 | { 27 | new CamelCaseElementNameConvention(), 28 | new IgnoreExtraElementsConvention(true), 29 | new IgnoreIfNullConvention(true), 30 | new EnumRepresentationConvention(BsonType.String), 31 | new IgnoreIfDefaultConvention(false) 32 | }, _ => true); 33 | } 34 | 35 | public IMongoCollection GetCollection(string? name = null) 36 | { 37 | return Database.GetCollection(name ?? typeof(T).Name.ToLower()); 38 | } 39 | 40 | public void Dispose() 41 | { 42 | GC.SuppressFinalize(this); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Persistence.NoSQL/MongoOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | 3 | namespace FSH.Framework.Persistence.Mongo; 4 | 5 | public class MongoOptions : IOptionsRoot 6 | { 7 | public string ConnectionString { get; set; } = null!; 8 | public string DatabaseName { get; set; } = null!; 9 | } 10 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Persistence.NoSQL/MongoRepository.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Database; 2 | using FSH.Framework.Core.Domain; 3 | using FSH.Framework.Core.Services; 4 | using MongoDB.Driver; 5 | using System.Linq.Expressions; 6 | 7 | namespace FSH.Framework.Persistence.Mongo; 8 | public class MongoRepository : IRepository where TDocument : class, IBaseEntity 9 | { 10 | private readonly IMongoDbContext _context; 11 | private readonly IMongoCollection _collection; 12 | private readonly IDateTimeService _dateTimeProvider; 13 | 14 | public MongoRepository(IMongoDbContext context, IDateTimeService dateTimeProvider) 15 | { 16 | _context = context; 17 | _collection = _context.GetCollection(); 18 | _dateTimeProvider = dateTimeProvider; 19 | } 20 | 21 | public async Task ExistsAsync(Expression> predicate, CancellationToken cancellationToken = default) 22 | { 23 | return await _collection.Find(predicate).AnyAsync(cancellationToken: cancellationToken)!.ConfigureAwait(true); 24 | } 25 | 26 | public async Task> FindAsync(Expression> predicate, CancellationToken cancellationToken = default) 27 | { 28 | return await _collection.Find(predicate).ToListAsync(cancellationToken: cancellationToken)!.ConfigureAwait(true); 29 | } 30 | 31 | public Task FindOneAsync(Expression> predicate, CancellationToken cancellationToken = default) 32 | { 33 | return _collection.Find(predicate).SingleOrDefaultAsync(cancellationToken: cancellationToken)!; 34 | } 35 | 36 | public Task FindByIdAsync(TId id, CancellationToken cancellationToken = default) 37 | { 38 | return FindOneAsync(e => e.Id!.Equals(id), cancellationToken); 39 | } 40 | 41 | public async Task> GetAllAsync(CancellationToken cancellationToken = default) 42 | { 43 | return await _collection.AsQueryable().ToListAsync(cancellationToken).ConfigureAwait(true); 44 | } 45 | 46 | public async Task AddAsync(TDocument entity, CancellationToken cancellationToken = default) 47 | { 48 | await _collection.InsertOneAsync(entity, new InsertOneOptions(), cancellationToken).ConfigureAwait(true); 49 | } 50 | 51 | public async Task UpdateAsync(TDocument entity, CancellationToken cancellationToken = default) 52 | { 53 | entity.UpdateModifiedProperties(_dateTimeProvider.DateTimeUtcNow, string.Empty); 54 | _ = await _collection.ReplaceOneAsync(x => x.Id!.Equals(entity.Id), entity, cancellationToken: cancellationToken).ConfigureAwait(true); 55 | } 56 | 57 | public Task DeleteRangeAsync(IReadOnlyList entities, CancellationToken cancellationToken = default) 58 | { 59 | throw new NotImplementedException(); 60 | } 61 | 62 | public Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default) 63 | { 64 | throw new NotImplementedException(); 65 | } 66 | 67 | public Task DeleteAsync(TDocument entity, CancellationToken cancellationToken = default) 68 | { 69 | throw new NotImplementedException(); 70 | } 71 | 72 | public async Task DeleteByIdAsync(TId id, CancellationToken cancellationToken = default) 73 | { 74 | await _collection.DeleteOneAsync(d => d.Id!.Equals(id), cancellationToken).ConfigureAwait(true); 75 | } 76 | 77 | public void Dispose() 78 | { 79 | _context?.Dispose(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Persistence.NoSQL/QueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Pagination; 2 | using Mapster; 3 | using MongoDB.Driver.Linq; 4 | 5 | namespace FSH.Framework.Persistence.Mongo; 6 | public static class QueryableExtensions 7 | { 8 | public static async Task> ApplyPagingAsync(this IMongoQueryable collection, int page = 1, int resultsPerPage = 10, CancellationToken cancellationToken = default) 9 | { 10 | if (page <= 0) page = 1; 11 | if (resultsPerPage <= 0) resultsPerPage = 10; 12 | int skipSize = (page - 1) * resultsPerPage; 13 | bool isEmpty = !await collection.AnyAsync(cancellationToken: cancellationToken).ConfigureAwait(true); 14 | if (isEmpty) return new(Enumerable.Empty(), 0, 0, 0); 15 | int totalItems = await collection.CountAsync(cancellationToken: cancellationToken).ConfigureAwait(true); 16 | var data = collection.Skip(skipSize).Take(resultsPerPage).ToList(); 17 | return new PagedList(data, totalItems, page, resultsPerPage); 18 | } 19 | public static async Task> ApplyPagingAsync(this IMongoQueryable collection, int page = 1, int resultsPerPage = 10, CancellationToken cancellationToken = default) 20 | { 21 | if (page <= 0) page = 1; 22 | if (resultsPerPage <= 0) resultsPerPage = 10; 23 | int skipSize = (page - 1) * resultsPerPage; 24 | bool isEmpty = !await collection.AnyAsync(cancellationToken: cancellationToken).ConfigureAwait(true); 25 | if (isEmpty) return new(Enumerable.Empty(), 0, 0, 0); 26 | int totalItems = await collection.CountAsync(cancellationToken: cancellationToken).ConfigureAwait(true); 27 | var data = collection.Skip(skipSize).Take(resultsPerPage).ProjectToType(); 28 | return new PagedList(data, totalItems, page, resultsPerPage); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/dotnet/framework/Persistence.NoSQL/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Database; 2 | using FSH.Framework.Infrastructure.Options; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace FSH.Framework.Persistence.Mongo; 7 | public static class ServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddMongoDbContext( 10 | this IServiceCollection services, IConfiguration configuration) 11 | where TContext : MongoDbContext 12 | { 13 | return services.AddMongoDbContext(configuration); 14 | } 15 | 16 | public static IServiceCollection AddMongoDbContext( 17 | this IServiceCollection services, IConfiguration configuration) 18 | where TContextService : IMongoDbContext 19 | where TContextImplementation : MongoDbContext, TContextService 20 | { 21 | var options = services.BindValidateReturn(configuration); 22 | if (string.IsNullOrEmpty(options.DatabaseName)) throw new ArgumentNullException(nameof(configuration)); 23 | if (string.IsNullOrEmpty(options.ConnectionString)) throw new ArgumentNullException(nameof(configuration)); 24 | services.AddScoped(typeof(TContextService), typeof(TContextImplementation)); 25 | services.AddScoped(typeof(TContextImplementation)); 26 | services.AddScoped(sp => sp.GetRequiredService()); 27 | services.AddTransient(typeof(IRepository<,>), typeof(MongoRepository<,>)); 28 | 29 | return services; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/dotnet/gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | #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. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base 4 | WORKDIR /app 5 | 6 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build 7 | WORKDIR /src 8 | COPY ["Directory.Build.props", "."] 9 | COPY ["gateway/Gateway.csproj", "gateway/"] 10 | COPY ["framework/Core/FSH.Framework.Core.csproj", "framework/Core/"] 11 | COPY ["framework/Infrastructure/FSH.Framework.Infrastructure.csproj", "framework/Infrastructure/"] 12 | RUN dotnet restore "gateway/Gateway.csproj" 13 | COPY . . 14 | WORKDIR "/src/gateway" 15 | RUN dotnet build "Gateway.csproj" -c Release -o /app/build 16 | 17 | FROM build AS publish 18 | RUN dotnet publish "Gateway.csproj" -c Release -o /app/publish /p:UseAppHost=false 19 | 20 | FROM base AS final 21 | WORKDIR /app 22 | COPY --from=publish /app/publish . 23 | ENTRYPOINT ["dotnet", "Twitter.Gateway.dll"] -------------------------------------------------------------------------------- /backend/dotnet/gateway/Gateway.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | Twitter.Gateway 6 | Twitter.Gateway 7 | enable 8 | enable 9 | d2ec1bbc-139a-4d81-a2b7-4dfcaa5145ce 10 | Linux 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /backend/dotnet/gateway/Program.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure; 2 | using FSH.Framework.Infrastructure.Auth.OpenId; 3 | using Microsoft.AspNetCore.Authentication; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | bool enableSwagger = false; 7 | 8 | var policyNames = new List 9 | { 10 | "user:read", 11 | "user:write", 12 | "tweet:read", 13 | "tweet:write" 14 | }; 15 | 16 | builder.Services.AddOpenIdAuth(builder.Configuration, policyNames); 17 | 18 | builder.AddInfrastructure(enableSwagger: enableSwagger); 19 | builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); 20 | var app = builder.Build(); 21 | app.UseInfrastructure(builder.Environment, enableSwagger: enableSwagger); 22 | app.MapGet("/", () => "Hello From Gateway"); 23 | app.UseRouting(); 24 | app.MapReverseProxy(config => 25 | { 26 | config.Use(async (context, next) => 27 | { 28 | string? token = await context.GetTokenAsync("access_token"); 29 | context.Request.Headers["Authorization"] = $"Bearer {token}"; 30 | 31 | await next().ConfigureAwait(false); 32 | }); 33 | }); 34 | 35 | app.Run(); -------------------------------------------------------------------------------- /backend/dotnet/gateway/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "https": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "ASPNETCORE_ENVIRONMENT": "Development" 7 | }, 8 | "dotnetRunMessages": true, 9 | "applicationUrl": "https://localhost:7000;http://localhost:5000" 10 | }, 11 | "Docker": { 12 | "commandName": "Docker", 13 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 14 | "publishAllPorts": true, 15 | "useSSL": true 16 | } 17 | }, 18 | "$schema": "https://json.schemastore.org/launchsettings.json" 19 | } -------------------------------------------------------------------------------- /backend/dotnet/gateway/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "OpenIdOptions": { 3 | "Authority": "https://localhost:7001/", 4 | "Audience": "gateway.resource.server" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /backend/dotnet/gateway/appsettings.docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "OpenIdOptions": { 3 | "Authority": "http://identity/", 4 | "Audience": "gateway.resource.server" 5 | }, 6 | "ReverseProxy": { 7 | "clusters": { 8 | "auth": { 9 | "destinations": { 10 | "auth": { 11 | "address": "http://auth" 12 | } 13 | } 14 | }, 15 | "storage": { 16 | "destinations": { 17 | "storage": { 18 | "address": "http://storage" 19 | } 20 | } 21 | }, 22 | "tweet": { 23 | "destinations": { 24 | "tweet": { 25 | "address": "http://tweet" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /backend/dotnet/gateway/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "SerilogOptions": { 4 | "WriteToFile": true, 5 | "StructuredConsoleLogging": false 6 | }, 7 | "AppOptions": { 8 | "Name": "Gateway" 9 | }, 10 | "CachingOptions": { 11 | "EnableDistributedCaching": false, 12 | "SlidingExpirationInMinutes": 5, 13 | "AbsoluteExpirationInMinutes": 10 14 | }, 15 | "OpenIdOptions": { 16 | "Authority": "https://localhost:7001/", 17 | "Audience": "gateway.resource.server" 18 | }, 19 | "ReverseProxy": { 20 | "routes": { 21 | "auth": { 22 | "clusterId": "auth", 23 | "match": { 24 | "path": "/auth/{**catch-all}" 25 | }, 26 | "transforms": [ 27 | { 28 | "pathPattern": "{**catch-all}" 29 | } 30 | ] 31 | }, 32 | "storage": { 33 | "clusterId": "storage", 34 | "match": { 35 | "path": "/storage/{**catch-all}" 36 | }, 37 | "transforms": [ 38 | { 39 | "pathPattern": "{**catch-all}" 40 | } 41 | ] 42 | }, 43 | "tweet": { 44 | "clusterId": "tweet", 45 | "match": { 46 | "path": "/tweet/{**catch-all}" 47 | }, 48 | "transforms": [ 49 | { 50 | "pathPattern": "{**catch-all}" 51 | } 52 | ] 53 | } 54 | }, 55 | "clusters": { 56 | "auth": { 57 | "destinations": { 58 | "auth": { 59 | "address": "https://localhost:5001" 60 | } 61 | } 62 | }, 63 | "storage": { 64 | "destinations": { 65 | "storage": { 66 | "address": "https://localhost:5002" 67 | } 68 | } 69 | }, 70 | "tweet": { 71 | "destinations": { 72 | "tweet": { 73 | "address": "https://localhost:5003" 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /backend/go/.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | *.env.local -------------------------------------------------------------------------------- /backend/go/services/storage/.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentstack/twitter-clone/4e3e32c4f74c78298b183b6bc67644bd3da521e0/backend/go/services/storage/.env -------------------------------------------------------------------------------- /backend/go/services/storage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest AS build 2 | WORKDIR /app 3 | COPY go.mod go.sum ./ 4 | RUN go mod download 5 | COPY . . 6 | RUN CGO_ENABLED=0 GOOS=linux go build -o storage ./cmd/app/ 7 | FROM gcr.io/distroless/base-debian11 AS release 8 | WORKDIR /app 9 | COPY --from=build /app/storage /app/ 10 | EXPOSE 80 11 | ENTRYPOINT ["/app/storage"] -------------------------------------------------------------------------------- /backend/go/services/storage/README.md: -------------------------------------------------------------------------------- 1 | # Storage Service 2 | 3 | Written in Go, this service should be able to upload, download, delete, update metadata of images with specific categories like post_attachment and avatar. -------------------------------------------------------------------------------- /backend/go/services/storage/cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "storage/controllers" 6 | "storage/handlers" 7 | 8 | "github.com/gofiber/fiber/v2" 9 | ) 10 | 11 | func main() { 12 | app := fiber.New() 13 | 14 | app.Use(func(c *fiber.Ctx) error { 15 | log.Printf("Incoming Request - Method: %s, URL: %s", c.Method(), c.OriginalURL()) 16 | return c.Next() 17 | }) 18 | // api.media.localhost.com/v1/media?type=avatar GET 19 | // api.media.localhost.com/v1/media POST 20 | // api.media.localhost.com/v1/media/{media_id} GET 21 | // api.media.localhost.com/v1/media/{media_id} DELETE 22 | // api.media.localhost.com/v1/media/{media_id}/metadata GET 23 | // api.media.localhost.com/v1/media/{media_id}/metadata PATCH 24 | 25 | mediaHandler := handlers.NewMediaHandler() 26 | mediaController := controllers.NewMediaController(mediaHandler) 27 | controllers.AddRoutes(app, mediaController) 28 | 29 | app.Listen(":80") 30 | } 31 | -------------------------------------------------------------------------------- /backend/go/services/storage/controllers/controllers.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "storage/handlers" 5 | 6 | "github.com/gofiber/fiber/v2" 7 | ) 8 | 9 | func AddRoutes(app *fiber.App, controller *MediaController) { 10 | routes := app.Group("/media") 11 | 12 | routes.Get("/", func(c *fiber.Ctx) error { 13 | return c.SendString("Hello, World 👋!") 14 | }) 15 | routes.Post("/", controller.Upload) 16 | } 17 | 18 | type MediaController struct { 19 | handler *handlers.MediaHandler 20 | } 21 | 22 | func NewMediaController(handler *handlers.MediaHandler) *MediaController { 23 | return &MediaController{ 24 | handler: handler, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/go/services/storage/controllers/mediacontroller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "io/ioutil" 5 | "storage/types" 6 | 7 | "github.com/gofiber/fiber/v2" 8 | ) 9 | 10 | func (controller *MediaController) Upload(c *fiber.Ctx) error { 11 | var request types.UploadRequest 12 | err := c.BodyParser(&request) 13 | if err != nil { 14 | return err 15 | } 16 | file, _ := c.FormFile("file") 17 | fileData, err := file.Open() 18 | if err != nil { 19 | return c.Status(fiber.StatusInternalServerError).SendString("Failed to open uploaded file") 20 | } 21 | defer fileData.Close() 22 | fileBytes, err := ioutil.ReadAll(fileData) 23 | if err != nil { 24 | return c.Status(fiber.StatusInternalServerError).SendString("Failed to read file contents") 25 | } 26 | metadataId, err := controller.handler.Upload(file.Filename, fileBytes, *request.MediaType) 27 | if err != nil { 28 | return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) 29 | } 30 | 31 | return c.Status(fiber.StatusOK).JSON(metadataId) 32 | } -------------------------------------------------------------------------------- /backend/go/services/storage/go.mod: -------------------------------------------------------------------------------- 1 | module storage 2 | 3 | go 1.20 4 | 5 | require github.com/gofiber/fiber/v2 v2.46.0 6 | 7 | require ( 8 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect 9 | github.com/aws/aws-sdk-go-v2/credentials v1.13.24 // indirect 10 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect 11 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect 12 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect 13 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect 14 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect 15 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.8 // indirect 16 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.12 // indirect 17 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect 18 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect 19 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.27 // indirect 20 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect 21 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect 23 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect 24 | github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // indirect 25 | github.com/aws/smithy-go v1.13.5 // indirect 26 | github.com/jmespath/go-jmespath v0.4.0 // indirect 27 | ) 28 | 29 | require ( 30 | github.com/andybalholm/brotli v1.0.5 // indirect 31 | github.com/aws/aws-sdk-go v1.44.281 32 | github.com/aws/aws-sdk-go-v2 v1.18.0 // indirect 33 | github.com/aws/aws-sdk-go-v2/config v1.18.25 34 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.26 35 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 36 | github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 37 | github.com/google/uuid v1.3.0 // indirect 38 | github.com/klauspost/compress v1.16.5 // indirect 39 | github.com/mattn/go-colorable v0.1.13 // indirect 40 | github.com/mattn/go-isatty v0.0.19 // indirect 41 | github.com/mattn/go-runewidth v0.0.14 // indirect 42 | github.com/philhofer/fwd v1.1.2 // indirect 43 | github.com/rivo/uniseg v0.4.4 // indirect 44 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect 45 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect 46 | github.com/tinylib/msgp v1.1.8 // indirect 47 | github.com/valyala/bytebufferpool v1.0.0 // indirect 48 | github.com/valyala/fasthttp v1.47.0 // indirect 49 | github.com/valyala/tcplisten v1.0.0 // indirect 50 | golang.org/x/sys v0.9.0 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /backend/go/services/storage/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/aws/aws-sdk-go v1.44.281 h1:z/ptheJvINaIAsKXthxONM+toTKw2pxyk700Hfm6yUw= 4 | github.com/aws/aws-sdk-go v1.44.281/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= 5 | github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY= 6 | github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= 7 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= 8 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= 9 | github.com/aws/aws-sdk-go-v2/config v1.18.25 h1:JuYyZcnMPBiFqn87L2cRppo+rNwgah6YwD3VuyvaW6Q= 10 | github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4= 11 | github.com/aws/aws-sdk-go-v2/credentials v1.13.24 h1:PjiYyls3QdCrzqUN35jMWtUK1vqVZ+zLfdOa/UPFDp0= 12 | github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o= 13 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.26 h1:bo6rxdLpbvkd9dqcwjdZCwHPdqEiVUbP6WEfOYKzCQo= 14 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.26/go.mod h1:L7MSOy/V38TX7k/evbfKQoUGzJD6Vvib7gTls+BiaBE= 15 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w= 16 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM= 17 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 h1:fI9/5BDEaAv/pv1VO1X1n3jfP9it+IGqWsCuuBQI8wM= 18 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67/go.mod h1:zQClPRIwQZfJlZq6WZve+s4Tb4JW+3V6eS+4+KrYeP8= 19 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4= 20 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw= 21 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk= 22 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM= 23 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8= 24 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc= 25 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs= 26 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g= 27 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.8 h1:9dy4f51hD3LQlM1QSl6s3B/qQV/vOhOrftpP8tzYIVY= 28 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.8/go.mod h1:1MNss6sqoIsFGisX92do/5doiUCBrN7EjhZCS/8DUjI= 29 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.12 h1:ajFG3cZxkWlsVbrMK0b5J6H0a/naM8LlsCzLJorLStE= 30 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.12/go.mod h1:pP+91QTpJMvcFTqGky6puHrkBs8oqoB3XOCiGRDaXwI= 31 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= 32 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= 33 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w= 34 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs= 35 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.27 h1:QmyPCRZNMR1pFbiOi9kBZWZuKrKB9LD4cxltxQk4tNE= 36 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.27/go.mod h1:DfuVY36ixXnsG+uTqnoLWunXAKJ4qjccoFrXUPpj+hs= 37 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo= 38 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw= 39 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ= 40 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM= 41 | github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 h1:O+9nAy9Bb6bJFTpeNFtd9UfHbgxO1o4ZDAM9rQp5NsY= 42 | github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8= 43 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 h1:UBQjaMTCKwyUYwiVnUt6toEJwGXsLBI6al083tpjJzY= 44 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI= 45 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 h1:PkHIIJs8qvq0e5QybnZoG1K/9QTrLr9OsqCIo59jOBA= 46 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk= 47 | github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 h1:2DQLAKDteoEDI8zpCzqBMaZlJuoE9iTYD0gFmXVax9E= 48 | github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8= 49 | github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= 50 | github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= 51 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 52 | github.com/gofiber/fiber/v2 v2.46.0 h1:wkkWotblsGVlLjXj2dpgKQAYHtXumsK/HyFugQM68Ns= 53 | github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc= 54 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 55 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 56 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 57 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 58 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 59 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 60 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 61 | github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= 62 | github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 63 | github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= 64 | github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 65 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 66 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 67 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 68 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 69 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 70 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 71 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 72 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 73 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 74 | github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= 75 | github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= 76 | github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= 77 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 78 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 79 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 80 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 81 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 82 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 83 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4= 84 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8= 85 | github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= 86 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= 87 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= 88 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 89 | github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= 90 | github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= 91 | github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= 92 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 93 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 94 | github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c= 95 | github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 96 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 97 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 98 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 99 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 100 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 101 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 102 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 103 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 104 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 105 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 106 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 107 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 108 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 109 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 110 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 111 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 112 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 113 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 114 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 125 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 126 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 130 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= 132 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 133 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 134 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 135 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 136 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 137 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 138 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 139 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 140 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 141 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 142 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 143 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 144 | golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 145 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 146 | golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 147 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 148 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 149 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 150 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 151 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 152 | -------------------------------------------------------------------------------- /backend/go/services/storage/handlers/handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | type MediaHandler struct{} 4 | 5 | func NewMediaHandler() *MediaHandler { 6 | return &MediaHandler{} 7 | } -------------------------------------------------------------------------------- /backend/go/services/storage/handlers/upload.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "path/filepath" 5 | "storage/pkg/aws" 6 | "storage/types" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/google/uuid" 12 | ) 13 | 14 | func (a *MediaHandler) Upload(fileName string, file []byte, fileType types.Category) (string, error) { 15 | 16 | // filename formatting 17 | fileExtension := filepath.Ext(fileName) 18 | fileNameOnly := strings.Trim(fileName, fileExtension) 19 | fileNameOnly = strings.ToLower(fileNameOnly) 20 | fileNameOnly = strings.ReplaceAll(fileNameOnly, " ", "-") 21 | formattedFileName := fileNameOnly + "-" + strconv.FormatInt(time.Now().UnixNano(), 10) + fileExtension 22 | key := fileType.ToString() + "/" + formattedFileName 23 | 24 | _, err := aws.UploadToS3(key, "cwm-dotnet-bucket", file) 25 | if err != nil { 26 | return "", err 27 | } 28 | metaData := types.Metadata{ 29 | Id: uuid.New().String(), 30 | Key: key, 31 | Category: fileType.ToString(), 32 | } 33 | err = aws.PutItemToDynamoDB(metaData, "media-metadata") 34 | if err != nil { 35 | return "", err 36 | } 37 | return metaData.Id, nil 38 | } 39 | -------------------------------------------------------------------------------- /backend/go/services/storage/pkg/aws/aws.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/aws/aws-sdk-go-v2/config" 8 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 9 | "github.com/aws/aws-sdk-go-v2/service/dynamodb" 10 | "github.com/aws/aws-sdk-go-v2/service/s3" 11 | ) 12 | 13 | var s3client *s3.Client 14 | var s3uploader *manager.Uploader 15 | var ddbClient *dynamodb.Client 16 | 17 | func init() { 18 | cfg, err := config.LoadDefaultConfig(context.TODO()) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | s3client = s3.NewFromConfig(cfg) 23 | s3uploader = manager.NewUploader(s3client) 24 | ddbClient = dynamodb.NewFromConfig(cfg) 25 | } 26 | 27 | -------------------------------------------------------------------------------- /backend/go/services/storage/pkg/aws/constants.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | const ( 4 | MaxRetryAttempts = 3 5 | ) 6 | -------------------------------------------------------------------------------- /backend/go/services/storage/pkg/aws/ddb.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" 8 | "github.com/aws/aws-sdk-go-v2/service/dynamodb" 9 | ) 10 | 11 | func PutItemToDynamoDB(item interface{}, tableName string) error { 12 | ddbItem, err := attributevalue.MarshalMap(item) 13 | if err != nil { 14 | return err 15 | } 16 | input := &dynamodb.PutItemInput{ 17 | Item: ddbItem, 18 | TableName: aws.String(tableName), 19 | } 20 | _, err = ddbClient.PutItem(context.TODO(), input) 21 | if err != nil { 22 | return err 23 | } 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /backend/go/services/storage/pkg/aws/s3.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/service/s3" 9 | ) 10 | 11 | func UploadToS3(key, bucketName string, fileData []byte) (string,error) { 12 | var retryCount int32 13 | var objectKey string 14 | for { 15 | result, err := s3uploader.Upload(context.TODO(), &s3.PutObjectInput{ 16 | Bucket: aws.String(bucketName), 17 | Key: aws.String(key), 18 | Body: bytes.NewReader(fileData), 19 | }) 20 | if err != nil && retryCount < MaxRetryAttempts{ 21 | retryCount++ 22 | if retryCount >= MaxRetryAttempts{ 23 | return "", err 24 | } 25 | } else{ 26 | objectKey = *result.Key 27 | break 28 | } 29 | } 30 | return objectKey,nil 31 | } -------------------------------------------------------------------------------- /backend/go/services/storage/types/category.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Category int 4 | 5 | const ( 6 | ImageAttachment Category = iota 7 | VideoAttachment 8 | Avatar 9 | ) 10 | 11 | func (category Category) ToString() string { 12 | switch category { 13 | case VideoAttachment: 14 | return "video-attachments" 15 | case ImageAttachment: 16 | return "image-attachments" 17 | case Avatar: 18 | return "avatars" 19 | } 20 | return "misc" 21 | } 22 | -------------------------------------------------------------------------------- /backend/go/services/storage/types/metadata.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Metadata struct { 4 | Id string `json:"id" dynamodbav:"id"` 5 | Key string `json:"key" dynamodbav:"key"` 6 | Category string `json:"category" dynamodbav:"category"` 7 | } 8 | -------------------------------------------------------------------------------- /backend/go/services/storage/types/uploadrequest.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type UploadRequest struct { 4 | MediaType *Category `json:"mediaType" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | name: twitter 3 | 4 | services: 5 | gateway: 6 | container_name: gateway 7 | build: 8 | context: ./backend/dotnet 9 | dockerfile: gateway/Dockerfile 10 | ports: 11 | - 7001:7001 12 | - 5001:5001 13 | environment: 14 | - ASPNETCORE_ENVIRONMENT=docker 15 | - ASPNETCORE_URLS=https://+:7001;http://+:5001 16 | - ASPNETCORE_HTTPS_PORT=7001 17 | - ASPNETCORE_Kestrel__Certificates__Default__Password=password! 18 | - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/cert.pfx 19 | volumes: 20 | - ~/.aspnet/https:/https:ro 21 | networks: 22 | - twitter 23 | 24 | storage: 25 | container_name: storage 26 | build: 27 | context: ./backend/go/services/storage 28 | dockerfile: Dockerfile 29 | image: twitter/storage:latest 30 | env_file: 31 | - ./backend/go/services/storage/.env.local 32 | networks: 33 | - twitter 34 | 35 | networks: 36 | twitter: 37 | name: twitter -------------------------------------------------------------------------------- /frontend/nextjs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /frontend/nextjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /frontend/nextjs/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentstack/twitter-clone/4e3e32c4f74c78298b183b6bc67644bd3da521e0/frontend/nextjs/app/favicon.ico -------------------------------------------------------------------------------- /frontend/nextjs/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/nextjs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { Inter } from 'next/font/google' 3 | 4 | const inter = Inter({ subsets: ['latin'] }) 5 | 6 | export const metadata = { 7 | title: 'Create Next App', 8 | description: 'Generated by create next app', 9 | } 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode 15 | }) { 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /frontend/nextjs/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |
7 |

8 | Get started by editing  9 | app/page.tsx 10 |

11 | 29 |
30 | 31 |
32 | Next.js Logo 40 |
41 | 42 | 111 |
112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /frontend/nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /frontend/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "20.3.0", 13 | "@types/react": "18.2.11", 14 | "@types/react-dom": "18.2.5", 15 | "autoprefixer": "10.4.14", 16 | "eslint": "8.42.0", 17 | "eslint-config-next": "13.4.5", 18 | "next": "13.4.5", 19 | "postcss": "8.4.24", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "tailwindcss": "3.3.2", 23 | "typescript": "5.1.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/nextjs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/nextjs/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/nextjs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/nextjs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /frontend/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /misc/twitter.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "74195b1f-4677-4271-87a7-18ba2ed24a93", 4 | "name": "twitter", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "9964323" 7 | }, 8 | "item": [ 9 | { 10 | "name": "storage", 11 | "item": [ 12 | { 13 | "name": "media", 14 | "item": [ 15 | { 16 | "name": "upload", 17 | "request": { 18 | "method": "POST", 19 | "header": [], 20 | "body": { 21 | "mode": "formdata", 22 | "formdata": [ 23 | { 24 | "key": "file", 25 | "type": "file", 26 | "src": "/C:/Users/iammu/Downloads/Deploy Blazor WebAssembly to AWS Amplify.png" 27 | }, 28 | { 29 | "key": "mediaType", 30 | "value": "0", 31 | "type": "text" 32 | }, 33 | { 34 | "key": "fileName", 35 | "value": "blazor", 36 | "type": "text" 37 | } 38 | ] 39 | }, 40 | "url": { 41 | "raw": "http://localhost:3000/v1/api/media", 42 | "protocol": "http", 43 | "host": [ 44 | "localhost" 45 | ], 46 | "port": "3000", 47 | "path": [ 48 | "v1", 49 | "api", 50 | "media" 51 | ] 52 | } 53 | }, 54 | "response": [] 55 | } 56 | ] 57 | } 58 | ] 59 | } 60 | ] 61 | } --------------------------------------------------------------------------------