diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index a2816621e0..0000000000 --- a/.editorconfig +++ /dev/null @@ -1,132 +0,0 @@ -############################### -# Core EditorConfig Options # -############################### -root = true -# All files -[*] -indent_style = space - -# XML project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] -indent_size = 2 - -# XML config files -[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] -indent_size = 2 - -# Code files -[*.{cs,csx,vb,vbx}] -indent_size = 4 -insert_final_newline = true -charset = utf-8-bom -############################### -# .NET Coding Conventions # -############################### -[*.{cs,vb}] -# Organize usings -dotnet_sort_system_directives_first = true -# this. preferences -dotnet_style_qualification_for_field = false:silent -dotnet_style_qualification_for_property = false:silent -dotnet_style_qualification_for_method = false:silent -dotnet_style_qualification_for_event = false:silent -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true:silent -dotnet_style_predefined_type_for_member_access = true:silent -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent -dotnet_style_readonly_field = true:suggestion -# Expression-level preferences -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -############################### -# Naming Conventions # -############################### -# Style Definitions -dotnet_naming_style.pascal_case_style.capitalization = pascal_case -# Use PascalCase for constant fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style -dotnet_naming_symbols.constant_fields.applicable_kinds = field -dotnet_naming_symbols.constant_fields.applicable_accessibilities = * -dotnet_naming_symbols.constant_fields.required_modifiers = const -############################### -# C# Coding Conventions # -############################### -[*.cs] -# var preferences -csharp_style_var_for_built_in_types = true:silent -csharp_style_var_when_type_is_apparent = true:silent -csharp_style_var_elsewhere = true:silent -# Expression-bodied members -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent -# Pattern matching preferences -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -# Null-checking preferences -csharp_style_throw_expression = true:suggestion -csharp_style_conditional_delegate_call = true:suggestion -# Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion -# Expression-level preferences -csharp_prefer_braces = true:silent -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -############################### -# C# Formatting Rules # -############################### -# New line preferences -csharp_new_line_before_open_brace = all -csharp_new_line_before_else = true -csharp_new_line_before_catch = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_between_query_expression_clauses = true -# Indentation preferences -csharp_indent_case_contents = true -csharp_indent_switch_labels = true -csharp_indent_labels = flush_left -# Space preferences -csharp_space_after_cast = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_around_binary_operators = before_and_after -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -# Wrapping preferences -csharp_preserve_single_line_statements = true -csharp_preserve_single_line_blocks = true -############################### -# VB Coding Conventions # -############################### -[*.vb] -# Modifier preferences -visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion \ No newline at end of file diff --git a/.github/workflows/basket-api-deploy.yml b/.github/workflows/basket-api-deploy.yml index bef582eb04..38c16ed2e5 100644 --- a/.github/workflows/basket-api-deploy.yml +++ b/.github/workflows/basket-api-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/basket-api.yml b/.github/workflows/basket-api.yml index 04658cb76c..d8b19993bb 100644 --- a/.github/workflows/basket-api.yml +++ b/.github/workflows/basket-api.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -41,7 +41,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/catalog-api-deploy.yml b/.github/workflows/catalog-api-deploy.yml index 7b78bda37d..804469296c 100644 --- a/.github/workflows/catalog-api-deploy.yml +++ b/.github/workflows/catalog-api-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/catalog-api.yml b/.github/workflows/catalog-api.yml index cd53f986fa..6def653d07 100644 --- a/.github/workflows/catalog-api.yml +++ b/.github/workflows/catalog-api.yml @@ -33,7 +33,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-test with: service: ${{ env.SERVICE }} @@ -47,7 +47,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/composite/build-push/action.yml b/.github/workflows/composite/build-push/action.yml index ddb0fc1d63..80be3b8576 100644 --- a/.github/workflows/composite/build-push/action.yml +++ b/.github/workflows/composite/build-push/action.yml @@ -31,9 +31,11 @@ runs: mkdir -p ~/.docker echo $'{\n "experimental": "enabled"\n}' | sudo tee ~/.docker/config.json sudo service docker restart + docker version -f '{{.Client.Experimental}}' + docker version -f '{{.Server.Experimental}}' - name: Login to Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v1 with: registry: ${{ inputs.registry_host }} username: ${{ inputs.registry_username }} @@ -66,4 +68,4 @@ runs: shell: bash run: | docker --config ~/.docker manifest create ${{ inputs.registry_endpoint }}/${{ inputs.image_name }}:${{ env.BRANCH }} ${{ inputs.registry_endpoint }}/${{ inputs.image_name }}:linux-${{ env.BRANCH }} - docker --config ~/.docker manifest push ${{ inputs.registry_endpoint }}/${{ inputs.image_name }}:${{ env.BRANCH }} + docker --config ~/.docker manifest push ${{ inputs.registry_endpoint }}/${{ inputs.image_name }}:${{ env.BRANCH }} \ No newline at end of file diff --git a/.github/workflows/identity-api-deploy.yml b/.github/workflows/identity-api-deploy.yml index 13bbae2574..c2c0df9369 100644 --- a/.github/workflows/identity-api-deploy.yml +++ b/.github/workflows/identity-api-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/identity-api.yml b/.github/workflows/identity-api.yml index d2f6ef0b2d..71dc799488 100644 --- a/.github/workflows/identity-api.yml +++ b/.github/workflows/identity-api.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -41,7 +41,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/mobileshoppingagg-deploy.yml b/.github/workflows/mobileshoppingagg-deploy.yml index 0ccd61b4fb..bfe6fa9672 100644 --- a/.github/workflows/mobileshoppingagg-deploy.yml +++ b/.github/workflows/mobileshoppingagg-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/mobileshoppingagg.yml b/.github/workflows/mobileshoppingagg.yml index 80af0eb139..f05e5e299c 100644 --- a/.github/workflows/mobileshoppingagg.yml +++ b/.github/workflows/mobileshoppingagg.yml @@ -28,7 +28,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -39,7 +39,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/ordering-api-deploy.yml b/.github/workflows/ordering-api-deploy.yml index 1ca6557847..e689d06a44 100644 --- a/.github/workflows/ordering-api-deploy.yml +++ b/.github/workflows/ordering-api-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/ordering-api.yml b/.github/workflows/ordering-api.yml index eebff24de9..617fba8d77 100644 --- a/.github/workflows/ordering-api.yml +++ b/.github/workflows/ordering-api.yml @@ -33,7 +33,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-test with: service: ${{ env.SERVICE }} @@ -47,7 +47,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/ordering-backgroundtasks-deploy.yml b/.github/workflows/ordering-backgroundtasks-deploy.yml index b1d5dd69a8..1b3b7245e3 100644 --- a/.github/workflows/ordering-backgroundtasks-deploy.yml +++ b/.github/workflows/ordering-backgroundtasks-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/ordering-backgroundtasks.yml b/.github/workflows/ordering-backgroundtasks.yml index 3f3b23da4f..0f5a535d88 100644 --- a/.github/workflows/ordering-backgroundtasks.yml +++ b/.github/workflows/ordering-backgroundtasks.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -41,7 +41,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/ordering-signalrhub-deploy.yml b/.github/workflows/ordering-signalrhub-deploy.yml index 447e68de57..455cf7f1dc 100644 --- a/.github/workflows/ordering-signalrhub-deploy.yml +++ b/.github/workflows/ordering-signalrhub-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/ordering-signalrhub.yml b/.github/workflows/ordering-signalrhub.yml index 0d75316c40..37d910e539 100644 --- a/.github/workflows/ordering-signalrhub.yml +++ b/.github/workflows/ordering-signalrhub.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -41,7 +41,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/payment-api-deploy.yml b/.github/workflows/payment-api-deploy.yml index 7f111f15d2..2f37988ede 100644 --- a/.github/workflows/payment-api-deploy.yml +++ b/.github/workflows/payment-api-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/payment-api.yml b/.github/workflows/payment-api.yml index 2ba5d2a29d..66c9411522 100644 --- a/.github/workflows/payment-api.yml +++ b/.github/workflows/payment-api.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -41,7 +41,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/webhooks-api-deploy.yml b/.github/workflows/webhooks-api-deploy.yml index 42f4b27e01..4d74a38e6b 100644 --- a/.github/workflows/webhooks-api-deploy.yml +++ b/.github/workflows/webhooks-api-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/webhooks-api.yml b/.github/workflows/webhooks-api.yml index 05b60ed018..bee7b76b35 100644 --- a/.github/workflows/webhooks-api.yml +++ b/.github/workflows/webhooks-api.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -41,7 +41,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/webhooks-client.yml b/.github/workflows/webhooks-client.yml index 54df468a2d..1c49a8a5d3 100644 --- a/.github/workflows/webhooks-client.yml +++ b/.github/workflows/webhooks-client.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -41,7 +41,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/webmvc-deploy.yml b/.github/workflows/webmvc-deploy.yml index f99a0fb14b..e70cefed2a 100644 --- a/.github/workflows/webmvc-deploy.yml +++ b/.github/workflows/webmvc-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: azure/login@v1 with: diff --git a/.github/workflows/webmvc.yml b/.github/workflows/webmvc.yml index bc4cb576dd..66fcf06bcc 100644 --- a/.github/workflows/webmvc.yml +++ b/.github/workflows/webmvc.yml @@ -31,7 +31,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -42,7 +42,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/webshoppingagg-deploy.yml b/.github/workflows/webshoppingagg-deploy.yml index 37ecebc49e..2b4bcaf12e 100644 --- a/.github/workflows/webshoppingagg-deploy.yml +++ b/.github/workflows/webshoppingagg-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/webshoppingagg.yml b/.github/workflows/webshoppingagg.yml index c5309a2eaa..93bc05e094 100644 --- a/.github/workflows/webshoppingagg.yml +++ b/.github/workflows/webshoppingagg.yml @@ -28,7 +28,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -39,7 +39,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/webspa-deploy.yml b/.github/workflows/webspa-deploy.yml index 9fb71b5a7c..2851f69621 100644 --- a/.github/workflows/webspa-deploy.yml +++ b/.github/workflows/webspa-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/webspa.yml b/.github/workflows/webspa.yml index a44a3d68f7..769a6bdcf9 100644 --- a/.github/workflows/webspa.yml +++ b/.github/workflows/webspa.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -41,7 +41,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.github/workflows/webstatus-deploy.yml b/.github/workflows/webstatus-deploy.yml index 6d9baf6492..866a46a5f8 100644 --- a/.github/workflows/webstatus-deploy.yml +++ b/.github/workflows/webstatus-deploy.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 - uses: ./.github/workflows/composite/deploy-helm with: diff --git a/.github/workflows/webstatus.yml b/.github/workflows/webstatus.yml index 38cbb33ef5..0cbb89c6ae 100644 --- a/.github/workflows/webstatus.yml +++ b/.github/workflows/webstatus.yml @@ -31,7 +31,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build with: service: ${{ env.SERVICE }} @@ -42,7 +42,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v2 - uses: ./.github/workflows/composite/build-push with: service: ${{ env.SERVICE }} diff --git a/.gitignore b/.gitignore index 2810d6b1cc..587a6e0a09 100644 --- a/.gitignore +++ b/.gitignore @@ -281,5 +281,3 @@ pub/ src/**/app.yaml src/**/inf.yaml -.angular/ -/src/Services/Identity/Identity.API/keys/*.json diff --git a/deploy/elk/Readme.md b/deploy/elk/Readme.md index a0cd286d89..dddeab3fd6 100644 --- a/deploy/elk/Readme.md +++ b/deploy/elk/Readme.md @@ -84,5 +84,5 @@ The only thing that remains is to connect to your vm vía browser. And check the You can get the password for accessing going to your virtual machine in azure and check the boot diagnostics, theres a message that shows to you which is your password. -When you have the user and password you can access to the kibana tool, and create the ```eshops-*``` index pattern that is well documented at the beginning of this documentation and then start to discover. -![](img/elk/) +When you have the user and password you can access to the kibana tool, and create the ```eshops-*``` index pattern that is well documented at the beggining of this documentation and then start to discover. +![](img/elk/) \ No newline at end of file diff --git a/src/.env b/src/.env index 6a32549993..3aac89a3f4 100644 --- a/src/.env +++ b/src/.env @@ -6,15 +6,15 @@ # Use this values to run the app locally in Windows ESHOP_EXTERNAL_DNS_NAME_OR_IP=host.docker.internal -ESHOP_STORAGE_CATALOG_URL=http://host.docker.internal:5121/c/api/v1/catalog/items/[0]/pic/ +ESHOP_STORAGE_CATALOG_URL=http://host.docker.internal:5202/c/api/v1/catalog/items/[0]/pic/ # Use this values to run the app locally in Mac # ESHOP_EXTERNAL_DNS_NAME_OR_IP=docker.for.mac.localhost -# ESHOP_STORAGE_CATALOG_URL=http://docker.for.mac.localhost:5121/c/api/v1/catalog/items/[0]/pic/ +# ESHOP_STORAGE_CATALOG_URL=http://docker.for.mac.localhost:5202/c/api/v1/catalog/items/[0]/pic/ # Use this values to run the app locally in Linux # ESHOP_EXTERNAL_DNS_NAME_OR_IP=docker.for.linux.localhost -# ESHOP_STORAGE_CATALOG_URL=http://docker.for.linux.localhost:5121/c/api/v1/catalog/items/[0]/pic/ +# ESHOP_STORAGE_CATALOG_URL=http://docker.for.linux.localhost:5202/c/api/v1/catalog/items/[0]/pic/ # Configure this values to the cloud storage locations # ESHOP_STORAGE_CATALOG_URL= diff --git a/src/ApiGateways/Envoy/config/mobileshopping/envoy.yaml b/src/ApiGateways/Envoy/config/mobileshopping/envoy.yaml new file mode 100644 index 0000000000..8f2162ad79 --- /dev/null +++ b/src/ApiGateways/Envoy/config/mobileshopping/envoy.yaml @@ -0,0 +1,139 @@ +admin: + access_log_path: "/dev/null" + address: + socket_address: + address: 0.0.0.0 + port_value: 8001 +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 80 + filter_chains: + - filters: + - name: envoy.http_connection_manager + config: + codec_type: auto + stat_prefix: ingress_http + route_config: + name: eshop_backend_route + virtual_hosts: + - name: eshop_backend + domains: + - "*" + routes: + - name: "c-short" + match: + prefix: "/c/" + route: + auto_host_rewrite: true + prefix_rewrite: "/catalog-api/" + cluster: catalog + - name: "c-long" + match: + prefix: "/catalog-api/" + route: + auto_host_rewrite: true + cluster: catalog + - name: "o-short" + match: + prefix: "/o/" + route: + auto_host_rewrite: true + prefix_rewrite: "/ordering-api/" + cluster: ordering + - name: "o-long" + match: + prefix: "/ordering-api/" + route: + auto_host_rewrite: true + cluster: ordering + - name: "h-long" + match: + prefix: "/hub/notificationhub" + route: + auto_host_rewrite: true + cluster: signalr-hub + timeout: 300s + - name: "b-short" + match: + prefix: "/b/" + route: + auto_host_rewrite: true + prefix_rewrite: "/basket-api/" + cluster: basket + - name: "b-long" + match: + prefix: "/basket-api/" + route: + auto_host_rewrite: true + cluster: basket + - name: "agg" + match: + prefix: "/" + route: + auto_host_rewrite: true + prefix_rewrite: "/" + cluster: shoppingagg + http_filters: + - name: envoy.router + access_log: + - name: envoy.file_access_log + filter: + not_health_check_filter: {} + config: + json_format: + time: "%START_TIME%" + protocol: "%PROTOCOL%" + duration: "%DURATION%" + request_method: "%REQ(:METHOD)%" + request_host: "%REQ(HOST)%" + path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%" + response_flags: "%RESPONSE_FLAGS%" + route_name: "%ROUTE_NAME%" + upstream_host: "%UPSTREAM_HOST%" + upstream_cluster: "%UPSTREAM_CLUSTER%" + upstream_local_address: "%UPSTREAM_LOCAL_ADDRESS%" + path: "/tmp/access.log" + clusters: + - name: shoppingagg + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + hosts: + - socket_address: + address: mobileshoppingagg + port_value: 80 + - name: catalog + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + hosts: + - socket_address: + address: catalog-api + port_value: 80 + - name: basket + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + hosts: + - socket_address: + address: basket-api + port_value: 80 + - name: ordering + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + hosts: + - socket_address: + address: ordering-api + port_value: 80 + - name: signalr-hub + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + hosts: + - socket_address: + address: ordering-signalrhub + port_value: 80 diff --git a/src/ApiGateways/Envoy/config/webshopping/envoy.yaml b/src/ApiGateways/Envoy/config/webshopping/envoy.yaml new file mode 100644 index 0000000000..688fb740cc --- /dev/null +++ b/src/ApiGateways/Envoy/config/webshopping/envoy.yaml @@ -0,0 +1,142 @@ +admin: + access_log_path: "/dev/null" + address: + socket_address: + address: 0.0.0.0 + port_value: 8001 +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 80 + filter_chains: + - filters: + - name: envoy.http_connection_manager + config: + codec_type: auto + stat_prefix: ingress_http + route_config: + name: eshop_backend_route + virtual_hosts: + - name: eshop_backend + domains: + - "*" + routes: + - name: "c-short" + match: + prefix: "/c/" + route: + auto_host_rewrite: true + prefix_rewrite: "/catalog-api/" + cluster: catalog + - name: "c-long" + match: + prefix: "/catalog-api/" + route: + auto_host_rewrite: true + cluster: catalog + - name: "o-short" + match: + prefix: "/o/" + route: + auto_host_rewrite: true + prefix_rewrite: "/ordering-api/" + cluster: ordering + - name: "o-long" + match: + prefix: "/ordering-api/" + route: + auto_host_rewrite: true + cluster: ordering + - name: "h-long" + match: + prefix: "/hub/notificationhub" + route: + auto_host_rewrite: true + cluster: signalr-hub + timeout: 300s + upgrade_configs: + upgrade_type: "websocket" + enabled: true + - name: "b-short" + match: + prefix: "/b/" + route: + auto_host_rewrite: true + prefix_rewrite: "/basket-api/" + cluster: basket + - name: "b-long" + match: + prefix: "/basket-api/" + route: + auto_host_rewrite: true + cluster: basket + - name: "agg" + match: + prefix: "/" + route: + auto_host_rewrite: true + prefix_rewrite: "/" + cluster: shoppingagg + http_filters: + - name: envoy.router + access_log: + - name: envoy.file_access_log + filter: + not_health_check_filter: {} + config: + json_format: + time: "%START_TIME%" + protocol: "%PROTOCOL%" + duration: "%DURATION%" + request_method: "%REQ(:METHOD)%" + request_host: "%REQ(HOST)%" + path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%" + response_flags: "%RESPONSE_FLAGS%" + route_name: "%ROUTE_NAME%" + upstream_host: "%UPSTREAM_HOST%" + upstream_cluster: "%UPSTREAM_CLUSTER%" + upstream_local_address: "%UPSTREAM_LOCAL_ADDRESS%" + path: "/tmp/access.log" + clusters: + - name: shoppingagg + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + hosts: + - socket_address: + address: webshoppingagg + port_value: 80 + - name: catalog + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + hosts: + - socket_address: + address: catalog-api + port_value: 80 + - name: basket + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + hosts: + - socket_address: + address: basket-api + port_value: 80 + - name: ordering + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + hosts: + - socket_address: + address: ordering-api + port_value: 80 + - name: signalr-hub + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + hosts: + - socket_address: + address: ordering-signalrhub + port_value: 80 diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Controllers/BasketController.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Controllers/BasketController.cs index 03d744555e..a3cf4f03c6 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Controllers/BasketController.cs +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Controllers/BasketController.cs @@ -16,7 +16,8 @@ public BasketController(ICatalogService catalogService, IBasketService basketSer [HttpPost] [HttpPut] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] public async Task> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data) { if (data.Items == null || !data.Items.Any()) @@ -72,7 +73,8 @@ public async Task> UpdateAllBasketAsync([FromBody] Upda [HttpPut] [Route("items")] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] public async Task> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data) { if (!data.Updates.Any()) @@ -108,8 +110,8 @@ public async Task> UpdateQuantitiesAsync([FromBody] Upd [HttpPost] [Route("items")] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.OK)] public async Task AddBasketItemAsync([FromBody] AddBasketItemRequest data) { if (data == null || data.Quantity == 0) diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Controllers/HomeController.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Controllers/HomeController.cs new file mode 100644 index 0000000000..5328f308d2 --- /dev/null +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Controllers/HomeController.cs @@ -0,0 +1,11 @@ +namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers; + +[Route("")] +public class HomeController : Controller +{ + [HttpGet] + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } +} diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Controllers/OrderController.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Controllers/OrderController.cs index 23c3df40d3..55b4dd70b2 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Controllers/OrderController.cs +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Controllers/OrderController.cs @@ -16,7 +16,8 @@ public OrderController(IBasketService basketService, IOrderingService orderingSe [Route("draft/{basketId}")] [HttpGet] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)] public async Task> GetOrderDraftAsync(string basketId) { if (string.IsNullOrEmpty(basketId)) diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile index 9a6b83be86..294e043758 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile @@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" @@ -32,7 +33,6 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" -COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" @@ -42,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" COPY "docker-compose.dcproj" "docker-compose.dcproj" -COPY "Directory.Packages.props" "Directory.Packages.props" COPY "NuGet.config" "NuGet.config" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile.develop b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile.develop index 22c8c38b70..41361d3a12 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile.develop +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile.develop @@ -6,6 +6,7 @@ EXPOSE 80 WORKDIR /src COPY ["src/ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj", "src/ApiGateways/Mobile.Bff.Shopping/aggregator/"] +COPY ["src/BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj", "src/BuildingBlocks/Devspaces.Support/"] COPY ["src/NuGet.config", "src/NuGet.config"] RUN dotnet restore src/ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj -nowarn:msb3202,nu1503 diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Extensions/Extensions.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Extensions/Extensions.cs deleted file mode 100644 index af98ca5c50..0000000000 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Extensions/Extensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -internal static class Extensions -{ - public static IServiceCollection AddReverseProxy(this IServiceCollection services, IConfiguration configuration) - { - services.AddReverseProxy().LoadFromConfig(configuration.GetRequiredSection("ReverseProxy")); - return services; - } - - public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) - { - services.AddHealthChecks() - .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("CatalogUrlHC")), name: "catalogapi-check", tags: new string[] { "catalogapi" }) - .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("OrderingUrlHC")), name: "orderingapi-check", tags: new string[] { "orderingapi" }) - .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("BasketUrlHC")), name: "basketapi-check", tags: new string[] { "basketapi" }) - .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("IdentityUrlHC")), name: "identityapi-check", tags: new string[] { "identityapi" }); - - return services; - } - - public static IServiceCollection AddApplicationServices(this IServiceCollection services) - { - // Register delegating handlers - services.AddTransient(); - - // Register http services - services.AddHttpClient() - .AddHttpMessageHandler(); - - return services; - } - - public static IServiceCollection AddGrpcServices(this IServiceCollection services) - { - services.AddTransient(); - - services.AddScoped(); - - services.AddGrpcClient((services, options) => - { - var basketApi = services.GetRequiredService>().Value.GrpcBasket; - options.Address = new Uri(basketApi); - }).AddInterceptor(); - - services.AddScoped(); - - services.AddGrpcClient((services, options) => - { - var catalogApi = services.GetRequiredService>().Value.GrpcCatalog; - options.Address = new Uri(catalogApi); - }).AddInterceptor(); - - services.AddScoped(); - - services.AddGrpcClient((services, options) => - { - var orderingApi = services.GetRequiredService>().Value.GrpcOrdering; - options.Address = new Uri(orderingApi); - }).AddInterceptor(); - - return services; - } -} diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Filters/AuthorizeCheckOperationFilter.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Filters/AuthorizeCheckOperationFilter.cs new file mode 100644 index 0000000000..11473d1c1e --- /dev/null +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Filters/AuthorizeCheckOperationFilter.cs @@ -0,0 +1,33 @@ +namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters +{ + namespace Basket.API.Infrastructure.Filters + { + public class AuthorizeCheckOperationFilter : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // Check for authorize attribute + var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType().Any() || + context.MethodInfo.GetCustomAttributes(true).OfType().Any(); + + if (!hasAuthorize) return; + + operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); + + var oAuthScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } + }; + + operation.Security = new List + { + new() + { + [ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" } + } + }; + } + } + } +} diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/GlobalUsings.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/GlobalUsings.cs index 23bb7c6ebf..ecae40e62b 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/GlobalUsings.cs +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/GlobalUsings.cs @@ -1,13 +1,42 @@ -global using System.Text.Json; -global using CatalogApi; -global using Grpc.Core; +global using CatalogApi; +global using Devspaces.Support; global using Grpc.Core.Interceptors; +global using Grpc.Core; global using GrpcBasket; +global using GrpcOrdering; +global using HealthChecks.UI.Client; +global using Microsoft.AspNetCore.Authentication.JwtBearer; +global using Microsoft.AspNetCore.Authentication; global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Diagnostics.HealthChecks; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; global using Microsoft.AspNetCore.Mvc; +global using Microsoft.AspNetCore; global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config; +global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters; global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure; global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; +global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Diagnostics.HealthChecks; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; -global using Services.Common; +global using Microsoft.OpenApi.Models; +global using Serilog; +global using Swashbuckle.AspNetCore.SwaggerGen; +global using System.Collections.Generic; +global using System.IdentityModel.Tokens.Jwt; +global using System.Linq; +global using System.Net.Http.Headers; +global using System.Net.Http; +global using System.Net; +global using System.Text.Json; +global using System.Threading.Tasks; +global using System.Threading; +global using System; +global using Microsoft.IdentityModel.Tokens; \ No newline at end of file diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Infrastructure/GrpcExceptionInterceptor.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Infrastructure/GrpcExceptionInterceptor.cs index ec159a0eae..c434074d3d 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Infrastructure/GrpcExceptionInterceptor.cs +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Infrastructure/GrpcExceptionInterceptor.cs @@ -28,7 +28,7 @@ private async Task HandleResponse(Task t) } catch (RpcException e) { - _logger.LogError(e, "Error calling via gRPC: {Status}", e.Status); + _logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message); return default; } } diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs new file mode 100644 index 0000000000..24914ca330 --- /dev/null +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs @@ -0,0 +1,44 @@ +namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure; + +public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger _logger; + + public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor, ILogger logger) + { + _httpContextAccessor = httpContextAccessor; + _logger = logger; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.Version = new System.Version(2, 0); + request.Method = HttpMethod.Get; + + var authorizationHeader = _httpContextAccessor.HttpContext + .Request.Headers["Authorization"]; + + if (!string.IsNullOrEmpty(authorizationHeader)) + { + request.Headers.Add("Authorization", new List() { authorizationHeader }); + } + + var token = await GetToken(); + + if (token != null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + + return await base.SendAsync(request, cancellationToken); + } + + async Task GetToken() + { + const string ACCESS_TOKEN = "access_token"; + + return await _httpContextAccessor.HttpContext + .GetTokenAsync(ACCESS_TOKEN); + } +} diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj index 7a4b8ed2d3..707bd266a8 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj @@ -5,19 +5,33 @@ Mobile.Shopping.HttpAggregator Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator ..\..\..\docker-compose.dcproj - enable + false + true - - - - - + - + + + + + + + + + + + + + + + + + + diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Models/UpdateBasketItemsRequest.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Models/UpdateBasketItemsRequest.cs index f7e807d8d4..d0686ef516 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Models/UpdateBasketItemsRequest.cs +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Models/UpdateBasketItemsRequest.cs @@ -2,6 +2,7 @@ public class UpdateBasketItemsRequest { + public string BasketId { get; set; } public ICollection Updates { get; set; } diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Program.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Program.cs index 91c5bca6bb..c5d6e10ff9 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Program.cs +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Program.cs @@ -1,24 +1,23 @@ -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); - -builder.Services.AddReverseProxy(builder.Configuration); -builder.Services.AddControllers(); - -builder.Services.AddHealthChecks(builder.Configuration); - -builder.Services.AddApplicationServices(); -builder.Services.AddGrpcServices(); - -builder.Services.Configure(builder.Configuration.GetSection("urls")); - -var app = builder.Build(); - -app.UseServiceDefaults(); - -app.UseHttpsRedirection(); - -app.MapControllers(); -app.MapReverseProxy(); - -await app.RunAsync(); +await BuildWebHost(args).RunAsync(); +IWebHost BuildWebHost(string[] args) => + WebHost + .CreateDefaultBuilder(args) + .ConfigureAppConfiguration(cb => + { + var sources = cb.Sources; + sources.Insert(3, new Microsoft.Extensions.Configuration.Json.JsonConfigurationSource() + { + Optional = true, + Path = "appsettings.localhost.json", + ReloadOnChange = false + }); + }) + .UseStartup() + .UseSerilog((builderContext, config) => + { + config + .MinimumLevel.Information() + .Enrich.FromLogContext() + .WriteTo.Console(); + }) + .Build(); \ No newline at end of file diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Properties/launchSettings.json b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Properties/launchSettings.json index 925e70b0d8..c259d5094b 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Properties/launchSettings.json +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Properties/launchSettings.json @@ -24,6 +24,13 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:61632/" + }, + "Azure Dev Spaces": { + "commandName": "AzureDevSpaces", + "launchBrowser": true, + "resourceGroup": "eshoptestedu", + "aksName": "eshoptestedu", + "subscriptionId": "e3035ac1-c06c-4daf-8939-57b3c5f1f759" } } } \ No newline at end of file diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/IBasketService.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/IBasketService.cs index bf9c1b7e96..e1c9a24bf9 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/IBasketService.cs +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/IBasketService.cs @@ -5,4 +5,5 @@ public interface IBasketService Task GetByIdAsync(string id); Task UpdateAsync(BasketData currentBasket); + } diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/OrderApiClient.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/OrderApiClient.cs index 34d00da5ae..6a8465df63 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/OrderApiClient.cs +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/OrderApiClient.cs @@ -23,6 +23,9 @@ public async Task GetOrderDraftFromBasketAsync(BasketData basket) var ordersDraftResponse = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(ordersDraftResponse, JsonDefaults.CaseInsensitiveOptions); + return JsonSerializer.Deserialize(ordersDraftResponse, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); } } diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/OrderingService.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/OrderingService.cs index 6d46a706f9..2a7de50a7f 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/OrderingService.cs +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Services/OrderingService.cs @@ -2,10 +2,10 @@ public class OrderingService : IOrderingService { - private readonly GrpcOrdering.OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; + private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; private readonly ILogger _logger; - public OrderingService(GrpcOrdering.OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger logger) + public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger logger) { _orderingGrpcClient = orderingGrpcClient; _logger = logger; @@ -48,14 +48,14 @@ private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketDat return data; } - private GrpcOrdering.CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) + private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) { - var command = new GrpcOrdering.CreateOrderDraftCommand + var command = new CreateOrderDraftCommand { BuyerId = basketData.BuyerId, }; - basketData.Items.ForEach(i => command.Items.Add(new GrpcOrdering.BasketItem + basketData.Items.ForEach(i => command.Items.Add(new BasketItem { Id = i.Id, OldUnitPrice = (double)i.OldUnitPrice, diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Startup.cs b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Startup.cs new file mode 100644 index 0000000000..1945cebaaa --- /dev/null +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Startup.cs @@ -0,0 +1,215 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; + +namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy()) + .AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" }) + .AddUrlGroup(new Uri(Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" }) + .AddUrlGroup(new Uri(Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" }) + .AddUrlGroup(new Uri(Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }) + .AddUrlGroup(new Uri(Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" }); + + services.AddCustomMvc(Configuration) + .AddCustomAuthentication(Configuration) + .AddDevspaces() + .AddHttpServices() + .AddGrpcServices(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + var pathBase = Configuration["PATH_BASE"]; + + if (!string.IsNullOrEmpty(pathBase)) + { + loggerFactory.CreateLogger().LogDebug("Using PATH BASE '{pathBase}'", pathBase); + app.UsePathBase(pathBase); + } + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseSwagger().UseSwaggerUI(c => + { + c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Purchase BFF V1"); + + c.OAuthClientId("mobileshoppingaggswaggerui"); + c.OAuthClientSecret(string.Empty); + c.OAuthRealm(string.Empty); + c.OAuthAppName("Purchase BFF Swagger UI"); + }); + + app.UseRouting(); + app.UseCors("CorsPolicy"); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + endpoints.MapControllers(); + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + } +} + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions(); + services.Configure(configuration.GetSection("urls")); + + services.AddControllers() + .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); + + services.AddSwaggerGen(options => + { + //options.DescribeAllEnumsAsStrings(); + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Shopping Aggregator for Mobile Clients", + Version = "v1", + Description = "Shopping Aggregator for Mobile Clients" + }); + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows() + { + Implicit = new OpenApiOAuthFlow() + { + AuthorizationUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/authorize"), + TokenUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/token"), + + Scopes = new Dictionary() + { + { "mobileshoppingagg", "Shopping Aggregator for Mobile Clients" } + } + } + } + }); + + options.OperationFilter(); + }); + + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .AllowAnyMethod() + .AllowAnyHeader() + .SetIsOriginAllowed((host) => true) + .AllowCredentials()); + }); + + return services; + } + public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); + + var identityUrl = configuration.GetValue("urls:identity"); + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + + }) + .AddJwtBearer(options => + { + options.Authority = identityUrl; + options.RequireHttpsMetadata = false; + options.Audience = "mobileshoppingagg"; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false + }; + }); + + return services; + } + public static IServiceCollection AddCustomAuthorization(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthorization(options => + { + options.AddPolicy("ApiScope", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim("scope", "mobileshoppingagg"); + }); + }); + return services; + } + + public static IServiceCollection AddHttpServices(this IServiceCollection services) + { + //register delegating handlers + services.AddTransient(); + services.AddSingleton(); + + //register http services + + services.AddHttpClient() + .AddDevspacesSupport(); + + return services; + } + + public static IServiceCollection AddGrpcServices(this IServiceCollection services) + { + services.AddTransient(); + + services.AddScoped(); + + services.AddGrpcClient((services, options) => + { + var basketApi = services.GetRequiredService>().Value.GrpcBasket; + options.Address = new Uri(basketApi); + }).AddInterceptor(); + + services.AddScoped(); + + services.AddGrpcClient((services, options) => + { + var catalogApi = services.GetRequiredService>().Value.GrpcCatalog; + options.Address = new Uri(catalogApi); + }).AddInterceptor(); + + services.AddScoped(); + + services.AddGrpcClient((services, options) => + { + var orderingApi = services.GetRequiredService>().Value.GrpcOrdering; + options.Address = new Uri(orderingApi); + }).AddInterceptor(); + + return services; + } + +} diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/appsettings.json b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/appsettings.json index 8526211d99..26bb0ac7ac 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/appsettings.json +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/appsettings.json @@ -1,138 +1,15 @@ { "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "System.Net.Http": "Warning" - } - }, - "OpenApi": { - "Endpoint": { - "Name": "Purchase BFF V1" - }, - "Document": { - "Description": "Shopping Aggregator for Mobile Clients", - "Title": "Shopping Aggregator for Mobile Clients", - "Version": "v1" - }, - "Auth": { - "ClientId": "mobileshoppingaggswaggerui", - "AppName": "Mobile shopping BFF Swagger UI" - } - }, - "Identity": { - "Url": "http://localhost:5223", - "Audience": "mobileshoppingagg", - "Scopes": { - "webshoppingagg": "Shopping Aggregator for Mobile Clients" - } - }, - "ReverseProxy": { - "Routes": { - "c-short": { - "ClusterId": "catalog", - "Match": { - "Path": "c/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/c" } - ] - }, - "c-long": { - "ClusterId": "catalog", - "Match": { - "Path": "catalog-api/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/catalog-api" } - ] - }, - "b-short": { - "ClusterId": "basket", - "Match": { - "Path": "b/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/b" } - ] - }, - "b-long": { - "ClusterId": "basket", - "Match": { - "Path": "basket-api/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/basket-api" } - ] - }, - "o-short": { - "ClusterId": "orders", - "Match": { - "Path": "o/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/o" } - ] - }, - "o-long": { - "ClusterId": "orders", - "Match": { - "Path": "ordering-api/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/ordering-api" } - ] - }, - "h-long": { - "ClusterId": "signalr", - "Match": { - "Path": "hub/notificationhub/{**catch-all}" - } + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" } }, - "Clusters": { - "basket": { - "Destinations": { - "destination0": { - "Address": "http://localhost:5221" - } - } - }, - "catalog": { - "Destinations": { - "destination0": { - "Address": "http://localhost:5222" - } - } - }, - "orders": { - "Destinations": { - "destination0": { - "Address": "http://localhost:5224" - } - } - }, - "signalr": { - "Destinations": { - "destination0": { - "Address": "http://localhost:5225" - } - } + "Console": { + "LogLevel": { + "Default": "Warning" } } - }, - "Urls": { - "Basket": "http://localhost:5221", - "Catalog": "http://localhost:5222", - "Orders": "http://localhost:5224", - "Identity": "http://localhost:5223", - "Signalr": "http://localhost:5225", - "GrpcBasket": "http://localhost:6221", - "GrpcCatalog": "http://localhost:6222", - "GrpcOrdering": "http://localhost:6224" - }, - "CatalogUrlHC": "http://localhost:5222/hc", - "OrderingUrlHC": "http://localhost:5224/hc", - "BasketUrlHC": "http://localhost:5221/hc", - "IdentityUrlHC": "http://localhost:5223/hc" + } } diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/appsettings.localhost.json b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/appsettings.localhost.json index ce1d6cfbdf..86fd1541d5 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/appsettings.localhost.json +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/appsettings.localhost.json @@ -8,19 +8,16 @@ "grpcCatalog": "http://localhost:81", "grpcOrdering": "http://localhost:5581" }, - "Identity": { - "ExternalUrl": "http://localhost:5105", - "Url": "http://localhost:5105", - }, + "IdentityUrlExternal": "http://localhost:5105", + "IdentityUrl": "http://localhost:5105", "Logging": { + "IncludeScopes": false, "Debug": { - "IncludeScopes": false, "LogLevel": { "Default": "Debug" } }, "Console": { - "IncludeScopes": false, "LogLevel": { "Default": "Debug" } diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/azds.yaml b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/azds.yaml new file mode 100644 index 0000000000..8dbac7128e --- /dev/null +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/azds.yaml @@ -0,0 +1,55 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: ..\..\..\.. + dockerfile: Dockerfile +install: + chart: ../../../../k8s/helm/mobileshoppingagg + set: + image: + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + # This expands to [space.s.]apigwms...aksapp.io + - $(spacePrefix)eshop$(hostSuffix) + inf: + k8s: + dns: $(spacePrefix)eshop$(hostSuffix) + values: + - values.dev.yaml? + - secrets.dev.yaml? + - app.yaml + - inf.yaml +configurations: + develop: + build: + useGitIgnore: true + dockerfile: Dockerfile.develop + container: + syncTarget: /src + sync: + - '**/Pages/**' + - '**/Views/**' + - '**/wwwroot/**' + - '!**/*.{sln,csproj}' + command: + - dotnet + - run + - --no-restore + - --no-build + - --no-launch-profile + - -c + - ${Configuration:-Debug} + iterate: + processesToKill: + - dotnet + - vsdbg + buildCommands: + - - dotnet + - build + - --no-restore + - -c + - ${Configuration:-Debug} diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/BasketController.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/BasketController.cs index 59550621b4..143ff9a2b0 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/BasketController.cs +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/BasketController.cs @@ -16,7 +16,8 @@ public BasketController(ICatalogService catalogService, IBasketService basketSer [HttpPost] [HttpPut] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] public async Task> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data) { if (data.Items == null || !data.Items.Any()) @@ -73,7 +74,8 @@ public async Task> UpdateAllBasketAsync([FromBody] Upda [HttpPut] [Route("items")] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)] public async Task> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data) { if (!data.Updates.Any()) @@ -107,8 +109,8 @@ public async Task> UpdateQuantitiesAsync([FromBody] Upd [HttpPost] [Route("items")] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.OK)] public async Task AddBasketItemAsync([FromBody] AddBasketItemRequest data) { if (data == null || data.Quantity == 0) diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/HomeController.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/HomeController.cs new file mode 100644 index 0000000000..55df5880b8 --- /dev/null +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/HomeController.cs @@ -0,0 +1,11 @@ +namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers; + +[Route("")] +public class HomeController : Controller +{ + [HttpGet] + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } +} diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/OrderController.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/OrderController.cs index b0b62c1e31..448bbec858 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/OrderController.cs +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Controllers/OrderController.cs @@ -16,7 +16,8 @@ public OrderController(IBasketService basketService, IOrderingService orderingSe [Route("draft/{basketId}")] [HttpGet] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)] public async Task> GetOrderDraftAsync(string basketId) { if (string.IsNullOrWhiteSpace(basketId)) diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile b/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile index e8bd952a24..5cf1c73327 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile @@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" @@ -32,7 +33,6 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" -COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" @@ -42,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" COPY "docker-compose.dcproj" "docker-compose.dcproj" -COPY "Directory.Packages.props" "Directory.Packages.props" COPY "NuGet.config" "NuGet.config" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile.develop b/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile.develop index 26c5b4599f..f3a93760dd 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile.develop +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile.develop @@ -6,6 +6,7 @@ EXPOSE 80 WORKDIR /src COPY ["src/ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj", "src/ApiGateways/Web.Bff.Shopping/aggregator/"] +COPY ["src/BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj", "src/BuildingBlocks/Devspaces.Support/"] COPY ["src/NuGet.config", "src/NuGet.config"] RUN dotnet restore src/ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj -nowarn:msb3202,nu1503 diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Extensions/Extensions.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Extensions/Extensions.cs deleted file mode 100644 index fefb4f3515..0000000000 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Extensions/Extensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -internal static class Extensions -{ - public static IServiceCollection AddReverseProxy(this IServiceCollection services, IConfiguration configuration) - { - services.AddReverseProxy().LoadFromConfig(configuration.GetRequiredSection("ReverseProxy")); - - return services; - } - - public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) - { - services.AddHealthChecks() - .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("CatalogUrlHC")), name: "catalogapi-check", tags: new string[] { "catalogapi" }) - .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("OrderingUrlHC")), name: "orderingapi-check", tags: new string[] { "orderingapi" }) - .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("BasketUrlHC")), name: "basketapi-check", tags: new string[] { "basketapi" }) - .AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("IdentityUrlHC")), name: "identityapi-check", tags: new string[] { "identityapi" }); - - return services; - } - - public static IServiceCollection AddApplicationServices(this IServiceCollection services) - { - // Register delegating handlers - services.AddTransient(); - - // Register http services - services.AddHttpClient() - .AddHttpMessageHandler(); - - return services; - } - - public static IServiceCollection AddGrpcServices(this IServiceCollection services) - { - services.AddTransient(); - - services.AddScoped(); - - services.AddGrpcClient((services, options) => - { - var basketApi = services.GetRequiredService>().Value.GrpcBasket; - options.Address = new Uri(basketApi); - }).AddInterceptor(); - - services.AddScoped(); - - services.AddGrpcClient((services, options) => - { - var catalogApi = services.GetRequiredService>().Value.GrpcCatalog; - options.Address = new Uri(catalogApi); - }).AddInterceptor(); - - services.AddScoped(); - - services.AddGrpcClient((services, options) => - { - var orderingApi = services.GetRequiredService>().Value.GrpcOrdering; - options.Address = new Uri(orderingApi); - }).AddInterceptor(); - - return services; - } -} diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Filters/AuthorizeCheckOperationFilter.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Filters/AuthorizeCheckOperationFilter.cs new file mode 100644 index 0000000000..99bf070487 --- /dev/null +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Filters/AuthorizeCheckOperationFilter.cs @@ -0,0 +1,34 @@ +namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters +{ + namespace Basket.API.Infrastructure.Filters + { + public class AuthorizeCheckOperationFilter : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // Check for authorize attribute + var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType().Any() || + context.MethodInfo.GetCustomAttributes(true).OfType().Any(); + + if (!hasAuthorize) return; + + operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); + + var oAuthScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } + }; + + operation.Security = new List + { + new() + { + [ oAuthScheme ] = new[] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" } + } + }; + } + } + } + +} \ No newline at end of file diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/GlobalUsings.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/GlobalUsings.cs index 0b09dde366..58765400a8 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/GlobalUsings.cs +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/GlobalUsings.cs @@ -1,13 +1,42 @@ -global using System.Text.Json; -global using CatalogApi; -global using Grpc.Core; +global using CatalogApi; +global using Devspaces.Support; global using Grpc.Core.Interceptors; +global using Grpc.Core; global using GrpcBasket; +global using GrpcOrdering; +global using HealthChecks.UI.Client; +global using Microsoft.AspNetCore.Authentication.JwtBearer; +global using Microsoft.AspNetCore.Authentication; global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Diagnostics.HealthChecks; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; global using Microsoft.AspNetCore.Mvc; +global using Microsoft.AspNetCore; global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config; +global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters; global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; +global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Diagnostics.HealthChecks; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; -global using Services.Common; +global using Microsoft.OpenApi.Models; +global using Serilog; +global using Swashbuckle.AspNetCore.SwaggerGen; +global using System.Collections.Generic; +global using System.IdentityModel.Tokens.Jwt; +global using System.Linq; +global using System.Net.Http.Headers; +global using System.Net.Http; +global using System.Net; +global using System.Text.Json; +global using System.Threading.Tasks; +global using System.Threading; +global using System; +global using Microsoft.IdentityModel.Tokens; diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Infrastructure/GrpcExceptionInterceptor.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Infrastructure/GrpcExceptionInterceptor.cs index 9611c6177a..20adb2fc7a 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Infrastructure/GrpcExceptionInterceptor.cs +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Infrastructure/GrpcExceptionInterceptor.cs @@ -28,7 +28,7 @@ private async Task HandleResponse(Task task) } catch (RpcException e) { - _logger.LogError(e, "Error calling via gRPC: {Status}", e.Status); + _logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message); return default; } } diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs new file mode 100644 index 0000000000..d9b3b0ee1a --- /dev/null +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs @@ -0,0 +1,40 @@ +namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; + +public class HttpClientAuthorizationDelegatingHandler + : DelegatingHandler +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var authorizationHeader = _httpContextAccessor.HttpContext + .Request.Headers["Authorization"]; + + if (!string.IsNullOrWhiteSpace(authorizationHeader)) + { + request.Headers.Add("Authorization", new List() { authorizationHeader }); + } + + var token = await GetTokenAsync(); + + if (token != null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + + return await base.SendAsync(request, cancellationToken); + } + + Task GetTokenAsync() + { + const string ACCESS_TOKEN = "access_token"; + + return _httpContextAccessor.HttpContext + .GetTokenAsync(ACCESS_TOKEN); + } +} diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Program.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Program.cs index 6bb3a0d3be..70506fbdaf 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Program.cs +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Program.cs @@ -1,38 +1,24 @@ -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); - -builder.Services.AddReverseProxy(builder.Configuration); -builder.Services.AddControllers(); - -builder.Services.AddHealthChecks(builder.Configuration); -builder.Services.AddCors(options => -{ - // TODO: Read allowed origins from configuration - options.AddPolicy("CorsPolicy", - builder => builder - .SetIsOriginAllowed((host) => true) - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials()); -}); - -builder.Services.AddApplicationServices(); -builder.Services.AddGrpcServices(); - -builder.Services.Configure(builder.Configuration.GetSection("urls")); - -var app = builder.Build(); - -app.UseServiceDefaults(); - -app.UseHttpsRedirection(); - -app.UseCors("CorsPolicy"); -app.UseAuthentication(); -app.UseAuthorization(); - -app.MapControllers(); -app.MapReverseProxy(); - -await app.RunAsync(); +await BuildWebHost(args).RunAsync(); + +IWebHost BuildWebHost(string[] args) => + WebHost + .CreateDefaultBuilder(args) + .ConfigureAppConfiguration(cb => + { + var sources = cb.Sources; + sources.Insert(3, new Microsoft.Extensions.Configuration.Json.JsonConfigurationSource() + { + Optional = true, + Path = "appsettings.localhost.json", + ReloadOnChange = false + }); + }) + .UseStartup() + .UseSerilog((builderContext, config) => + { + config + .MinimumLevel.Information() + .Enrich.FromLogContext() + .WriteTo.Console(); + }) + .Build(); \ No newline at end of file diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Properties/launchSettings.json b/src/ApiGateways/Web.Bff.Shopping/aggregator/Properties/launchSettings.json index 4525154b72..925e70b0d8 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Properties/launchSettings.json +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Properties/launchSettings.json @@ -1,12 +1,29 @@ { + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:57425/", + "sslPort": 0 + } + }, "profiles": { - "Web.Shopping.HttpAggregator": { - "commandName": "Project", + "IIS Express": { + "commandName": "IISExpress", "launchBrowser": true, - "applicationUrl": "http://localhost:5229/", + "launchUrl": "api/values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "PurchaseForMvc": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:61632/" } } } \ No newline at end of file diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/BasketService.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/BasketService.cs index a58c6a9031..41d14d450c 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/BasketService.cs +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/BasketService.cs @@ -10,7 +10,7 @@ public BasketService(Basket.BasketClient basketClient, ILogger lo _basketClient = basketClient; _logger = logger; } - + public async Task GetByIdAsync(string id) { _logger.LogDebug("grpc client created, request = {@id}", id); diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/OrderApiClient.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/OrderApiClient.cs index 9a237e6731..53fb65bb86 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/OrderApiClient.cs +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/OrderApiClient.cs @@ -23,6 +23,9 @@ public async Task GetOrderDraftFromBasketAsync(BasketData basket) var ordersDraftResponse = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(ordersDraftResponse, JsonDefaults.CaseInsensitiveOptions); + return JsonSerializer.Deserialize(ordersDraftResponse, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); } } diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/OrderingService.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/OrderingService.cs index c9398179fb..afa86b31bc 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/OrderingService.cs +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Services/OrderingService.cs @@ -2,10 +2,10 @@ public class OrderingService : IOrderingService { - private readonly GrpcOrdering.OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; + private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; private readonly ILogger _logger; - public OrderingService(GrpcOrdering.OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger logger) + public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger logger) { _orderingGrpcClient = orderingGrpcClient; _logger = logger; @@ -48,14 +48,14 @@ private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketDat return data; } - private GrpcOrdering.CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) + private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) { - var command = new GrpcOrdering.CreateOrderDraftCommand + var command = new CreateOrderDraftCommand { BuyerId = basketData.BuyerId, }; - basketData.Items.ForEach(i => command.Items.Add(new GrpcOrdering.BasketItem + basketData.Items.ForEach(i => command.Items.Add(new BasketItem { Id = i.Id, OldUnitPrice = (double)i.OldUnitPrice, diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Startup.cs b/src/ApiGateways/Web.Bff.Shopping/aggregator/Startup.cs new file mode 100644 index 0000000000..272ab35878 --- /dev/null +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Startup.cs @@ -0,0 +1,201 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; + +namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy()) + .AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" }) + .AddUrlGroup(new Uri(Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" }) + .AddUrlGroup(new Uri(Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" }) + .AddUrlGroup(new Uri(Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }) + .AddUrlGroup(new Uri(Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" }); + + services.AddCustomMvc(Configuration) + .AddCustomAuthentication(Configuration) + //.AddCustomAuthorization(Configuration) + .AddDevspaces() + .AddApplicationServices() + .AddGrpcServices(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + var pathBase = Configuration["PATH_BASE"]; + if (!string.IsNullOrEmpty(pathBase)) + { + loggerFactory.CreateLogger().LogDebug("Using PATH BASE '{pathBase}'", pathBase); + app.UsePathBase(pathBase); + } + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseSwagger().UseSwaggerUI(c => + { + c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Purchase BFF V1"); + + c.OAuthClientId("webshoppingaggswaggerui"); + c.OAuthClientSecret(string.Empty); + c.OAuthRealm(string.Empty); + c.OAuthAppName("web shopping bff Swagger UI"); + }); + + app.UseRouting(); + app.UseCors("CorsPolicy"); + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + endpoints.MapControllers(); + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + } +} + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); + + var identityUrl = configuration.GetValue("urls:identity"); + services.AddAuthentication("Bearer") + .AddJwtBearer(options => + { + options.Authority = identityUrl; + options.RequireHttpsMetadata = false; + options.Audience = "webshoppingagg"; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false + }; + }); + + return services; + } + public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions(); + services.Configure(configuration.GetSection("urls")); + + services.AddControllers() + .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); + + services.AddSwaggerGen(options => + { + //options.DescribeAllEnumsAsStrings(); + + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Shopping Aggregator for Web Clients", + Version = "v1", + Description = "Shopping Aggregator for Web Clients" + }); + + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows() + { + Implicit = new OpenApiOAuthFlow() + { + AuthorizationUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/authorize"), + TokenUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/token"), + + Scopes = new Dictionary() + { + { "webshoppingagg", "Shopping Aggregator for Web Clients" } + } + } + } + }); + + options.OperationFilter(); + }); + + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .SetIsOriginAllowed((host) => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + + return services; + } + public static IServiceCollection AddApplicationServices(this IServiceCollection services) + { + //register delegating handlers + services.AddTransient(); + services.AddSingleton(); + + //register http services + + services.AddHttpClient() + .AddHttpMessageHandler() + .AddDevspacesSupport(); + + return services; + } + + public static IServiceCollection AddGrpcServices(this IServiceCollection services) + { + services.AddTransient(); + + services.AddScoped(); + + services.AddGrpcClient((services, options) => + { + var basketApi = services.GetRequiredService>().Value.GrpcBasket; + options.Address = new Uri(basketApi); + }).AddInterceptor(); + + services.AddScoped(); + + services.AddGrpcClient((services, options) => + { + var catalogApi = services.GetRequiredService>().Value.GrpcCatalog; + options.Address = new Uri(catalogApi); + }).AddInterceptor(); + + services.AddScoped(); + + services.AddGrpcClient((services, options) => + { + var orderingApi = services.GetRequiredService>().Value.GrpcOrdering; + options.Address = new Uri(orderingApi); + }).AddInterceptor(); + + return services; + } +} diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj b/src/ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj index 7417597afd..d8104f2d12 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj @@ -4,20 +4,35 @@ net7.0 Web.Shopping.HttpAggregator Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator - enable ..\..\..\docker-compose.dcproj + false + true - - - - - + - + + + + + + + + + + + + + + + + + + + diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/appsettings.Development.json b/src/ApiGateways/Web.Bff.Shopping/aggregator/appsettings.Development.json index b0bacf4285..19b8c15296 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/appsettings.Development.json +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/appsettings.Development.json @@ -1,8 +1,15 @@ { "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Debug" + } + }, + "Console": { + "LogLevel": { + "Default": "Debug" + } } } } diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/appsettings.json b/src/ApiGateways/Web.Bff.Shopping/aggregator/appsettings.json index 711da61433..26bb0ac7ac 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/appsettings.json +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/appsettings.json @@ -1,138 +1,15 @@ { "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "System.Net.Http": "Warning" - } - }, - "OpenApi": { - "Endpoint": { - "Name": "Purchase BFF V1" - }, - "Document": { - "Description": "Shopping Aggregator for Web Clients", - "Title": "Shopping Aggregator for Web Clients", - "Version": "v1" - }, - "Auth": { - "ClientId": "webshoppingaggswaggerui", - "AppName": "Web Shopping BFF Swagger UI" - } - }, - "Identity": { - "Url": "http://localhost:5223", - "Audience": "webshoppingagg", - "Scopes": { - "webshoppingagg": "Shopping Aggregator for Web Clients" - } - }, - "ReverseProxy": { - "Routes": { - "c-short": { - "ClusterId": "catalog", - "Match": { - "Path": "c/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/c" } - ] - }, - "c-long": { - "ClusterId": "catalog", - "Match": { - "Path": "catalog-api/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/catalog-api" } - ] - }, - "b-short": { - "ClusterId": "basket", - "Match": { - "Path": "b/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/b" } - ] - }, - "b-long": { - "ClusterId": "basket", - "Match": { - "Path": "basket-api/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/basket-api" } - ] - }, - "o-short": { - "ClusterId": "orders", - "Match": { - "Path": "o/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/o" } - ] - }, - "o-long": { - "ClusterId": "orders", - "Match": { - "Path": "ordering-api/{**catch-all}" - }, - "Transforms": [ - { "PathRemovePrefix": "/ordering-api" } - ] - }, - "h-long": { - "ClusterId": "signalr", - "Match": { - "Path": "hub/notificationhub/{**catch-all}" - } + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" } }, - "Clusters": { - "basket": { - "Destinations": { - "destination0": { - "Address": "http://localhost:5221" - } - } - }, - "catalog": { - "Destinations": { - "destination0": { - "Address": "http://localhost:5222" - } - } - }, - "orders": { - "Destinations": { - "destination0": { - "Address": "http://localhost:5224" - } - } - }, - "signalr": { - "Destinations": { - "destination0": { - "Address": "http://localhost:5225" - } - } + "Console": { + "LogLevel": { + "Default": "Warning" } } - }, - "Urls": { - "Basket": "http://localhost:5221", - "Catalog": "http://localhost:5222", - "Orders": "http://localhost:5224", - "Identity": "http://localhost:5223", - "Signalr": "http://localhost:5225", - "GrpcBasket": "http://localhost:6221", - "GrpcCatalog": "http://localhost:6222", - "GrpcOrdering": "http://localhost:6224" - }, - "CatalogUrlHC": "http://localhost:5222/hc", - "OrderingUrlHC": "http://localhost:5224/hc", - "BasketUrlHC": "http://localhost:5221/hc", - "IdentityUrlHC": "http://localhost:5223/hc" + } } diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/appsettings.localhost.json b/src/ApiGateways/Web.Bff.Shopping/aggregator/appsettings.localhost.json new file mode 100644 index 0000000000..055bcfc7fc --- /dev/null +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/appsettings.localhost.json @@ -0,0 +1,11 @@ +{ + "urls": { + "basket": "http://localhost:55103", + "catalog": "http://localhost:55101", + "orders": "http://localhost:55102", + "identity": "http://localhost:55105", + "grpcBasket": "http://localhost:5580", + "grpcCatalog": "http://localhost:81", + "grpcOrdering": "http://localhost:5581" + } +} diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/azds.yaml b/src/ApiGateways/Web.Bff.Shopping/aggregator/azds.yaml new file mode 100644 index 0000000000..189d2261de --- /dev/null +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/azds.yaml @@ -0,0 +1,55 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: ..\..\..\.. + dockerfile: Dockerfile +install: + chart: ../../../../k8s/helm/webshoppingagg + set: + image: + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + # This expands to [space.s.]apigwms...aksapp.io + - $(spacePrefix)eshop$(hostSuffix) + inf: + k8s: + dns: $(spacePrefix)eshop$(hostSuffix) + values: + - values.dev.yaml? + - secrets.dev.yaml? + - app.yaml + - inf.yaml +configurations: + develop: + build: + useGitIgnore: true + dockerfile: Dockerfile.develop + container: + syncTarget: /src + sync: + - '**/Pages/**' + - '**/Views/**' + - '**/wwwroot/**' + - '!**/*.{sln,csproj}' + command: + - dotnet + - run + - --no-restore + - --no-build + - --no-launch-profile + - -c + - ${Configuration:-Debug} + iterate: + processesToKill: + - dotnet + - vsdbg + buildCommands: + - - dotnet + - build + - --no-restore + - -c + - ${Configuration:-Debug} diff --git a/src/BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj b/src/BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj new file mode 100644 index 0000000000..d3b1f90985 --- /dev/null +++ b/src/BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj @@ -0,0 +1,11 @@ + + + + net7.0 + + + + + + + diff --git a/src/BuildingBlocks/Devspaces.Support/DevspacesMessageHandler.cs b/src/BuildingBlocks/Devspaces.Support/DevspacesMessageHandler.cs new file mode 100644 index 0000000000..ac054693fe --- /dev/null +++ b/src/BuildingBlocks/Devspaces.Support/DevspacesMessageHandler.cs @@ -0,0 +1,22 @@ +namespace Devspaces.Support; + +public class DevspacesMessageHandler : DelegatingHandler +{ + private const string DevspacesHeaderName = "azds-route-as"; + private readonly IHttpContextAccessor _httpContextAccessor; + public DevspacesMessageHandler(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var req = _httpContextAccessor.HttpContext.Request; + + if (req.Headers.ContainsKey(DevspacesHeaderName)) + { + request.Headers.Add(DevspacesHeaderName, req.Headers[DevspacesHeaderName] as IEnumerable); + } + return base.SendAsync(request, cancellationToken); + } +} diff --git a/src/BuildingBlocks/Devspaces.Support/GlobalUsings.cs b/src/BuildingBlocks/Devspaces.Support/GlobalUsings.cs new file mode 100644 index 0000000000..06c07e8fbe --- /dev/null +++ b/src/BuildingBlocks/Devspaces.Support/GlobalUsings.cs @@ -0,0 +1,6 @@ +global using Microsoft.AspNetCore.Http; +global using Microsoft.Extensions.DependencyInjection; +global using System.Collections.Generic; +global using System.Net.Http; +global using System.Threading.Tasks; +global using System.Threading; diff --git a/src/BuildingBlocks/Devspaces.Support/HttpClientBuilderDevspacesExtensions.cs b/src/BuildingBlocks/Devspaces.Support/HttpClientBuilderDevspacesExtensions.cs new file mode 100644 index 0000000000..fd78b9a402 --- /dev/null +++ b/src/BuildingBlocks/Devspaces.Support/HttpClientBuilderDevspacesExtensions.cs @@ -0,0 +1,10 @@ +namespace Devspaces.Support; + +public static class HttpClientBuilderDevspacesExtensions +{ + public static IHttpClientBuilder AddDevspacesSupport(this IHttpClientBuilder builder) + { + builder.AddHttpMessageHandler(); + return builder; + } +} diff --git a/src/BuildingBlocks/Devspaces.Support/ServiceCollectionDevspacesExtensions.cs b/src/BuildingBlocks/Devspaces.Support/ServiceCollectionDevspacesExtensions.cs new file mode 100644 index 0000000000..15c3b1515c --- /dev/null +++ b/src/BuildingBlocks/Devspaces.Support/ServiceCollectionDevspacesExtensions.cs @@ -0,0 +1,10 @@ +namespace Devspaces.Support; + +public static class ServiceCollectionDevspacesExtensions +{ + public static IServiceCollection AddDevspaces(this IServiceCollection services) + { + services.AddTransient(); + return services; + } +} diff --git a/src/BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj b/src/BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj index 9e640f87c0..c1c2ae2db5 100644 --- a/src/BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj +++ b/src/BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj @@ -1,19 +1,16 @@  - + net7.0 - enable - false - false - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/BuildingBlocks/EventBus/EventBus.Tests/InMemory_SubscriptionManager_Tests.cs b/src/BuildingBlocks/EventBus/EventBus.Tests/InMemory_SubscriptionManager_Tests.cs index 9b5a30103b..13ae99afa3 100644 --- a/src/BuildingBlocks/EventBus/EventBus.Tests/InMemory_SubscriptionManager_Tests.cs +++ b/src/BuildingBlocks/EventBus/EventBus.Tests/InMemory_SubscriptionManager_Tests.cs @@ -1,4 +1,6 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +using System; +using System.Linq; using Xunit; namespace EventBus.Tests @@ -16,7 +18,7 @@ public void After_Creation_Should_Be_Empty() public void After_One_Event_Subscription_Should_Contain_The_Event() { var manager = new InMemoryEventBusSubscriptionsManager(); - manager.AddSubscription(); + manager.AddSubscription(); Assert.True(manager.HasSubscriptionsForEvent()); } diff --git a/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEvent.cs b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEvent.cs index 63496664ff..4e85e7ae67 100644 --- a/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEvent.cs +++ b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEvent.cs @@ -1,4 +1,7 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using System; +using System.Collections.Generic; +using System.Text; namespace EventBus.Tests { diff --git a/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEventHandler.cs b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEventHandler.cs index 177c1eab76..72e1ed2cd9 100644 --- a/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEventHandler.cs +++ b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEventHandler.cs @@ -1,4 +1,8 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; namespace EventBus.Tests { @@ -11,10 +15,9 @@ public TestIntegrationEventHandler() Handled = false; } - public Task Handle(TestIntegrationEvent @event) + public async Task Handle(TestIntegrationEvent @event) { Handled = true; - return Task.CompletedTask; } } } diff --git a/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationOtherEventHandler.cs b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationOtherEventHandler.cs index 3f529219be..0b5b793ee4 100644 --- a/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationOtherEventHandler.cs +++ b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationOtherEventHandler.cs @@ -1,4 +1,8 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; namespace EventBus.Tests { @@ -11,10 +15,9 @@ public TestIntegrationOtherEventHandler() Handled = false; } - public Task Handle(TestIntegrationEvent @event) + public async Task Handle(TestIntegrationEvent @event) { Handled = true; - return Task.CompletedTask; } } } diff --git a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs index cab6338e86..492a10e423 100644 --- a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs +++ b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs @@ -8,6 +8,12 @@ void Subscribe() where T : IntegrationEvent where TH : IIntegrationEventHandler; + void SubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler; + + void UnsubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler; + void Unsubscribe() where TH : IIntegrationEventHandler where T : IntegrationEvent; diff --git a/src/BuildingBlocks/EventBus/EventBus/EventBus.csproj b/src/BuildingBlocks/EventBus/EventBus/EventBus.csproj index f2f64f36b7..6d33cff346 100644 --- a/src/BuildingBlocks/EventBus/EventBus/EventBus.csproj +++ b/src/BuildingBlocks/EventBus/EventBus/EventBus.csproj @@ -2,7 +2,6 @@ net7.0 - enable Microsoft.eShopOnContainers.BuildingBlocks.EventBus diff --git a/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs b/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs index 5c53b8c48f..31118c4da5 100644 --- a/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs +++ b/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs @@ -1,7 +1,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; public record IntegrationEvent -{ +{ public IntegrationEvent() { Id = Guid.NewGuid(); diff --git a/src/BuildingBlocks/EventBus/EventBus/GlobalUsings.cs b/src/BuildingBlocks/EventBus/EventBus/GlobalUsings.cs index 607805e339..faddc5a133 100644 --- a/src/BuildingBlocks/EventBus/EventBus/GlobalUsings.cs +++ b/src/BuildingBlocks/EventBus/EventBus/GlobalUsings.cs @@ -1,4 +1,8 @@ -global using System.Text.Json.Serialization; -global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; global using static Microsoft.eShopOnContainers.BuildingBlocks.EventBus.InMemoryEventBusSubscriptionsManager; +global using System.Collections.Generic; +global using System.Linq; +global using System.Text.Json.Serialization; +global using System.Threading.Tasks; +global using System; diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersistentConnection.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersistentConnection.cs index 4f3f573f5c..48714cd2f0 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersistentConnection.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersistentConnection.cs @@ -59,7 +59,7 @@ public bool TryConnect() .Or() .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => { - _logger.LogWarning(ex, "RabbitMQ Client could not connect after {TimeOut}s", $"{time.TotalSeconds:n1}"); + _logger.LogWarning(ex, "RabbitMQ Client could not connect after {TimeOut}s ({ExceptionMessage})", $"{time.TotalSeconds:n1}", ex.Message); } ); @@ -81,7 +81,7 @@ public bool TryConnect() } else { - _logger.LogCritical("Fatal error: RabbitMQ connections could not be created and opened"); + _logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened"); return false; } diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs index d27f775e8d..2721bf09ff 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs @@ -1,31 +1,28 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; -using Microsoft.Extensions.DependencyInjection; public class EventBusRabbitMQ : IEventBus, IDisposable { const string BROKER_NAME = "eshop_event_bus"; - - private static readonly JsonSerializerOptions s_indentedOptions = new() { WriteIndented = true }; - private static readonly JsonSerializerOptions s_caseInsensitiveOptions = new() { PropertyNameCaseInsensitive = true }; + const string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; private readonly IRabbitMQPersistentConnection _persistentConnection; private readonly ILogger _logger; private readonly IEventBusSubscriptionsManager _subsManager; - private readonly IServiceProvider _serviceProvider; + private readonly ILifetimeScope _autofac; private readonly int _retryCount; private IModel _consumerChannel; private string _queueName; public EventBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, ILogger logger, - IServiceProvider serviceProvider, IEventBusSubscriptionsManager subsManager, string queueName = null, int retryCount = 5) + ILifetimeScope autofac, IEventBusSubscriptionsManager subsManager, string queueName = null, int retryCount = 5) { _persistentConnection = persistentConnection ?? throw new ArgumentNullException(nameof(persistentConnection)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); _queueName = queueName; _consumerChannel = CreateConsumerChannel(); - _serviceProvider = serviceProvider; + _autofac = autofac; _retryCount = retryCount; _subsManager.OnEventRemoved += SubsManager_OnEventRemoved; } @@ -60,7 +57,7 @@ public void Publish(IntegrationEvent @event) .Or() .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => { - _logger.LogWarning(ex, "Could not publish event: {EventId} after {Timeout}s", @event.Id, $"{time.TotalSeconds:n1}"); + _logger.LogWarning(ex, "Could not publish event: {EventId} after {Timeout}s ({ExceptionMessage})", @event.Id, $"{time.TotalSeconds:n1}", ex.Message); }); var eventName = @event.GetType().Name; @@ -72,14 +69,17 @@ public void Publish(IntegrationEvent @event) channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); - var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), s_indentedOptions); + var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), new JsonSerializerOptions + { + WriteIndented = true + }); policy.Execute(() => { var properties = channel.CreateBasicProperties(); properties.DeliveryMode = 2; // persistent - _logger.LogTrace("Publishing event to RabbitMQ: {EventId}", @event.Id); + _logger.LogTrace("Publishing event to RabbitMQ: {EventId}", @event.Id); channel.BasicPublish( exchange: BROKER_NAME, @@ -122,7 +122,7 @@ private void DoInternalSubscription(string eventName) { _persistentConnection.TryConnect(); } - + _consumerChannel.QueueBind(queue: _queueName, exchange: BROKER_NAME, routingKey: eventName); @@ -193,7 +193,7 @@ private async Task Consumer_Received(object sender, BasicDeliverEventArgs eventA } catch (Exception ex) { - _logger.LogWarning(ex, "Error Processing message \"{Message}\"", message); + _logger.LogWarning(ex, "----- ERROR Processing message \"{Message}\"", message); } // Even on exception we take the message off the queue. @@ -240,23 +240,23 @@ private async Task ProcessEvent(string eventName, string message) if (_subsManager.HasSubscriptionsForEvent(eventName)) { - await using var scope = _serviceProvider.CreateAsyncScope(); + await using var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME); var subscriptions = _subsManager.GetHandlersForEvent(eventName); foreach (var subscription in subscriptions) { if (subscription.IsDynamic) { - if (scope.ServiceProvider.GetService(subscription.HandlerType) is not IDynamicIntegrationEventHandler handler) continue; + if (scope.ResolveOptional(subscription.HandlerType) is not IDynamicIntegrationEventHandler handler) continue; using dynamic eventData = JsonDocument.Parse(message); await Task.Yield(); await handler.Handle(eventData); } else { - var handler = scope.ServiceProvider.GetService(subscription.HandlerType); + var handler = scope.ResolveOptional(subscription.HandlerType); if (handler == null) continue; var eventType = _subsManager.GetEventTypeByName(eventName); - var integrationEvent = JsonSerializer.Deserialize(message, eventType, s_caseInsensitiveOptions); + var integrationEvent = JsonSerializer.Deserialize(message, eventType, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); await Task.Yield(); diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj index d0c268e50d..6a1b9a4771 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj @@ -2,15 +2,15 @@ net7.0 - enable Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ - - - - + + + + + diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/GlobalUsings.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/GlobalUsings.cs index 9e0f245679..6fa5f0bf44 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/GlobalUsings.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/GlobalUsings.cs @@ -1,13 +1,17 @@ -global using System.Net.Sockets; -global using System.Text; -global using System.Text.Json; -global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; -global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; -global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions; -global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Logging; global using Polly; global using Polly.Retry; global using RabbitMQ.Client; global using RabbitMQ.Client.Events; global using RabbitMQ.Client.Exceptions; +global using System; +global using System.IO; +global using System.Net.Sockets; +global using Autofac; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions; +global using System.Text; +global using System.Threading.Tasks; +global using System.Text.Json; diff --git a/src/BuildingBlocks/EventBus/EventBusServiceBus/DefaultServiceBusPersisterConnection.cs b/src/BuildingBlocks/EventBus/EventBusServiceBus/DefaultServiceBusPersisterConnection.cs index 502a8cc8cf..edaed74551 100644 --- a/src/BuildingBlocks/EventBus/EventBusServiceBus/DefaultServiceBusPersisterConnection.cs +++ b/src/BuildingBlocks/EventBus/EventBusServiceBus/DefaultServiceBusPersisterConnection.cs @@ -27,7 +27,7 @@ public ServiceBusClient TopicClient } } - public ServiceBusAdministrationClient AdministrationClient => + public ServiceBusAdministrationClient AdministrationClient => _subscriptionClient; public ServiceBusClient CreateModel() diff --git a/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.cs b/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.cs index 10abbfafc8..b1dce99170 100644 --- a/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.cs +++ b/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; - namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; public class EventBusServiceBus : IEventBus, IAsyncDisposable @@ -7,20 +5,21 @@ public class EventBusServiceBus : IEventBus, IAsyncDisposable private readonly IServiceBusPersisterConnection _serviceBusPersisterConnection; private readonly ILogger _logger; private readonly IEventBusSubscriptionsManager _subsManager; - private readonly IServiceProvider _serviceProvider; + private readonly ILifetimeScope _autofac; private readonly string _topicName = "eshop_event_bus"; private readonly string _subscriptionName; private readonly ServiceBusSender _sender; private readonly ServiceBusProcessor _processor; + private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus"; private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent"; public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection, - ILogger logger, IEventBusSubscriptionsManager subsManager, IServiceProvider serviceProvider, string subscriptionClientName) + ILogger logger, IEventBusSubscriptionsManager subsManager, ILifetimeScope autofac, string subscriptionClientName) { _serviceBusPersisterConnection = serviceBusPersisterConnection; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); - _serviceProvider = serviceProvider; + _autofac = autofac; _subscriptionName = subscriptionClientName; _sender = _serviceBusPersisterConnection.TopicClient.CreateSender(_topicName); ServiceBusProcessorOptions options = new ServiceBusProcessorOptions { MaxConcurrentCalls = 10, AutoCompleteMessages = false }; @@ -140,7 +139,7 @@ private Task ErrorHandler(ProcessErrorEventArgs args) var ex = args.Exception; var context = args.ErrorSource; - _logger.LogError(ex, "Error handling message - Context: {@ExceptionContext}", context); + _logger.LogError(ex, "ERROR handling message: {ExceptionMessage} - Context: {@ExceptionContext}", ex.Message, context); return Task.CompletedTask; } @@ -150,20 +149,20 @@ private async Task ProcessEvent(string eventName, string message) var processed = false; if (_subsManager.HasSubscriptionsForEvent(eventName)) { - await using var scope = _serviceProvider.CreateAsyncScope(); + var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME); var subscriptions = _subsManager.GetHandlersForEvent(eventName); foreach (var subscription in subscriptions) { if (subscription.IsDynamic) { - if (scope.ServiceProvider.GetService(subscription.HandlerType) is not IDynamicIntegrationEventHandler handler) continue; + if (scope.ResolveOptional(subscription.HandlerType) is not IDynamicIntegrationEventHandler handler) continue; using dynamic eventData = JsonDocument.Parse(message); await handler.Handle(eventData); } else { - var handler = scope.ServiceProvider.GetService(subscription.HandlerType); + var handler = scope.ResolveOptional(subscription.HandlerType); if (handler == null) continue; var eventType = _subsManager.GetEventTypeByName(eventName); var integrationEvent = JsonSerializer.Deserialize(message, eventType); @@ -197,4 +196,4 @@ public async ValueTask DisposeAsync() _subsManager.Clear(); await _processor.CloseAsync(); } -} +} \ No newline at end of file diff --git a/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj b/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj index b4c2057414..4b02b0d90d 100644 --- a/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj +++ b/src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj @@ -2,14 +2,14 @@ net7.0 - enable Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus - - - + + + + diff --git a/src/BuildingBlocks/EventBus/EventBusServiceBus/GlobalUsings.cs b/src/BuildingBlocks/EventBus/EventBusServiceBus/GlobalUsings.cs index 7d908e21d8..b0465794fa 100644 --- a/src/BuildingBlocks/EventBus/EventBusServiceBus/GlobalUsings.cs +++ b/src/BuildingBlocks/EventBus/EventBusServiceBus/GlobalUsings.cs @@ -1,11 +1,19 @@ -global using System.Text; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +global using static Microsoft.eShopOnContainers.BuildingBlocks.EventBus.InMemoryEventBusSubscriptionsManager; +global using System.Collections.Generic; +global using System.Linq; +global using System.Text.Json.Serialization; +global using System.Threading.Tasks; +global using System; +global using Autofac; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +global using Microsoft.Extensions.Logging; +global using System.Text; global using System.Text.Json; global using Azure.Messaging.ServiceBus; global using Azure.Messaging.ServiceBus.Administration; -global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; -global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; -global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -global using Microsoft.Extensions.Logging; +global using System; diff --git a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/GlobalUsings.cs b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/GlobalUsings.cs index 6f43fe2feb..866b0cea69 100644 --- a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/GlobalUsings.cs +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/GlobalUsings.cs @@ -1,8 +1,12 @@ -global using System.ComponentModel.DataAnnotations.Schema; -global using System.Data.Common; -global using System.Reflection; -global using System.Text.Json; -global using Microsoft.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore.Metadata.Builders; -global using Microsoft.EntityFrameworkCore.Storage; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +global using System; +global using System.Text.Json; +global using System.ComponentModel.DataAnnotations.Schema; +global using System.Linq; +global using System.Threading.Tasks; +global using Microsoft.EntityFrameworkCore.Storage; +global using System.Collections.Generic; +global using System.Data.Common; +global using System.Reflection; diff --git a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj index 8879349cb8..0343bb20c7 100644 --- a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj @@ -2,18 +2,17 @@ net7.0 - enable Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEntry.cs b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEntry.cs index 70e778e500..90826f22ef 100644 --- a/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEntry.cs +++ b/src/BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEntry.cs @@ -2,16 +2,16 @@ public class IntegrationEventLogEntry { - private static readonly JsonSerializerOptions s_indentedOptions = new() { WriteIndented = true }; - private static readonly JsonSerializerOptions s_caseInsensitiveOptions = new() { PropertyNameCaseInsensitive = true }; - private IntegrationEventLogEntry() { } public IntegrationEventLogEntry(IntegrationEvent @event, Guid transactionId) { EventId = @event.Id; CreationTime = @event.CreationDate; - EventTypeName = @event.GetType().FullName; - Content = JsonSerializer.Serialize(@event, @event.GetType(), s_indentedOptions); + EventTypeName = @event.GetType().FullName; + Content = JsonSerializer.Serialize(@event, @event.GetType(), new JsonSerializerOptions + { + WriteIndented = true + }); State = EventStateEnum.NotPublished; TimesSent = 0; TransactionId = transactionId.ToString(); @@ -29,8 +29,8 @@ public IntegrationEventLogEntry(IntegrationEvent @event, Guid transactionId) public string TransactionId { get; private set; } public IntegrationEventLogEntry DeserializeJsonContent(Type type) - { - IntegrationEvent = JsonSerializer.Deserialize(Content, type, s_caseInsensitiveOptions) as IntegrationEvent; + { + IntegrationEvent = JsonSerializer.Deserialize(Content, type, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }) as IntegrationEvent; return this; } } diff --git a/src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj b/src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj index ed969814ca..c3bad0c69b 100644 --- a/src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj +++ b/src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj @@ -2,17 +2,25 @@ net7.0 - enable + false - - - + + + - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHostExtensions.cs b/src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHostExtensions.cs index 240c405699..3c7fc105a9 100644 --- a/src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHostExtensions.cs +++ b/src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHostExtensions.cs @@ -1,75 +1,77 @@ -using System.Data.SqlClient; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Polly; +using System; +using System.Data.SqlClient; -namespace Microsoft.AspNetCore.Hosting; - -public static class IWebHostExtensions +namespace Microsoft.AspNetCore.Hosting { - public static bool IsInKubernetes(this IServiceProvider services) + public static class IWebHostExtensions { - var cfg = services.GetService(); - var orchestratorType = cfg.GetValue("OrchestratorType"); - return orchestratorType?.ToUpper() == "K8S"; - } - - public static IServiceProvider MigrateDbContext(this IServiceProvider services, Action seeder) where TContext : DbContext - { - var underK8s = services.IsInKubernetes(); - - using var scope = services.CreateScope(); - var scopeServices = scope.ServiceProvider; - var logger = scopeServices.GetRequiredService>(); - var context = scopeServices.GetService(); + public static bool IsInKubernetes(this IWebHost webHost) + { + var cfg = webHost.Services.GetService(); + var orchestratorType = cfg.GetValue("OrchestratorType"); + return orchestratorType?.ToUpper() == "K8S"; + } - try + public static IWebHost MigrateDbContext(this IWebHost webHost, Action seeder) where TContext : DbContext { - logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); + var underK8s = webHost.IsInKubernetes(); + + using var scope = webHost.Services.CreateScope(); + var services = scope.ServiceProvider; + var logger = services.GetRequiredService>(); + var context = services.GetService(); - if (underK8s) + try { - InvokeSeeder(seeder, context, scopeServices); + logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); + + if (underK8s) + { + InvokeSeeder(seeder, context, services); + } + else + { + var retries = 10; + var retry = Policy.Handle() + .WaitAndRetry( + retryCount: retries, + sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + onRetry: (exception, timeSpan, retry, ctx) => + { + logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries); + }); + + //if the sql server container is not created on run docker compose this + //migration can't fail for network related exception. The retry options for DbContext only + //apply to transient exceptions + // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) + retry.Execute(() => InvokeSeeder(seeder, context, services)); + } + + logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); } - else + catch (Exception ex) { - var retries = 10; - var retry = Policy.Handle() - .WaitAndRetry( - retryCount: retries, - sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), - onRetry: (exception, timeSpan, retry, ctx) => - { - logger.LogWarning(exception, "[{prefix}] Error migrating database (attempt {retry} of {retries})", nameof(TContext), retry, retries); - }); - - //if the sql server container is not created on run docker compose this - //migration can't fail for network related exception. The retry options for DbContext only - //apply to transient exceptions - // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) - retry.Execute(() => InvokeSeeder(seeder, context, scopeServices)); + logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); + if (underK8s) + { + throw; // Rethrow under k8s because we rely on k8s to re-run the pod + } } - logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); + return webHost; } - catch (Exception ex) + + private static void InvokeSeeder(Action seeder, TContext context, IServiceProvider services) + where TContext : DbContext { - logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); - if (underK8s) - { - throw; // Rethrow under k8s because we rely on k8s to re-run the pod - } + context.Database.Migrate(); + seeder(context, services); } - - return services; - } - - private static void InvokeSeeder(Action seeder, TContext context, IServiceProvider services) - where TContext : DbContext - { - context.Database.Migrate(); - seeder(context, services); } } diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props deleted file mode 100644 index 2eefee58b3..0000000000 --- a/src/Directory.Packages.props +++ /dev/null @@ -1,93 +0,0 @@ - - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/NuGet.config b/src/NuGet.config index 61b13d16be..f578cf9693 100644 --- a/src/NuGet.config +++ b/src/NuGet.config @@ -1,10 +1,9 @@  + + + - - + - - - - + \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/Auth/Client/enable-token-client.js b/src/Services/Basket/Basket.API/Auth/Client/enable-token-client.js new file mode 100644 index 0000000000..3c207e7eaf --- /dev/null +++ b/src/Services/Basket/Basket.API/Auth/Client/enable-token-client.js @@ -0,0 +1,28 @@ +(function ($, swaggerUi) { + $(function () { + var settings = { + authority: 'https://localhost:5105', + client_id: 'js', + popup_redirect_uri: window.location.protocol + + '//' + + window.location.host + + '/tokenclient/popup.html', + + response_type: 'id_token token', + scope: 'openid profile basket', + + filter_protocol_claims: true + }, + manager = new OidcTokenManager(settings), + $inputApiKey = $('#input_apiKey'); + + $inputApiKey.on('dblclick', function () { + manager.openPopupForTokenAsync() + .then(function () { + $inputApiKey.val(manager.access_token).change(); + }, function (error) { + console.error(error); + }); + }); + }); +})(jQuery, window.swaggerUi); \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/Auth/Client/oidc-token-manager.js b/src/Services/Basket/Basket.API/Auth/Client/oidc-token-manager.js new file mode 100644 index 0000000000..a6f3f29e57 --- /dev/null +++ b/src/Services/Basket/Basket.API/Auth/Client/oidc-token-manager.js @@ -0,0 +1,8896 @@ +(function () { + + // globals + var _promiseFactory; + var _httpRequest; +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +/** + * CryptoJS core components. + */ +var CryptoJS = CryptoJS || (function (Math, undefined) { + /** + * CryptoJS namespace. + */ + var C = {}; + + /** + * Library namespace. + */ + var C_lib = C.lib = {}; + + /** + * Base object for prototypal inheritance. + */ + var Base = C_lib.Base = (function () { + function F() {} + + return { + /** + * Creates a new object that inherits from this object. + * + * @param {Object} overrides Properties to copy into the new object. + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * field: 'value', + * + * method: function () { + * } + * }); + */ + extend: function (overrides) { + // Spawn + F.prototype = this; + var subtype = new F(); + + // Augment + if (overrides) { + subtype.mixIn(overrides); + } + + // Create default initializer + if (!subtype.hasOwnProperty('init')) { + subtype.init = function () { + subtype.$super.init.apply(this, arguments); + }; + } + + // Initializer's prototype is the subtype object + subtype.init.prototype = subtype; + + // Reference supertype + subtype.$super = this; + + return subtype; + }, + + /** + * Extends this object and runs the init method. + * Arguments to create() will be passed to init(). + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var instance = MyType.create(); + */ + create: function () { + var instance = this.extend(); + instance.init.apply(instance, arguments); + + return instance; + }, + + /** + * Initializes a newly created object. + * Override this method to add some logic when your objects are created. + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * init: function () { + * // ... + * } + * }); + */ + init: function () { + }, + + /** + * Copies properties into this object. + * + * @param {Object} properties The properties to mix in. + * + * @example + * + * MyType.mixIn({ + * field: 'value' + * }); + */ + mixIn: function (properties) { + for (var propertyName in properties) { + if (properties.hasOwnProperty(propertyName)) { + this[propertyName] = properties[propertyName]; + } + } + + // IE won't copy toString using the loop above + if (properties.hasOwnProperty('toString')) { + this.toString = properties.toString; + } + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = instance.clone(); + */ + clone: function () { + return this.init.prototype.extend(this); + } + }; + }()); + + /** + * An array of 32-bit words. + * + * @property {Array} words The array of 32-bit words. + * @property {number} sigBytes The number of significant bytes in this word array. + */ + var WordArray = C_lib.WordArray = Base.extend({ + /** + * Initializes a newly created word array. + * + * @param {Array} words (Optional) An array of 32-bit words. + * @param {number} sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.create(); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6); + */ + init: function (words, sigBytes) { + words = this.words = words || []; + + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 4; + } + }, + + /** + * Converts this word array to a string. + * + * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex + * + * @return {string} The stringified word array. + * + * @example + * + * var string = wordArray + ''; + * var string = wordArray.toString(); + * var string = wordArray.toString(CryptoJS.enc.Utf8); + */ + toString: function (encoder) { + return (encoder || Hex).stringify(this); + }, + + /** + * Concatenates a word array to this word array. + * + * @param {WordArray} wordArray The word array to append. + * + * @return {WordArray} This word array. + * + * @example + * + * wordArray1.concat(wordArray2); + */ + concat: function (wordArray) { + // Shortcuts + var thisWords = this.words; + var thatWords = wordArray.words; + var thisSigBytes = this.sigBytes; + var thatSigBytes = wordArray.sigBytes; + + // Clamp excess bits + this.clamp(); + + // Concat + if (thisSigBytes % 4) { + // Copy one byte at a time + for (var i = 0; i < thatSigBytes; i++) { + var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); + } + } else if (thatWords.length > 0xffff) { + // Copy one word at a time + for (var i = 0; i < thatSigBytes; i += 4) { + thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2]; + } + } else { + // Copy all words at once + thisWords.push.apply(thisWords, thatWords); + } + this.sigBytes += thatSigBytes; + + // Chainable + return this; + }, + + /** + * Removes insignificant bits. + * + * @example + * + * wordArray.clamp(); + */ + clamp: function () { + // Shortcuts + var words = this.words; + var sigBytes = this.sigBytes; + + // Clamp + words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); + words.length = Math.ceil(sigBytes / 4); + }, + + /** + * Creates a copy of this word array. + * + * @return {WordArray} The clone. + * + * @example + * + * var clone = wordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone.words = this.words.slice(0); + + return clone; + }, + + /** + * Creates a word array filled with random bytes. + * + * @param {number} nBytes The number of random bytes to generate. + * + * @return {WordArray} The random word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.random(16); + */ + random: function (nBytes) { + var words = []; + for (var i = 0; i < nBytes; i += 4) { + words.push((Math.random() * 0x100000000) | 0); + } + + return new WordArray.init(words, nBytes); + } + }); + + /** + * Encoder namespace. + */ + var C_enc = C.enc = {}; + + /** + * Hex encoding strategy. + */ + var Hex = C_enc.Hex = { + /** + * Converts a word array to a hex string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The hex string. + * + * @static + * + * @example + * + * var hexString = CryptoJS.enc.Hex.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var hexChars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + hexChars.push((bite >>> 4).toString(16)); + hexChars.push((bite & 0x0f).toString(16)); + } + + return hexChars.join(''); + }, + + /** + * Converts a hex string to a word array. + * + * @param {string} hexStr The hex string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Hex.parse(hexString); + */ + parse: function (hexStr) { + // Shortcut + var hexStrLength = hexStr.length; + + // Convert + var words = []; + for (var i = 0; i < hexStrLength; i += 2) { + words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4); + } + + return new WordArray.init(words, hexStrLength / 2); + } + }; + + /** + * Latin1 encoding strategy. + */ + var Latin1 = C_enc.Latin1 = { + /** + * Converts a word array to a Latin1 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The Latin1 string. + * + * @static + * + * @example + * + * var latin1String = CryptoJS.enc.Latin1.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var latin1Chars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + latin1Chars.push(String.fromCharCode(bite)); + } + + return latin1Chars.join(''); + }, + + /** + * Converts a Latin1 string to a word array. + * + * @param {string} latin1Str The Latin1 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Latin1.parse(latin1String); + */ + parse: function (latin1Str) { + // Shortcut + var latin1StrLength = latin1Str.length; + + // Convert + var words = []; + for (var i = 0; i < latin1StrLength; i++) { + words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); + } + + return new WordArray.init(words, latin1StrLength); + } + }; + + /** + * UTF-8 encoding strategy. + */ + var Utf8 = C_enc.Utf8 = { + /** + * Converts a word array to a UTF-8 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-8 string. + * + * @static + * + * @example + * + * var utf8String = CryptoJS.enc.Utf8.stringify(wordArray); + */ + stringify: function (wordArray) { + try { + return decodeURIComponent(escape(Latin1.stringify(wordArray))); + } catch (e) { + throw new Error('Malformed UTF-8 data'); + } + }, + + /** + * Converts a UTF-8 string to a word array. + * + * @param {string} utf8Str The UTF-8 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf8.parse(utf8String); + */ + parse: function (utf8Str) { + return Latin1.parse(unescape(encodeURIComponent(utf8Str))); + } + }; + + /** + * Abstract buffered block algorithm template. + * + * The property blockSize must be implemented in a concrete subtype. + * + * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0 + */ + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({ + /** + * Resets this block algorithm's data buffer to its initial state. + * + * @example + * + * bufferedBlockAlgorithm.reset(); + */ + reset: function () { + // Initial values + this._data = new WordArray.init(); + this._nDataBytes = 0; + }, + + /** + * Adds new data to this block algorithm's buffer. + * + * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8. + * + * @example + * + * bufferedBlockAlgorithm._append('data'); + * bufferedBlockAlgorithm._append(wordArray); + */ + _append: function (data) { + // Convert string to WordArray, else assume WordArray already + if (typeof data == 'string') { + data = Utf8.parse(data); + } + + // Append + this._data.concat(data); + this._nDataBytes += data.sigBytes; + }, + + /** + * Processes available data blocks. + * + * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype. + * + * @param {boolean} doFlush Whether all blocks and partial blocks should be processed. + * + * @return {WordArray} The processed data. + * + * @example + * + * var processedData = bufferedBlockAlgorithm._process(); + * var processedData = bufferedBlockAlgorithm._process(!!'flush'); + */ + _process: function (doFlush) { + // Shortcuts + var data = this._data; + var dataWords = data.words; + var dataSigBytes = data.sigBytes; + var blockSize = this.blockSize; + var blockSizeBytes = blockSize * 4; + + // Count blocks ready + var nBlocksReady = dataSigBytes / blockSizeBytes; + if (doFlush) { + // Round up to include partial blocks + nBlocksReady = Math.ceil(nBlocksReady); + } else { + // Round down to include only full blocks, + // less the number of blocks that must remain in the buffer + nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); + } + + // Count words ready + var nWordsReady = nBlocksReady * blockSize; + + // Count bytes ready + var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes); + + // Process blocks + if (nWordsReady) { + for (var offset = 0; offset < nWordsReady; offset += blockSize) { + // Perform concrete-algorithm logic + this._doProcessBlock(dataWords, offset); + } + + // Remove processed words + var processedWords = dataWords.splice(0, nWordsReady); + data.sigBytes -= nBytesReady; + } + + // Return processed words + return new WordArray.init(processedWords, nBytesReady); + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = bufferedBlockAlgorithm.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone._data = this._data.clone(); + + return clone; + }, + + _minBufferSize: 0 + }); + + /** + * Abstract hasher template. + * + * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits) + */ + var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({ + /** + * Configuration options. + */ + cfg: Base.extend(), + + /** + * Initializes a newly created hasher. + * + * @param {Object} cfg (Optional) The configuration options to use for this hash computation. + * + * @example + * + * var hasher = CryptoJS.algo.SHA256.create(); + */ + init: function (cfg) { + // Apply config defaults + this.cfg = this.cfg.extend(cfg); + + // Set initial values + this.reset(); + }, + + /** + * Resets this hasher to its initial state. + * + * @example + * + * hasher.reset(); + */ + reset: function () { + // Reset data buffer + BufferedBlockAlgorithm.reset.call(this); + + // Perform concrete-hasher logic + this._doReset(); + }, + + /** + * Updates this hasher with a message. + * + * @param {WordArray|string} messageUpdate The message to append. + * + * @return {Hasher} This hasher. + * + * @example + * + * hasher.update('message'); + * hasher.update(wordArray); + */ + update: function (messageUpdate) { + // Append + this._append(messageUpdate); + + // Update the hash + this._process(); + + // Chainable + return this; + }, + + /** + * Finalizes the hash computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} messageUpdate (Optional) A final message update. + * + * @return {WordArray} The hash. + * + * @example + * + * var hash = hasher.finalize(); + * var hash = hasher.finalize('message'); + * var hash = hasher.finalize(wordArray); + */ + finalize: function (messageUpdate) { + // Final message update + if (messageUpdate) { + this._append(messageUpdate); + } + + // Perform concrete-hasher logic + var hash = this._doFinalize(); + + return hash; + }, + + blockSize: 512/32, + + /** + * Creates a shortcut function to a hasher's object interface. + * + * @param {Hasher} hasher The hasher to create a helper for. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256); + */ + _createHelper: function (hasher) { + return function (message, cfg) { + return new hasher.init(cfg).finalize(message); + }; + }, + + /** + * Creates a shortcut function to the HMAC's object interface. + * + * @param {Hasher} hasher The hasher to use in this HMAC helper. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256); + */ + _createHmacHelper: function (hasher) { + return function (message, key) { + return new C_algo.HMAC.init(hasher, key).finalize(message); + }; + } + }); + + /** + * Algorithm namespace. + */ + var C_algo = C.algo = {}; + + return C; +}(Math)); + +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +(function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Reusable object + var W = []; + + /** + * SHA-1 hash algorithm. + */ + var SHA1 = C_algo.SHA1 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0x67452301, 0xefcdab89, + 0x98badcfe, 0x10325476, + 0xc3d2e1f0 + ]); + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var H = this._hash.words; + + // Working variables + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + var e = H[4]; + + // Computation + for (var i = 0; i < 80; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var n = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]; + W[i] = (n << 1) | (n >>> 31); + } + + var t = ((a << 5) | (a >>> 27)) + e + W[i]; + if (i < 20) { + t += ((b & c) | (~b & d)) + 0x5a827999; + } else if (i < 40) { + t += (b ^ c ^ d) + 0x6ed9eba1; + } else if (i < 60) { + t += ((b & c) | (b & d) | (c & d)) - 0x70e44324; + } else /* if (i < 80) */ { + t += (b ^ c ^ d) - 0x359d3e2a; + } + + e = d; + d = c; + c = (b << 30) | (b >>> 2); + b = a; + a = t; + } + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + H[4] = (H[4] + e) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Return final computed hash + return this._hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA1('message'); + * var hash = CryptoJS.SHA1(wordArray); + */ + C.SHA1 = Hasher._createHelper(SHA1); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA1(message, key); + */ + C.HmacSHA1 = Hasher._createHmacHelper(SHA1); +}()); + +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +(function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Initialization and round constants tables + var H = []; + var K = []; + + // Compute constants + (function () { + function isPrime(n) { + var sqrtN = Math.sqrt(n); + for (var factor = 2; factor <= sqrtN; factor++) { + if (!(n % factor)) { + return false; + } + } + + return true; + } + + function getFractionalBits(n) { + return ((n - (n | 0)) * 0x100000000) | 0; + } + + var n = 2; + var nPrime = 0; + while (nPrime < 64) { + if (isPrime(n)) { + if (nPrime < 8) { + H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2)); + } + K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3)); + + nPrime++; + } + + n++; + } + }()); + + // Reusable object + var W = []; + + /** + * SHA-256 hash algorithm. + */ + var SHA256 = C_algo.SHA256 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init(H.slice(0)); + }, + + _doProcessBlock: function (M, offset) { + // Shortcut + var H = this._hash.words; + + // Working variables + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + var e = H[4]; + var f = H[5]; + var g = H[6]; + var h = H[7]; + + // Computation + for (var i = 0; i < 64; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var gamma0x = W[i - 15]; + var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^ + ((gamma0x << 14) | (gamma0x >>> 18)) ^ + (gamma0x >>> 3); + + var gamma1x = W[i - 2]; + var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^ + ((gamma1x << 13) | (gamma1x >>> 19)) ^ + (gamma1x >>> 10); + + W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; + } + + var ch = (e & f) ^ (~e & g); + var maj = (a & b) ^ (a & c) ^ (b & c); + + var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22)); + var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25)); + + var t1 = h + sigma1 + ch + K[i] + W[i]; + var t2 = sigma0 + maj; + + h = g; + g = f; + f = e; + e = (d + t1) | 0; + d = c; + c = b; + b = a; + a = (t1 + t2) | 0; + } + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + H[4] = (H[4] + e) | 0; + H[5] = (H[5] + f) | 0; + H[6] = (H[6] + g) | 0; + H[7] = (H[7] + h) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Return final computed hash + return this._hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA256('message'); + * var hash = CryptoJS.SHA256(wordArray); + */ + C.SHA256 = Hasher._createHelper(SHA256); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA256(message, key); + */ + C.HmacSHA256 = Hasher._createHmacHelper(SHA256); +}(Math)); + +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +(function (undefined) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Base = C_lib.Base; + var X32WordArray = C_lib.WordArray; + + /** + * x64 namespace. + */ + var C_x64 = C.x64 = {}; + + /** + * A 64-bit word. + */ + var X64Word = C_x64.Word = Base.extend({ + /** + * Initializes a newly created 64-bit word. + * + * @param {number} high The high 32 bits. + * @param {number} low The low 32 bits. + * + * @example + * + * var x64Word = CryptoJS.x64.Word.create(0x00010203, 0x04050607); + */ + init: function (high, low) { + this.high = high; + this.low = low; + } + + /** + * Bitwise NOTs this word. + * + * @return {X64Word} A new x64-Word object after negating. + * + * @example + * + * var negated = x64Word.not(); + */ + // not: function () { + // var high = ~this.high; + // var low = ~this.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise ANDs this word with the passed word. + * + * @param {X64Word} word The x64-Word to AND with this word. + * + * @return {X64Word} A new x64-Word object after ANDing. + * + * @example + * + * var anded = x64Word.and(anotherX64Word); + */ + // and: function (word) { + // var high = this.high & word.high; + // var low = this.low & word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise ORs this word with the passed word. + * + * @param {X64Word} word The x64-Word to OR with this word. + * + * @return {X64Word} A new x64-Word object after ORing. + * + * @example + * + * var ored = x64Word.or(anotherX64Word); + */ + // or: function (word) { + // var high = this.high | word.high; + // var low = this.low | word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Bitwise XORs this word with the passed word. + * + * @param {X64Word} word The x64-Word to XOR with this word. + * + * @return {X64Word} A new x64-Word object after XORing. + * + * @example + * + * var xored = x64Word.xor(anotherX64Word); + */ + // xor: function (word) { + // var high = this.high ^ word.high; + // var low = this.low ^ word.low; + + // return X64Word.create(high, low); + // }, + + /** + * Shifts this word n bits to the left. + * + * @param {number} n The number of bits to shift. + * + * @return {X64Word} A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftL(25); + */ + // shiftL: function (n) { + // if (n < 32) { + // var high = (this.high << n) | (this.low >>> (32 - n)); + // var low = this.low << n; + // } else { + // var high = this.low << (n - 32); + // var low = 0; + // } + + // return X64Word.create(high, low); + // }, + + /** + * Shifts this word n bits to the right. + * + * @param {number} n The number of bits to shift. + * + * @return {X64Word} A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftR(7); + */ + // shiftR: function (n) { + // if (n < 32) { + // var low = (this.low >>> n) | (this.high << (32 - n)); + // var high = this.high >>> n; + // } else { + // var low = this.high >>> (n - 32); + // var high = 0; + // } + + // return X64Word.create(high, low); + // }, + + /** + * Rotates this word n bits to the left. + * + * @param {number} n The number of bits to rotate. + * + * @return {X64Word} A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotL(25); + */ + // rotL: function (n) { + // return this.shiftL(n).or(this.shiftR(64 - n)); + // }, + + /** + * Rotates this word n bits to the right. + * + * @param {number} n The number of bits to rotate. + * + * @return {X64Word} A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotR(7); + */ + // rotR: function (n) { + // return this.shiftR(n).or(this.shiftL(64 - n)); + // }, + + /** + * Adds this word with the passed word. + * + * @param {X64Word} word The x64-Word to add with this word. + * + * @return {X64Word} A new x64-Word object after adding. + * + * @example + * + * var added = x64Word.add(anotherX64Word); + */ + // add: function (word) { + // var low = (this.low + word.low) | 0; + // var carry = (low >>> 0) < (this.low >>> 0) ? 1 : 0; + // var high = (this.high + word.high + carry) | 0; + + // return X64Word.create(high, low); + // } + }); + + /** + * An array of 64-bit words. + * + * @property {Array} words The array of CryptoJS.x64.Word objects. + * @property {number} sigBytes The number of significant bytes in this word array. + */ + var X64WordArray = C_x64.WordArray = Base.extend({ + /** + * Initializes a newly created word array. + * + * @param {Array} words (Optional) An array of CryptoJS.x64.Word objects. + * @param {number} sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.x64.WordArray.create(); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ]); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ], 10); + */ + init: function (words, sigBytes) { + words = this.words = words || []; + + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 8; + } + }, + + /** + * Converts this 64-bit word array to a 32-bit word array. + * + * @return {CryptoJS.lib.WordArray} This word array's data as a 32-bit word array. + * + * @example + * + * var x32WordArray = x64WordArray.toX32(); + */ + toX32: function () { + // Shortcuts + var x64Words = this.words; + var x64WordsLength = x64Words.length; + + // Convert + var x32Words = []; + for (var i = 0; i < x64WordsLength; i++) { + var x64Word = x64Words[i]; + x32Words.push(x64Word.high); + x32Words.push(x64Word.low); + } + + return X32WordArray.create(x32Words, this.sigBytes); + }, + + /** + * Creates a copy of this word array. + * + * @return {X64WordArray} The clone. + * + * @example + * + * var clone = x64WordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + + // Clone "words" array + var words = clone.words = this.words.slice(0); + + // Clone each X64Word object + var wordsLength = words.length; + for (var i = 0; i < wordsLength; i++) { + words[i] = words[i].clone(); + } + + return clone; + } + }); +}()); +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +(function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var Hasher = C_lib.Hasher; + var C_x64 = C.x64; + var X64Word = C_x64.Word; + var X64WordArray = C_x64.WordArray; + var C_algo = C.algo; + + function X64Word_create() { + return X64Word.create.apply(X64Word, arguments); + } + + // Constants + var K = [ + X64Word_create(0x428a2f98, 0xd728ae22), X64Word_create(0x71374491, 0x23ef65cd), + X64Word_create(0xb5c0fbcf, 0xec4d3b2f), X64Word_create(0xe9b5dba5, 0x8189dbbc), + X64Word_create(0x3956c25b, 0xf348b538), X64Word_create(0x59f111f1, 0xb605d019), + X64Word_create(0x923f82a4, 0xaf194f9b), X64Word_create(0xab1c5ed5, 0xda6d8118), + X64Word_create(0xd807aa98, 0xa3030242), X64Word_create(0x12835b01, 0x45706fbe), + X64Word_create(0x243185be, 0x4ee4b28c), X64Word_create(0x550c7dc3, 0xd5ffb4e2), + X64Word_create(0x72be5d74, 0xf27b896f), X64Word_create(0x80deb1fe, 0x3b1696b1), + X64Word_create(0x9bdc06a7, 0x25c71235), X64Word_create(0xc19bf174, 0xcf692694), + X64Word_create(0xe49b69c1, 0x9ef14ad2), X64Word_create(0xefbe4786, 0x384f25e3), + X64Word_create(0x0fc19dc6, 0x8b8cd5b5), X64Word_create(0x240ca1cc, 0x77ac9c65), + X64Word_create(0x2de92c6f, 0x592b0275), X64Word_create(0x4a7484aa, 0x6ea6e483), + X64Word_create(0x5cb0a9dc, 0xbd41fbd4), X64Word_create(0x76f988da, 0x831153b5), + X64Word_create(0x983e5152, 0xee66dfab), X64Word_create(0xa831c66d, 0x2db43210), + X64Word_create(0xb00327c8, 0x98fb213f), X64Word_create(0xbf597fc7, 0xbeef0ee4), + X64Word_create(0xc6e00bf3, 0x3da88fc2), X64Word_create(0xd5a79147, 0x930aa725), + X64Word_create(0x06ca6351, 0xe003826f), X64Word_create(0x14292967, 0x0a0e6e70), + X64Word_create(0x27b70a85, 0x46d22ffc), X64Word_create(0x2e1b2138, 0x5c26c926), + X64Word_create(0x4d2c6dfc, 0x5ac42aed), X64Word_create(0x53380d13, 0x9d95b3df), + X64Word_create(0x650a7354, 0x8baf63de), X64Word_create(0x766a0abb, 0x3c77b2a8), + X64Word_create(0x81c2c92e, 0x47edaee6), X64Word_create(0x92722c85, 0x1482353b), + X64Word_create(0xa2bfe8a1, 0x4cf10364), X64Word_create(0xa81a664b, 0xbc423001), + X64Word_create(0xc24b8b70, 0xd0f89791), X64Word_create(0xc76c51a3, 0x0654be30), + X64Word_create(0xd192e819, 0xd6ef5218), X64Word_create(0xd6990624, 0x5565a910), + X64Word_create(0xf40e3585, 0x5771202a), X64Word_create(0x106aa070, 0x32bbd1b8), + X64Word_create(0x19a4c116, 0xb8d2d0c8), X64Word_create(0x1e376c08, 0x5141ab53), + X64Word_create(0x2748774c, 0xdf8eeb99), X64Word_create(0x34b0bcb5, 0xe19b48a8), + X64Word_create(0x391c0cb3, 0xc5c95a63), X64Word_create(0x4ed8aa4a, 0xe3418acb), + X64Word_create(0x5b9cca4f, 0x7763e373), X64Word_create(0x682e6ff3, 0xd6b2b8a3), + X64Word_create(0x748f82ee, 0x5defb2fc), X64Word_create(0x78a5636f, 0x43172f60), + X64Word_create(0x84c87814, 0xa1f0ab72), X64Word_create(0x8cc70208, 0x1a6439ec), + X64Word_create(0x90befffa, 0x23631e28), X64Word_create(0xa4506ceb, 0xde82bde9), + X64Word_create(0xbef9a3f7, 0xb2c67915), X64Word_create(0xc67178f2, 0xe372532b), + X64Word_create(0xca273ece, 0xea26619c), X64Word_create(0xd186b8c7, 0x21c0c207), + X64Word_create(0xeada7dd6, 0xcde0eb1e), X64Word_create(0xf57d4f7f, 0xee6ed178), + X64Word_create(0x06f067aa, 0x72176fba), X64Word_create(0x0a637dc5, 0xa2c898a6), + X64Word_create(0x113f9804, 0xbef90dae), X64Word_create(0x1b710b35, 0x131c471b), + X64Word_create(0x28db77f5, 0x23047d84), X64Word_create(0x32caab7b, 0x40c72493), + X64Word_create(0x3c9ebe0a, 0x15c9bebc), X64Word_create(0x431d67c4, 0x9c100d4c), + X64Word_create(0x4cc5d4be, 0xcb3e42b6), X64Word_create(0x597f299c, 0xfc657e2a), + X64Word_create(0x5fcb6fab, 0x3ad6faec), X64Word_create(0x6c44198c, 0x4a475817) + ]; + + // Reusable objects + var W = []; + (function () { + for (var i = 0; i < 80; i++) { + W[i] = X64Word_create(); + } + }()); + + /** + * SHA-512 hash algorithm. + */ + var SHA512 = C_algo.SHA512 = Hasher.extend({ + _doReset: function () { + this._hash = new X64WordArray.init([ + new X64Word.init(0x6a09e667, 0xf3bcc908), new X64Word.init(0xbb67ae85, 0x84caa73b), + new X64Word.init(0x3c6ef372, 0xfe94f82b), new X64Word.init(0xa54ff53a, 0x5f1d36f1), + new X64Word.init(0x510e527f, 0xade682d1), new X64Word.init(0x9b05688c, 0x2b3e6c1f), + new X64Word.init(0x1f83d9ab, 0xfb41bd6b), new X64Word.init(0x5be0cd19, 0x137e2179) + ]); + }, + + _doProcessBlock: function (M, offset) { + // Shortcuts + var H = this._hash.words; + + var H0 = H[0]; + var H1 = H[1]; + var H2 = H[2]; + var H3 = H[3]; + var H4 = H[4]; + var H5 = H[5]; + var H6 = H[6]; + var H7 = H[7]; + + var H0h = H0.high; + var H0l = H0.low; + var H1h = H1.high; + var H1l = H1.low; + var H2h = H2.high; + var H2l = H2.low; + var H3h = H3.high; + var H3l = H3.low; + var H4h = H4.high; + var H4l = H4.low; + var H5h = H5.high; + var H5l = H5.low; + var H6h = H6.high; + var H6l = H6.low; + var H7h = H7.high; + var H7l = H7.low; + + // Working variables + var ah = H0h; + var al = H0l; + var bh = H1h; + var bl = H1l; + var ch = H2h; + var cl = H2l; + var dh = H3h; + var dl = H3l; + var eh = H4h; + var el = H4l; + var fh = H5h; + var fl = H5l; + var gh = H6h; + var gl = H6l; + var hh = H7h; + var hl = H7l; + + // Rounds + for (var i = 0; i < 80; i++) { + // Shortcut + var Wi = W[i]; + + // Extend message + if (i < 16) { + var Wih = Wi.high = M[offset + i * 2] | 0; + var Wil = Wi.low = M[offset + i * 2 + 1] | 0; + } else { + // Gamma0 + var gamma0x = W[i - 15]; + var gamma0xh = gamma0x.high; + var gamma0xl = gamma0x.low; + var gamma0h = ((gamma0xh >>> 1) | (gamma0xl << 31)) ^ ((gamma0xh >>> 8) | (gamma0xl << 24)) ^ (gamma0xh >>> 7); + var gamma0l = ((gamma0xl >>> 1) | (gamma0xh << 31)) ^ ((gamma0xl >>> 8) | (gamma0xh << 24)) ^ ((gamma0xl >>> 7) | (gamma0xh << 25)); + + // Gamma1 + var gamma1x = W[i - 2]; + var gamma1xh = gamma1x.high; + var gamma1xl = gamma1x.low; + var gamma1h = ((gamma1xh >>> 19) | (gamma1xl << 13)) ^ ((gamma1xh << 3) | (gamma1xl >>> 29)) ^ (gamma1xh >>> 6); + var gamma1l = ((gamma1xl >>> 19) | (gamma1xh << 13)) ^ ((gamma1xl << 3) | (gamma1xh >>> 29)) ^ ((gamma1xl >>> 6) | (gamma1xh << 26)); + + // W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16] + var Wi7 = W[i - 7]; + var Wi7h = Wi7.high; + var Wi7l = Wi7.low; + + var Wi16 = W[i - 16]; + var Wi16h = Wi16.high; + var Wi16l = Wi16.low; + + var Wil = gamma0l + Wi7l; + var Wih = gamma0h + Wi7h + ((Wil >>> 0) < (gamma0l >>> 0) ? 1 : 0); + var Wil = Wil + gamma1l; + var Wih = Wih + gamma1h + ((Wil >>> 0) < (gamma1l >>> 0) ? 1 : 0); + var Wil = Wil + Wi16l; + var Wih = Wih + Wi16h + ((Wil >>> 0) < (Wi16l >>> 0) ? 1 : 0); + + Wi.high = Wih; + Wi.low = Wil; + } + + var chh = (eh & fh) ^ (~eh & gh); + var chl = (el & fl) ^ (~el & gl); + var majh = (ah & bh) ^ (ah & ch) ^ (bh & ch); + var majl = (al & bl) ^ (al & cl) ^ (bl & cl); + + var sigma0h = ((ah >>> 28) | (al << 4)) ^ ((ah << 30) | (al >>> 2)) ^ ((ah << 25) | (al >>> 7)); + var sigma0l = ((al >>> 28) | (ah << 4)) ^ ((al << 30) | (ah >>> 2)) ^ ((al << 25) | (ah >>> 7)); + var sigma1h = ((eh >>> 14) | (el << 18)) ^ ((eh >>> 18) | (el << 14)) ^ ((eh << 23) | (el >>> 9)); + var sigma1l = ((el >>> 14) | (eh << 18)) ^ ((el >>> 18) | (eh << 14)) ^ ((el << 23) | (eh >>> 9)); + + // t1 = h + sigma1 + ch + K[i] + W[i] + var Ki = K[i]; + var Kih = Ki.high; + var Kil = Ki.low; + + var t1l = hl + sigma1l; + var t1h = hh + sigma1h + ((t1l >>> 0) < (hl >>> 0) ? 1 : 0); + var t1l = t1l + chl; + var t1h = t1h + chh + ((t1l >>> 0) < (chl >>> 0) ? 1 : 0); + var t1l = t1l + Kil; + var t1h = t1h + Kih + ((t1l >>> 0) < (Kil >>> 0) ? 1 : 0); + var t1l = t1l + Wil; + var t1h = t1h + Wih + ((t1l >>> 0) < (Wil >>> 0) ? 1 : 0); + + // t2 = sigma0 + maj + var t2l = sigma0l + majl; + var t2h = sigma0h + majh + ((t2l >>> 0) < (sigma0l >>> 0) ? 1 : 0); + + // Update working variables + hh = gh; + hl = gl; + gh = fh; + gl = fl; + fh = eh; + fl = el; + el = (dl + t1l) | 0; + eh = (dh + t1h + ((el >>> 0) < (dl >>> 0) ? 1 : 0)) | 0; + dh = ch; + dl = cl; + ch = bh; + cl = bl; + bh = ah; + bl = al; + al = (t1l + t2l) | 0; + ah = (t1h + t2h + ((al >>> 0) < (t1l >>> 0) ? 1 : 0)) | 0; + } + + // Intermediate hash value + H0l = H0.low = (H0l + al); + H0.high = (H0h + ah + ((H0l >>> 0) < (al >>> 0) ? 1 : 0)); + H1l = H1.low = (H1l + bl); + H1.high = (H1h + bh + ((H1l >>> 0) < (bl >>> 0) ? 1 : 0)); + H2l = H2.low = (H2l + cl); + H2.high = (H2h + ch + ((H2l >>> 0) < (cl >>> 0) ? 1 : 0)); + H3l = H3.low = (H3l + dl); + H3.high = (H3h + dh + ((H3l >>> 0) < (dl >>> 0) ? 1 : 0)); + H4l = H4.low = (H4l + el); + H4.high = (H4h + eh + ((H4l >>> 0) < (el >>> 0) ? 1 : 0)); + H5l = H5.low = (H5l + fl); + H5.high = (H5h + fh + ((H5l >>> 0) < (fl >>> 0) ? 1 : 0)); + H6l = H6.low = (H6l + gl); + H6.high = (H6h + gh + ((H6l >>> 0) < (gl >>> 0) ? 1 : 0)); + H7l = H7.low = (H7l + hl); + H7.high = (H7h + hh + ((H7l >>> 0) < (hl >>> 0) ? 1 : 0)); + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 30] = Math.floor(nBitsTotal / 0x100000000); + dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 31] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + + // Hash final blocks + this._process(); + + // Convert hash to 32-bit word array before returning + var hash = this._hash.toX32(); + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + }, + + blockSize: 1024/32 + }); + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.SHA512('message'); + * var hash = CryptoJS.SHA512(wordArray); + */ + C.SHA512 = Hasher._createHelper(SHA512); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacSHA512(message, key); + */ + C.HmacSHA512 = Hasher._createHmacHelper(SHA512); +}()); + + +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +var b64map="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +var b64pad="="; + +function hex2b64(h) { + var i; + var c; + var ret = ""; + for(i = 0; i+3 <= h.length; i+=3) { + c = parseInt(h.substring(i,i+3),16); + ret += b64map.charAt(c >> 6) + b64map.charAt(c & 63); + } + if(i+1 == h.length) { + c = parseInt(h.substring(i,i+1),16); + ret += b64map.charAt(c << 2); + } + else if(i+2 == h.length) { + c = parseInt(h.substring(i,i+2),16); + ret += b64map.charAt(c >> 2) + b64map.charAt((c & 3) << 4); + } + if (b64pad) while((ret.length & 3) > 0) ret += b64pad; + return ret; +} + +// convert a base64 string to hex +function b64tohex(s) { + var ret = "" + var i; + var k = 0; // b64 state, 0-3 + var slop; + var v; + for(i = 0; i < s.length; ++i) { + if(s.charAt(i) == b64pad) break; + v = b64map.indexOf(s.charAt(i)); + if(v < 0) continue; + if(k == 0) { + ret += int2char(v >> 2); + slop = v & 3; + k = 1; + } + else if(k == 1) { + ret += int2char((slop << 2) | (v >> 4)); + slop = v & 0xf; + k = 2; + } + else if(k == 2) { + ret += int2char(slop); + ret += int2char(v >> 2); + slop = v & 3; + k = 3; + } + else { + ret += int2char((slop << 2) | (v >> 4)); + ret += int2char(v & 0xf); + k = 0; + } + } + if(k == 1) + ret += int2char(slop << 2); + return ret; +} + +// convert a base64 string to a byte/number array +function b64toBA(s) { + //piggyback on b64tohex for now, optimize later + var h = b64tohex(s); + var i; + var a = new Array(); + for(i = 0; 2*i < h.length; ++i) { + a[i] = parseInt(h.substring(2*i,2*i+2),16); + } + return a; +} +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +// Copyright (c) 2005 Tom Wu +// All Rights Reserved. +// See "LICENSE" for details. + +// Basic JavaScript BN library - subset useful for RSA encryption. + +// Bits per digit +var dbits; + +// JavaScript engine analysis +var canary = 0xdeadbeefcafe; +var j_lm = ((canary&0xffffff)==0xefcafe); + +// (public) Constructor +function BigInteger(a,b,c) { + if(a != null) + if("number" == typeof a) this.fromNumber(a,b,c); + else if(b == null && "string" != typeof a) this.fromString(a,256); + else this.fromString(a,b); +} + +// return new, unset BigInteger +function nbi() { return new BigInteger(null); } + +// am: Compute w_j += (x*this_i), propagate carries, +// c is initial carry, returns final carry. +// c < 3*dvalue, x < 2*dvalue, this_i < dvalue +// We need to select the fastest one that works in this environment. + +// am1: use a single mult and divide to get the high bits, +// max digit bits should be 26 because +// max internal value = 2*dvalue^2-2*dvalue (< 2^53) +function am1(i,x,w,j,c,n) { + while(--n >= 0) { + var v = x*this[i++]+w[j]+c; + c = Math.floor(v/0x4000000); + w[j++] = v&0x3ffffff; + } + return c; +} +// am2 avoids a big mult-and-extract completely. +// Max digit bits should be <= 30 because we do bitwise ops +// on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) +function am2(i,x,w,j,c,n) { + var xl = x&0x7fff, xh = x>>15; + while(--n >= 0) { + var l = this[i]&0x7fff; + var h = this[i++]>>15; + var m = xh*l+h*xl; + l = xl*l+((m&0x7fff)<<15)+w[j]+(c&0x3fffffff); + c = (l>>>30)+(m>>>15)+xh*h+(c>>>30); + w[j++] = l&0x3fffffff; + } + return c; +} +// Alternately, set max digit bits to 28 since some +// browsers slow down when dealing with 32-bit numbers. +function am3(i,x,w,j,c,n) { + var xl = x&0x3fff, xh = x>>14; + while(--n >= 0) { + var l = this[i]&0x3fff; + var h = this[i++]>>14; + var m = xh*l+h*xl; + l = xl*l+((m&0x3fff)<<14)+w[j]+c; + c = (l>>28)+(m>>14)+xh*h; + w[j++] = l&0xfffffff; + } + return c; +} +if(j_lm && (navigator.appName == "Microsoft Internet Explorer")) { + BigInteger.prototype.am = am2; + dbits = 30; +} +else if(j_lm && (navigator.appName != "Netscape")) { + BigInteger.prototype.am = am1; + dbits = 26; +} +else { // Mozilla/Netscape seems to prefer am3 + BigInteger.prototype.am = am3; + dbits = 28; +} + +BigInteger.prototype.DB = dbits; +BigInteger.prototype.DM = ((1<= 0; --i) r[i] = this[i]; + r.t = this.t; + r.s = this.s; +} + +// (protected) set from integer value x, -DV <= x < DV +function bnpFromInt(x) { + this.t = 1; + this.s = (x<0)?-1:0; + if(x > 0) this[0] = x; + else if(x < -1) this[0] = x+this.DV; + else this.t = 0; +} + +// return bigint initialized to value +function nbv(i) { var r = nbi(); r.fromInt(i); return r; } + +// (protected) set from string and radix +function bnpFromString(s,b) { + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 256) k = 8; // byte array + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else { this.fromRadix(s,b); return; } + this.t = 0; + this.s = 0; + var i = s.length, mi = false, sh = 0; + while(--i >= 0) { + var x = (k==8)?s[i]&0xff:intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-") mi = true; + continue; + } + mi = false; + if(sh == 0) + this[this.t++] = x; + else if(sh+k > this.DB) { + this[this.t-1] |= (x&((1<<(this.DB-sh))-1))<>(this.DB-sh)); + } + else + this[this.t-1] |= x<= this.DB) sh -= this.DB; + } + if(k == 8 && (s[0]&0x80) != 0) { + this.s = -1; + if(sh > 0) this[this.t-1] |= ((1<<(this.DB-sh))-1)< 0 && this[this.t-1] == c) --this.t; +} + +// (public) return string representation in given radix +function bnToString(b) { + if(this.s < 0) return "-"+this.negate().toString(b); + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else return this.toRadix(b); + var km = (1< 0) { + if(p < this.DB && (d = this[i]>>p) > 0) { m = true; r = int2char(d); } + while(i >= 0) { + if(p < k) { + d = (this[i]&((1<>(p+=this.DB-k); + } + else { + d = (this[i]>>(p-=k))&km; + if(p <= 0) { p += this.DB; --i; } + } + if(d > 0) m = true; + if(m) r += int2char(d); + } + } + return m?r:"0"; +} + +// (public) -this +function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; } + +// (public) |this| +function bnAbs() { return (this.s<0)?this.negate():this; } + +// (public) return + if this > a, - if this < a, 0 if equal +function bnCompareTo(a) { + var r = this.s-a.s; + if(r != 0) return r; + var i = this.t; + r = i-a.t; + if(r != 0) return (this.s<0)?-r:r; + while(--i >= 0) if((r=this[i]-a[i]) != 0) return r; + return 0; +} + +// returns bit length of the integer x +function nbits(x) { + var r = 1, t; + if((t=x>>>16) != 0) { x = t; r += 16; } + if((t=x>>8) != 0) { x = t; r += 8; } + if((t=x>>4) != 0) { x = t; r += 4; } + if((t=x>>2) != 0) { x = t; r += 2; } + if((t=x>>1) != 0) { x = t; r += 1; } + return r; +} + +// (public) return the number of bits in "this" +function bnBitLength() { + if(this.t <= 0) return 0; + return this.DB*(this.t-1)+nbits(this[this.t-1]^(this.s&this.DM)); +} + +// (protected) r = this << n*DB +function bnpDLShiftTo(n,r) { + var i; + for(i = this.t-1; i >= 0; --i) r[i+n] = this[i]; + for(i = n-1; i >= 0; --i) r[i] = 0; + r.t = this.t+n; + r.s = this.s; +} + +// (protected) r = this >> n*DB +function bnpDRShiftTo(n,r) { + for(var i = n; i < this.t; ++i) r[i-n] = this[i]; + r.t = Math.max(this.t-n,0); + r.s = this.s; +} + +// (protected) r = this << n +function bnpLShiftTo(n,r) { + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<= 0; --i) { + r[i+ds+1] = (this[i]>>cbs)|c; + c = (this[i]&bm)<= 0; --i) r[i] = 0; + r[ds] = c; + r.t = this.t+ds+1; + r.s = this.s; + r.clamp(); +} + +// (protected) r = this >> n +function bnpRShiftTo(n,r) { + r.s = this.s; + var ds = Math.floor(n/this.DB); + if(ds >= this.t) { r.t = 0; return; } + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<>bs; + for(var i = ds+1; i < this.t; ++i) { + r[i-ds-1] |= (this[i]&bm)<>bs; + } + if(bs > 0) r[this.t-ds-1] |= (this.s&bm)<>= this.DB; + } + if(a.t < this.t) { + c -= a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c -= a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c -= a.s; + } + r.s = (c<0)?-1:0; + if(c < -1) r[i++] = this.DV+c; + else if(c > 0) r[i++] = c; + r.t = i; + r.clamp(); +} + +// (protected) r = this * a, r != this,a (HAC 14.12) +// "this" should be the larger one if appropriate. +function bnpMultiplyTo(a,r) { + var x = this.abs(), y = a.abs(); + var i = x.t; + r.t = i+y.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < y.t; ++i) r[i+x.t] = x.am(0,y[i],r,i,0,x.t); + r.s = 0; + r.clamp(); + if(this.s != a.s) BigInteger.ZERO.subTo(r,r); +} + +// (protected) r = this^2, r != this (HAC 14.16) +function bnpSquareTo(r) { + var x = this.abs(); + var i = r.t = 2*x.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < x.t-1; ++i) { + var c = x.am(i,x[i],r,2*i,0,1); + if((r[i+x.t]+=x.am(i+1,2*x[i],r,2*i+1,c,x.t-i-1)) >= x.DV) { + r[i+x.t] -= x.DV; + r[i+x.t+1] = 1; + } + } + if(r.t > 0) r[r.t-1] += x.am(i,x[i],r,2*i,0,1); + r.s = 0; + r.clamp(); +} + +// (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) +// r != q, this != m. q or r may be null. +function bnpDivRemTo(m,q,r) { + var pm = m.abs(); + if(pm.t <= 0) return; + var pt = this.abs(); + if(pt.t < pm.t) { + if(q != null) q.fromInt(0); + if(r != null) this.copyTo(r); + return; + } + if(r == null) r = nbi(); + var y = nbi(), ts = this.s, ms = m.s; + var nsh = this.DB-nbits(pm[pm.t-1]); // normalize modulus + if(nsh > 0) { pm.lShiftTo(nsh,y); pt.lShiftTo(nsh,r); } + else { pm.copyTo(y); pt.copyTo(r); } + var ys = y.t; + var y0 = y[ys-1]; + if(y0 == 0) return; + var yt = y0*(1<1)?y[ys-2]>>this.F2:0); + var d1 = this.FV/yt, d2 = (1<= 0) { + r[r.t++] = 1; + r.subTo(t,r); + } + BigInteger.ONE.dlShiftTo(ys,t); + t.subTo(y,y); // "negative" y so we can replace sub with am later + while(y.t < ys) y[y.t++] = 0; + while(--j >= 0) { + // Estimate quotient digit + var qd = (r[--i]==y0)?this.DM:Math.floor(r[i]*d1+(r[i-1]+e)*d2); + if((r[i]+=y.am(0,qd,r,j,0,ys)) < qd) { // Try it out + y.dlShiftTo(j,t); + r.subTo(t,r); + while(r[i] < --qd) r.subTo(t,r); + } + } + if(q != null) { + r.drShiftTo(ys,q); + if(ts != ms) BigInteger.ZERO.subTo(q,q); + } + r.t = ys; + r.clamp(); + if(nsh > 0) r.rShiftTo(nsh,r); // Denormalize remainder + if(ts < 0) BigInteger.ZERO.subTo(r,r); +} + +// (public) this mod a +function bnMod(a) { + var r = nbi(); + this.abs().divRemTo(a,null,r); + if(this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r,r); + return r; +} + +// Modular reduction using "classic" algorithm +function Classic(m) { this.m = m; } +function cConvert(x) { + if(x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); + else return x; +} +function cRevert(x) { return x; } +function cReduce(x) { x.divRemTo(this.m,null,x); } +function cMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } +function cSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +Classic.prototype.convert = cConvert; +Classic.prototype.revert = cRevert; +Classic.prototype.reduce = cReduce; +Classic.prototype.mulTo = cMulTo; +Classic.prototype.sqrTo = cSqrTo; + +// (protected) return "-1/this % 2^DB"; useful for Mont. reduction +// justification: +// xy == 1 (mod m) +// xy = 1+km +// xy(2-xy) = (1+km)(1-km) +// x[y(2-xy)] = 1-k^2m^2 +// x[y(2-xy)] == 1 (mod m^2) +// if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 +// should reduce x and y(2-xy) by m^2 at each step to keep size bounded. +// JS multiply "overflows" differently from C/C++, so care is needed here. +function bnpInvDigit() { + if(this.t < 1) return 0; + var x = this[0]; + if((x&1) == 0) return 0; + var y = x&3; // y == 1/x mod 2^2 + y = (y*(2-(x&0xf)*y))&0xf; // y == 1/x mod 2^4 + y = (y*(2-(x&0xff)*y))&0xff; // y == 1/x mod 2^8 + y = (y*(2-(((x&0xffff)*y)&0xffff)))&0xffff; // y == 1/x mod 2^16 + // last step - calculate inverse mod DV directly; + // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints + y = (y*(2-x*y%this.DV))%this.DV; // y == 1/x mod 2^dbits + // we really want the negative inverse, and -DV < y < DV + return (y>0)?this.DV-y:-y; +} + +// Montgomery reduction +function Montgomery(m) { + this.m = m; + this.mp = m.invDigit(); + this.mpl = this.mp&0x7fff; + this.mph = this.mp>>15; + this.um = (1<<(m.DB-15))-1; + this.mt2 = 2*m.t; +} + +// xR mod m +function montConvert(x) { + var r = nbi(); + x.abs().dlShiftTo(this.m.t,r); + r.divRemTo(this.m,null,r); + if(x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r,r); + return r; +} + +// x/R mod m +function montRevert(x) { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; +} + +// x = x/R mod m (HAC 14.32) +function montReduce(x) { + while(x.t <= this.mt2) // pad x so am has enough room later + x[x.t++] = 0; + for(var i = 0; i < this.m.t; ++i) { + // faster way of calculating u0 = x[i]*mp mod DV + var j = x[i]&0x7fff; + var u0 = (j*this.mpl+(((j*this.mph+(x[i]>>15)*this.mpl)&this.um)<<15))&x.DM; + // use am to combine the multiply-shift-add into one call + j = i+this.m.t; + x[j] += this.m.am(0,u0,x,i,0,this.m.t); + // propagate carry + while(x[j] >= x.DV) { x[j] -= x.DV; x[++j]++; } + } + x.clamp(); + x.drShiftTo(this.m.t,x); + if(x.compareTo(this.m) >= 0) x.subTo(this.m,x); +} + +// r = "x^2/R mod m"; x != r +function montSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +// r = "xy/R mod m"; x,y != r +function montMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + +Montgomery.prototype.convert = montConvert; +Montgomery.prototype.revert = montRevert; +Montgomery.prototype.reduce = montReduce; +Montgomery.prototype.mulTo = montMulTo; +Montgomery.prototype.sqrTo = montSqrTo; + +// (protected) true iff this is even +function bnpIsEven() { return ((this.t>0)?(this[0]&1):this.s) == 0; } + +// (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) +function bnpExp(e,z) { + if(e > 0xffffffff || e < 1) return BigInteger.ONE; + var r = nbi(), r2 = nbi(), g = z.convert(this), i = nbits(e)-1; + g.copyTo(r); + while(--i >= 0) { + z.sqrTo(r,r2); + if((e&(1< 0) z.mulTo(r2,g,r); + else { var t = r; r = r2; r2 = t; } + } + return z.revert(r); +} + +// (public) this^e % m, 0 <= e < 2^32 +function bnModPowInt(e,m) { + var z; + if(e < 256 || m.isEven()) z = new Classic(m); else z = new Montgomery(m); + return this.exp(e,z); +} + +// protected +BigInteger.prototype.copyTo = bnpCopyTo; +BigInteger.prototype.fromInt = bnpFromInt; +BigInteger.prototype.fromString = bnpFromString; +BigInteger.prototype.clamp = bnpClamp; +BigInteger.prototype.dlShiftTo = bnpDLShiftTo; +BigInteger.prototype.drShiftTo = bnpDRShiftTo; +BigInteger.prototype.lShiftTo = bnpLShiftTo; +BigInteger.prototype.rShiftTo = bnpRShiftTo; +BigInteger.prototype.subTo = bnpSubTo; +BigInteger.prototype.multiplyTo = bnpMultiplyTo; +BigInteger.prototype.squareTo = bnpSquareTo; +BigInteger.prototype.divRemTo = bnpDivRemTo; +BigInteger.prototype.invDigit = bnpInvDigit; +BigInteger.prototype.isEven = bnpIsEven; +BigInteger.prototype.exp = bnpExp; + +// public +BigInteger.prototype.toString = bnToString; +BigInteger.prototype.negate = bnNegate; +BigInteger.prototype.abs = bnAbs; +BigInteger.prototype.compareTo = bnCompareTo; +BigInteger.prototype.bitLength = bnBitLength; +BigInteger.prototype.mod = bnMod; +BigInteger.prototype.modPowInt = bnModPowInt; + +// "constants" +BigInteger.ZERO = nbv(0); +BigInteger.ONE = nbv(1); +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +// Copyright (c) 2005-2009 Tom Wu +// All Rights Reserved. +// See "LICENSE" for details. + +// Extended JavaScript BN functions, required for RSA private ops. + +// Version 1.1: new BigInteger("0", 10) returns "proper" zero +// Version 1.2: square() API, isProbablePrime fix + +// (public) +function bnClone() { var r = nbi(); this.copyTo(r); return r; } + +// (public) return value as integer +function bnIntValue() { + if(this.s < 0) { + if(this.t == 1) return this[0]-this.DV; + else if(this.t == 0) return -1; + } + else if(this.t == 1) return this[0]; + else if(this.t == 0) return 0; + // assumes 16 < DB < 32 + return ((this[1]&((1<<(32-this.DB))-1))<>24; } + +// (public) return value as short (assumes DB>=16) +function bnShortValue() { return (this.t==0)?this.s:(this[0]<<16)>>16; } + +// (protected) return x s.t. r^x < DV +function bnpChunkSize(r) { return Math.floor(Math.LN2*this.DB/Math.log(r)); } + +// (public) 0 if this == 0, 1 if this > 0 +function bnSigNum() { + if(this.s < 0) return -1; + else if(this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0; + else return 1; +} + +// (protected) convert to radix string +function bnpToRadix(b) { + if(b == null) b = 10; + if(this.signum() == 0 || b < 2 || b > 36) return "0"; + var cs = this.chunkSize(b); + var a = Math.pow(b,cs); + var d = nbv(a), y = nbi(), z = nbi(), r = ""; + this.divRemTo(d,y,z); + while(y.signum() > 0) { + r = (a+z.intValue()).toString(b).substr(1) + r; + y.divRemTo(d,y,z); + } + return z.intValue().toString(b) + r; +} + +// (protected) convert from radix string +function bnpFromRadix(s,b) { + this.fromInt(0); + if(b == null) b = 10; + var cs = this.chunkSize(b); + var d = Math.pow(b,cs), mi = false, j = 0, w = 0; + for(var i = 0; i < s.length; ++i) { + var x = intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-" && this.signum() == 0) mi = true; + continue; + } + w = b*w+x; + if(++j >= cs) { + this.dMultiply(d); + this.dAddOffset(w,0); + j = 0; + w = 0; + } + } + if(j > 0) { + this.dMultiply(Math.pow(b,j)); + this.dAddOffset(w,0); + } + if(mi) BigInteger.ZERO.subTo(this,this); +} + +// (protected) alternate constructor +function bnpFromNumber(a,b,c) { + if("number" == typeof b) { + // new BigInteger(int,int,RNG) + if(a < 2) this.fromInt(1); + else { + this.fromNumber(a,c); + if(!this.testBit(a-1)) // force MSB set + this.bitwiseTo(BigInteger.ONE.shiftLeft(a-1),op_or,this); + if(this.isEven()) this.dAddOffset(1,0); // force odd + while(!this.isProbablePrime(b)) { + this.dAddOffset(2,0); + if(this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a-1),this); + } + } + } + else { + // new BigInteger(int,RNG) + var x = new Array(), t = a&7; + x.length = (a>>3)+1; + b.nextBytes(x); + if(t > 0) x[0] &= ((1< 0) { + if(p < this.DB && (d = this[i]>>p) != (this.s&this.DM)>>p) + r[k++] = d|(this.s<<(this.DB-p)); + while(i >= 0) { + if(p < 8) { + d = (this[i]&((1<>(p+=this.DB-8); + } + else { + d = (this[i]>>(p-=8))&0xff; + if(p <= 0) { p += this.DB; --i; } + } + if((d&0x80) != 0) d |= -256; + if(k == 0 && (this.s&0x80) != (d&0x80)) ++k; + if(k > 0 || d != this.s) r[k++] = d; + } + } + return r; +} + +function bnEquals(a) { return(this.compareTo(a)==0); } +function bnMin(a) { return(this.compareTo(a)<0)?this:a; } +function bnMax(a) { return(this.compareTo(a)>0)?this:a; } + +// (protected) r = this op a (bitwise) +function bnpBitwiseTo(a,op,r) { + var i, f, m = Math.min(a.t,this.t); + for(i = 0; i < m; ++i) r[i] = op(this[i],a[i]); + if(a.t < this.t) { + f = a.s&this.DM; + for(i = m; i < this.t; ++i) r[i] = op(this[i],f); + r.t = this.t; + } + else { + f = this.s&this.DM; + for(i = m; i < a.t; ++i) r[i] = op(f,a[i]); + r.t = a.t; + } + r.s = op(this.s,a.s); + r.clamp(); +} + +// (public) this & a +function op_and(x,y) { return x&y; } +function bnAnd(a) { var r = nbi(); this.bitwiseTo(a,op_and,r); return r; } + +// (public) this | a +function op_or(x,y) { return x|y; } +function bnOr(a) { var r = nbi(); this.bitwiseTo(a,op_or,r); return r; } + +// (public) this ^ a +function op_xor(x,y) { return x^y; } +function bnXor(a) { var r = nbi(); this.bitwiseTo(a,op_xor,r); return r; } + +// (public) this & ~a +function op_andnot(x,y) { return x&~y; } +function bnAndNot(a) { var r = nbi(); this.bitwiseTo(a,op_andnot,r); return r; } + +// (public) ~this +function bnNot() { + var r = nbi(); + for(var i = 0; i < this.t; ++i) r[i] = this.DM&~this[i]; + r.t = this.t; + r.s = ~this.s; + return r; +} + +// (public) this << n +function bnShiftLeft(n) { + var r = nbi(); + if(n < 0) this.rShiftTo(-n,r); else this.lShiftTo(n,r); + return r; +} + +// (public) this >> n +function bnShiftRight(n) { + var r = nbi(); + if(n < 0) this.lShiftTo(-n,r); else this.rShiftTo(n,r); + return r; +} + +// return index of lowest 1-bit in x, x < 2^31 +function lbit(x) { + if(x == 0) return -1; + var r = 0; + if((x&0xffff) == 0) { x >>= 16; r += 16; } + if((x&0xff) == 0) { x >>= 8; r += 8; } + if((x&0xf) == 0) { x >>= 4; r += 4; } + if((x&3) == 0) { x >>= 2; r += 2; } + if((x&1) == 0) ++r; + return r; +} + +// (public) returns index of lowest 1-bit (or -1 if none) +function bnGetLowestSetBit() { + for(var i = 0; i < this.t; ++i) + if(this[i] != 0) return i*this.DB+lbit(this[i]); + if(this.s < 0) return this.t*this.DB; + return -1; +} + +// return number of 1 bits in x +function cbit(x) { + var r = 0; + while(x != 0) { x &= x-1; ++r; } + return r; +} + +// (public) return number of set bits +function bnBitCount() { + var r = 0, x = this.s&this.DM; + for(var i = 0; i < this.t; ++i) r += cbit(this[i]^x); + return r; +} + +// (public) true iff nth bit is set +function bnTestBit(n) { + var j = Math.floor(n/this.DB); + if(j >= this.t) return(this.s!=0); + return((this[j]&(1<<(n%this.DB)))!=0); +} + +// (protected) this op (1<>= this.DB; + } + if(a.t < this.t) { + c += a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c += a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += a.s; + } + r.s = (c<0)?-1:0; + if(c > 0) r[i++] = c; + else if(c < -1) r[i++] = this.DV+c; + r.t = i; + r.clamp(); +} + +// (public) this + a +function bnAdd(a) { var r = nbi(); this.addTo(a,r); return r; } + +// (public) this - a +function bnSubtract(a) { var r = nbi(); this.subTo(a,r); return r; } + +// (public) this * a +function bnMultiply(a) { var r = nbi(); this.multiplyTo(a,r); return r; } + +// (public) this^2 +function bnSquare() { var r = nbi(); this.squareTo(r); return r; } + +// (public) this / a +function bnDivide(a) { var r = nbi(); this.divRemTo(a,r,null); return r; } + +// (public) this % a +function bnRemainder(a) { var r = nbi(); this.divRemTo(a,null,r); return r; } + +// (public) [this/a,this%a] +function bnDivideAndRemainder(a) { + var q = nbi(), r = nbi(); + this.divRemTo(a,q,r); + return new Array(q,r); +} + +// (protected) this *= n, this >= 0, 1 < n < DV +function bnpDMultiply(n) { + this[this.t] = this.am(0,n-1,this,0,0,this.t); + ++this.t; + this.clamp(); +} + +// (protected) this += n << w words, this >= 0 +function bnpDAddOffset(n,w) { + if(n == 0) return; + while(this.t <= w) this[this.t++] = 0; + this[w] += n; + while(this[w] >= this.DV) { + this[w] -= this.DV; + if(++w >= this.t) this[this.t++] = 0; + ++this[w]; + } +} + +// A "null" reducer +function NullExp() {} +function nNop(x) { return x; } +function nMulTo(x,y,r) { x.multiplyTo(y,r); } +function nSqrTo(x,r) { x.squareTo(r); } + +NullExp.prototype.convert = nNop; +NullExp.prototype.revert = nNop; +NullExp.prototype.mulTo = nMulTo; +NullExp.prototype.sqrTo = nSqrTo; + +// (public) this^e +function bnPow(e) { return this.exp(e,new NullExp()); } + +// (protected) r = lower n words of "this * a", a.t <= n +// "this" should be the larger one if appropriate. +function bnpMultiplyLowerTo(a,n,r) { + var i = Math.min(this.t+a.t,n); + r.s = 0; // assumes a,this >= 0 + r.t = i; + while(i > 0) r[--i] = 0; + var j; + for(j = r.t-this.t; i < j; ++i) r[i+this.t] = this.am(0,a[i],r,i,0,this.t); + for(j = Math.min(a.t,n); i < j; ++i) this.am(0,a[i],r,i,0,n-i); + r.clamp(); +} + +// (protected) r = "this * a" without lower n words, n > 0 +// "this" should be the larger one if appropriate. +function bnpMultiplyUpperTo(a,n,r) { + --n; + var i = r.t = this.t+a.t-n; + r.s = 0; // assumes a,this >= 0 + while(--i >= 0) r[i] = 0; + for(i = Math.max(n-this.t,0); i < a.t; ++i) + r[this.t+i-n] = this.am(n-i,a[i],r,0,0,this.t+i-n); + r.clamp(); + r.drShiftTo(1,r); +} + +// Barrett modular reduction +function Barrett(m) { + // setup Barrett + this.r2 = nbi(); + this.q3 = nbi(); + BigInteger.ONE.dlShiftTo(2*m.t,this.r2); + this.mu = this.r2.divide(m); + this.m = m; +} + +function barrettConvert(x) { + if(x.s < 0 || x.t > 2*this.m.t) return x.mod(this.m); + else if(x.compareTo(this.m) < 0) return x; + else { var r = nbi(); x.copyTo(r); this.reduce(r); return r; } +} + +function barrettRevert(x) { return x; } + +// x = x mod m (HAC 14.42) +function barrettReduce(x) { + x.drShiftTo(this.m.t-1,this.r2); + if(x.t > this.m.t+1) { x.t = this.m.t+1; x.clamp(); } + this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3); + this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2); + while(x.compareTo(this.r2) < 0) x.dAddOffset(1,this.m.t+1); + x.subTo(this.r2,x); + while(x.compareTo(this.m) >= 0) x.subTo(this.m,x); +} + +// r = x^2 mod m; x != r +function barrettSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +// r = x*y mod m; x,y != r +function barrettMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + +Barrett.prototype.convert = barrettConvert; +Barrett.prototype.revert = barrettRevert; +Barrett.prototype.reduce = barrettReduce; +Barrett.prototype.mulTo = barrettMulTo; +Barrett.prototype.sqrTo = barrettSqrTo; + +// (public) this^e % m (HAC 14.85) +function bnModPow(e,m) { + var i = e.bitLength(), k, r = nbv(1), z; + if(i <= 0) return r; + else if(i < 18) k = 1; + else if(i < 48) k = 3; + else if(i < 144) k = 4; + else if(i < 768) k = 5; + else k = 6; + if(i < 8) + z = new Classic(m); + else if(m.isEven()) + z = new Barrett(m); + else + z = new Montgomery(m); + + // precomputation + var g = new Array(), n = 3, k1 = k-1, km = (1< 1) { + var g2 = nbi(); + z.sqrTo(g[1],g2); + while(n <= km) { + g[n] = nbi(); + z.mulTo(g2,g[n-2],g[n]); + n += 2; + } + } + + var j = e.t-1, w, is1 = true, r2 = nbi(), t; + i = nbits(e[j])-1; + while(j >= 0) { + if(i >= k1) w = (e[j]>>(i-k1))&km; + else { + w = (e[j]&((1<<(i+1))-1))<<(k1-i); + if(j > 0) w |= e[j-1]>>(this.DB+i-k1); + } + + n = k; + while((w&1) == 0) { w >>= 1; --n; } + if((i -= n) < 0) { i += this.DB; --j; } + if(is1) { // ret == 1, don't bother squaring or multiplying it + g[w].copyTo(r); + is1 = false; + } + else { + while(n > 1) { z.sqrTo(r,r2); z.sqrTo(r2,r); n -= 2; } + if(n > 0) z.sqrTo(r,r2); else { t = r; r = r2; r2 = t; } + z.mulTo(r2,g[w],r); + } + + while(j >= 0 && (e[j]&(1< 0) { + x.rShiftTo(g,x); + y.rShiftTo(g,y); + } + while(x.signum() > 0) { + if((i = x.getLowestSetBit()) > 0) x.rShiftTo(i,x); + if((i = y.getLowestSetBit()) > 0) y.rShiftTo(i,y); + if(x.compareTo(y) >= 0) { + x.subTo(y,x); + x.rShiftTo(1,x); + } + else { + y.subTo(x,y); + y.rShiftTo(1,y); + } + } + if(g > 0) y.lShiftTo(g,y); + return y; +} + +// (protected) this % n, n < 2^26 +function bnpModInt(n) { + if(n <= 0) return 0; + var d = this.DV%n, r = (this.s<0)?n-1:0; + if(this.t > 0) + if(d == 0) r = this[0]%n; + else for(var i = this.t-1; i >= 0; --i) r = (d*r+this[i])%n; + return r; +} + +// (public) 1/this % m (HAC 14.61) +function bnModInverse(m) { + var ac = m.isEven(); + if((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO; + var u = m.clone(), v = this.clone(); + var a = nbv(1), b = nbv(0), c = nbv(0), d = nbv(1); + while(u.signum() != 0) { + while(u.isEven()) { + u.rShiftTo(1,u); + if(ac) { + if(!a.isEven() || !b.isEven()) { a.addTo(this,a); b.subTo(m,b); } + a.rShiftTo(1,a); + } + else if(!b.isEven()) b.subTo(m,b); + b.rShiftTo(1,b); + } + while(v.isEven()) { + v.rShiftTo(1,v); + if(ac) { + if(!c.isEven() || !d.isEven()) { c.addTo(this,c); d.subTo(m,d); } + c.rShiftTo(1,c); + } + else if(!d.isEven()) d.subTo(m,d); + d.rShiftTo(1,d); + } + if(u.compareTo(v) >= 0) { + u.subTo(v,u); + if(ac) a.subTo(c,a); + b.subTo(d,b); + } + else { + v.subTo(u,v); + if(ac) c.subTo(a,c); + d.subTo(b,d); + } + } + if(v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; + if(d.compareTo(m) >= 0) return d.subtract(m); + if(d.signum() < 0) d.addTo(m,d); else return d; + if(d.signum() < 0) return d.add(m); else return d; +} + +var lowprimes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997]; +var lplim = (1<<26)/lowprimes[lowprimes.length-1]; + +// (public) test primality with certainty >= 1-.5^t +function bnIsProbablePrime(t) { + var i, x = this.abs(); + if(x.t == 1 && x[0] <= lowprimes[lowprimes.length-1]) { + for(i = 0; i < lowprimes.length; ++i) + if(x[0] == lowprimes[i]) return true; + return false; + } + if(x.isEven()) return false; + i = 1; + while(i < lowprimes.length) { + var m = lowprimes[i], j = i+1; + while(j < lowprimes.length && m < lplim) m *= lowprimes[j++]; + m = x.modInt(m); + while(i < j) if(m%lowprimes[i++] == 0) return false; + } + return x.millerRabin(t); +} + +// (protected) true if probably prime (HAC 4.24, Miller-Rabin) +function bnpMillerRabin(t) { + var n1 = this.subtract(BigInteger.ONE); + var k = n1.getLowestSetBit(); + if(k <= 0) return false; + var r = n1.shiftRight(k); + t = (t+1)>>1; + if(t > lowprimes.length) t = lowprimes.length; + var a = nbi(); + for(var i = 0; i < t; ++i) { + //Pick bases at random, instead of starting at 2 + a.fromInt(lowprimes[Math.floor(Math.random()*lowprimes.length)]); + var y = a.modPow(r,this); + if(y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) { + var j = 1; + while(j++ < k && y.compareTo(n1) != 0) { + y = y.modPowInt(2,this); + if(y.compareTo(BigInteger.ONE) == 0) return false; + } + if(y.compareTo(n1) != 0) return false; + } + } + return true; +} + +// protected +BigInteger.prototype.chunkSize = bnpChunkSize; +BigInteger.prototype.toRadix = bnpToRadix; +BigInteger.prototype.fromRadix = bnpFromRadix; +BigInteger.prototype.fromNumber = bnpFromNumber; +BigInteger.prototype.bitwiseTo = bnpBitwiseTo; +BigInteger.prototype.changeBit = bnpChangeBit; +BigInteger.prototype.addTo = bnpAddTo; +BigInteger.prototype.dMultiply = bnpDMultiply; +BigInteger.prototype.dAddOffset = bnpDAddOffset; +BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo; +BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo; +BigInteger.prototype.modInt = bnpModInt; +BigInteger.prototype.millerRabin = bnpMillerRabin; + +// public +BigInteger.prototype.clone = bnClone; +BigInteger.prototype.intValue = bnIntValue; +BigInteger.prototype.byteValue = bnByteValue; +BigInteger.prototype.shortValue = bnShortValue; +BigInteger.prototype.signum = bnSigNum; +BigInteger.prototype.toByteArray = bnToByteArray; +BigInteger.prototype.equals = bnEquals; +BigInteger.prototype.min = bnMin; +BigInteger.prototype.max = bnMax; +BigInteger.prototype.and = bnAnd; +BigInteger.prototype.or = bnOr; +BigInteger.prototype.xor = bnXor; +BigInteger.prototype.andNot = bnAndNot; +BigInteger.prototype.not = bnNot; +BigInteger.prototype.shiftLeft = bnShiftLeft; +BigInteger.prototype.shiftRight = bnShiftRight; +BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit; +BigInteger.prototype.bitCount = bnBitCount; +BigInteger.prototype.testBit = bnTestBit; +BigInteger.prototype.setBit = bnSetBit; +BigInteger.prototype.clearBit = bnClearBit; +BigInteger.prototype.flipBit = bnFlipBit; +BigInteger.prototype.add = bnAdd; +BigInteger.prototype.subtract = bnSubtract; +BigInteger.prototype.multiply = bnMultiply; +BigInteger.prototype.divide = bnDivide; +BigInteger.prototype.remainder = bnRemainder; +BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder; +BigInteger.prototype.modPow = bnModPow; +BigInteger.prototype.modInverse = bnModInverse; +BigInteger.prototype.pow = bnPow; +BigInteger.prototype.gcd = bnGCD; +BigInteger.prototype.isProbablePrime = bnIsProbablePrime; + +// JSBN-specific extension +BigInteger.prototype.square = bnSquare; + +// BigInteger interfaces not implemented in jsbn: + +// BigInteger(int signum, byte[] magnitude) +// double doubleValue() +// float floatValue() +// int hashCode() +// long longValue() +// static BigInteger valueOf(long val) +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +// Depends on jsbn.js and rng.js + +// Version 1.1: support utf-8 encoding in pkcs1pad2 + +// convert a (hex) string to a bignum object +function parseBigInt(str,r) { + return new BigInteger(str,r); +} + +function linebrk(s,n) { + var ret = ""; + var i = 0; + while(i + n < s.length) { + ret += s.substring(i,i+n) + "\n"; + i += n; + } + return ret + s.substring(i,s.length); +} + +function byte2Hex(b) { + if(b < 0x10) + return "0" + b.toString(16); + else + return b.toString(16); +} + +// PKCS#1 (type 2, random) pad input string s to n bytes, and return a bigint +function pkcs1pad2(s,n) { + if(n < s.length + 11) { // TODO: fix for utf-8 + alert("Message too long for RSA"); + return null; + } + var ba = new Array(); + var i = s.length - 1; + while(i >= 0 && n > 0) { + var c = s.charCodeAt(i--); + if(c < 128) { // encode using utf-8 + ba[--n] = c; + } + else if((c > 127) && (c < 2048)) { + ba[--n] = (c & 63) | 128; + ba[--n] = (c >> 6) | 192; + } + else { + ba[--n] = (c & 63) | 128; + ba[--n] = ((c >> 6) & 63) | 128; + ba[--n] = (c >> 12) | 224; + } + } + ba[--n] = 0; + var rng = new SecureRandom(); + var x = new Array(); + while(n > 2) { // random non-zero pad + x[0] = 0; + while(x[0] == 0) rng.nextBytes(x); + ba[--n] = x[0]; + } + ba[--n] = 2; + ba[--n] = 0; + return new BigInteger(ba); +} + +// PKCS#1 (OAEP) mask generation function +function oaep_mgf1_arr(seed, len, hash) +{ + var mask = '', i = 0; + + while (mask.length < len) + { + mask += hash(String.fromCharCode.apply(String, seed.concat([ + (i & 0xff000000) >> 24, + (i & 0x00ff0000) >> 16, + (i & 0x0000ff00) >> 8, + i & 0x000000ff]))); + i += 1; + } + + return mask; +} + +var SHA1_SIZE = 20; + +// PKCS#1 (OAEP) pad input string s to n bytes, and return a bigint +function oaep_pad(s, n, hash) +{ + if (s.length + 2 * SHA1_SIZE + 2 > n) + { + throw "Message too long for RSA"; + } + + var PS = '', i; + + for (i = 0; i < n - s.length - 2 * SHA1_SIZE - 2; i += 1) + { + PS += '\x00'; + } + + var DB = rstr_sha1('') + PS + '\x01' + s; + var seed = new Array(SHA1_SIZE); + new SecureRandom().nextBytes(seed); + + var dbMask = oaep_mgf1_arr(seed, DB.length, hash || rstr_sha1); + var maskedDB = []; + + for (i = 0; i < DB.length; i += 1) + { + maskedDB[i] = DB.charCodeAt(i) ^ dbMask.charCodeAt(i); + } + + var seedMask = oaep_mgf1_arr(maskedDB, seed.length, rstr_sha1); + var maskedSeed = [0]; + + for (i = 0; i < seed.length; i += 1) + { + maskedSeed[i + 1] = seed[i] ^ seedMask.charCodeAt(i); + } + + return new BigInteger(maskedSeed.concat(maskedDB)); +} + +// "empty" RSA key constructor +function RSAKey() { + this.n = null; + this.e = 0; + this.d = null; + this.p = null; + this.q = null; + this.dmp1 = null; + this.dmq1 = null; + this.coeff = null; +} + +// Set the public key fields N and e from hex strings +function RSASetPublic(N,E) { + this.isPublic = true; + if (typeof N !== "string") + { + this.n = N; + this.e = E; + } + else if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + } + else + alert("Invalid RSA public key"); +} + +// Perform raw public operation on "x": return x^e (mod n) +function RSADoPublic(x) { + return x.modPowInt(this.e, this.n); +} + +// Return the PKCS#1 RSA encryption of "text" as an even-length hex string +function RSAEncrypt(text) { + var m = pkcs1pad2(text,(this.n.bitLength()+7)>>3); + if(m == null) return null; + var c = this.doPublic(m); + if(c == null) return null; + var h = c.toString(16); + if((h.length & 1) == 0) return h; else return "0" + h; +} + +// Return the PKCS#1 OAEP RSA encryption of "text" as an even-length hex string +function RSAEncryptOAEP(text, hash) { + var m = oaep_pad(text, (this.n.bitLength()+7)>>3, hash); + if(m == null) return null; + var c = this.doPublic(m); + if(c == null) return null; + var h = c.toString(16); + if((h.length & 1) == 0) return h; else return "0" + h; +} + +// Return the PKCS#1 RSA encryption of "text" as a Base64-encoded string +//function RSAEncryptB64(text) { +// var h = this.encrypt(text); +// if(h) return hex2b64(h); else return null; +//} + +// protected +RSAKey.prototype.doPublic = RSADoPublic; + +// public +RSAKey.prototype.setPublic = RSASetPublic; +RSAKey.prototype.encrypt = RSAEncrypt; +RSAKey.prototype.encryptOAEP = RSAEncryptOAEP; +//RSAKey.prototype.encrypt_b64 = RSAEncryptB64; + +RSAKey.prototype.type = "RSA"; +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ +// Depends on rsa.js and jsbn2.js + +// Version 1.1: support utf-8 decoding in pkcs1unpad2 + +// Undo PKCS#1 (type 2, random) padding and, if valid, return the plaintext +function pkcs1unpad2(d,n) { + var b = d.toByteArray(); + var i = 0; + while(i < b.length && b[i] == 0) ++i; + if(b.length-i != n-1 || b[i] != 2) + return null; + ++i; + while(b[i] != 0) + if(++i >= b.length) return null; + var ret = ""; + while(++i < b.length) { + var c = b[i] & 255; + if(c < 128) { // utf-8 decode + ret += String.fromCharCode(c); + } + else if((c > 191) && (c < 224)) { + ret += String.fromCharCode(((c & 31) << 6) | (b[i+1] & 63)); + ++i; + } + else { + ret += String.fromCharCode(((c & 15) << 12) | ((b[i+1] & 63) << 6) | (b[i+2] & 63)); + i += 2; + } + } + return ret; +} + +// PKCS#1 (OAEP) mask generation function +function oaep_mgf1_str(seed, len, hash) +{ + var mask = '', i = 0; + + while (mask.length < len) + { + mask += hash(seed + String.fromCharCode.apply(String, [ + (i & 0xff000000) >> 24, + (i & 0x00ff0000) >> 16, + (i & 0x0000ff00) >> 8, + i & 0x000000ff])); + i += 1; + } + + return mask; +} + +var SHA1_SIZE = 20; + +// Undo PKCS#1 (OAEP) padding and, if valid, return the plaintext +function oaep_unpad(d, n, hash) +{ + d = d.toByteArray(); + + var i; + + for (i = 0; i < d.length; i += 1) + { + d[i] &= 0xff; + } + + while (d.length < n) + { + d.unshift(0); + } + + d = String.fromCharCode.apply(String, d); + + if (d.length < 2 * SHA1_SIZE + 2) + { + throw "Cipher too short"; + } + + var maskedSeed = d.substr(1, SHA1_SIZE) + var maskedDB = d.substr(SHA1_SIZE + 1); + + var seedMask = oaep_mgf1_str(maskedDB, SHA1_SIZE, hash || rstr_sha1); + var seed = [], i; + + for (i = 0; i < maskedSeed.length; i += 1) + { + seed[i] = maskedSeed.charCodeAt(i) ^ seedMask.charCodeAt(i); + } + + var dbMask = oaep_mgf1_str(String.fromCharCode.apply(String, seed), + d.length - SHA1_SIZE, rstr_sha1); + + var DB = []; + + for (i = 0; i < maskedDB.length; i += 1) + { + DB[i] = maskedDB.charCodeAt(i) ^ dbMask.charCodeAt(i); + } + + DB = String.fromCharCode.apply(String, DB); + + if (DB.substr(0, SHA1_SIZE) !== rstr_sha1('')) + { + throw "Hash mismatch"; + } + + DB = DB.substr(SHA1_SIZE); + + var first_one = DB.indexOf('\x01'); + var last_zero = (first_one != -1) ? DB.substr(0, first_one).lastIndexOf('\x00') : -1; + + if (last_zero + 1 != first_one) + { + throw "Malformed data"; + } + + return DB.substr(first_one + 1); +} + +// Set the private key fields N, e, and d from hex strings +function RSASetPrivate(N,E,D) { + this.isPrivate = true; + if (typeof N !== "string") + { + this.n = N; + this.e = E; + this.d = D; + } + else if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + this.d = parseBigInt(D,16); + } + else + alert("Invalid RSA private key"); +} + +// Set the private key fields N, e, d and CRT params from hex strings +function RSASetPrivateEx(N,E,D,P,Q,DP,DQ,C) { + this.isPrivate = true; + if (N == null) throw "RSASetPrivateEx N == null"; + if (E == null) throw "RSASetPrivateEx E == null"; + if (N.length == 0) throw "RSASetPrivateEx N.length == 0"; + if (E.length == 0) throw "RSASetPrivateEx E.length == 0"; + + if (N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + this.d = parseBigInt(D,16); + this.p = parseBigInt(P,16); + this.q = parseBigInt(Q,16); + this.dmp1 = parseBigInt(DP,16); + this.dmq1 = parseBigInt(DQ,16); + this.coeff = parseBigInt(C,16); + } else { + alert("Invalid RSA private key in RSASetPrivateEx"); + } +} + +// Generate a new random private key B bits long, using public expt E +function RSAGenerate(B,E) { + var rng = new SecureRandom(); + var qs = B>>1; + this.e = parseInt(E,16); + var ee = new BigInteger(E,16); + for(;;) { + for(;;) { + this.p = new BigInteger(B-qs,1,rng); + if(this.p.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) == 0 && this.p.isProbablePrime(10)) break; + } + for(;;) { + this.q = new BigInteger(qs,1,rng); + if(this.q.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) == 0 && this.q.isProbablePrime(10)) break; + } + if(this.p.compareTo(this.q) <= 0) { + var t = this.p; + this.p = this.q; + this.q = t; + } + var p1 = this.p.subtract(BigInteger.ONE); // p1 = p - 1 + var q1 = this.q.subtract(BigInteger.ONE); // q1 = q - 1 + var phi = p1.multiply(q1); + if(phi.gcd(ee).compareTo(BigInteger.ONE) == 0) { + this.n = this.p.multiply(this.q); // this.n = p * q + this.d = ee.modInverse(phi); // this.d = + this.dmp1 = this.d.mod(p1); // this.dmp1 = d mod (p - 1) + this.dmq1 = this.d.mod(q1); // this.dmq1 = d mod (q - 1) + this.coeff = this.q.modInverse(this.p); // this.coeff = (q ^ -1) mod p + break; + } + } + this.isPrivate = true; +} + +// Perform raw private operation on "x": return x^d (mod n) +function RSADoPrivate(x) { + if(this.p == null || this.q == null) + return x.modPow(this.d, this.n); + + // TODO: re-calculate any missing CRT params + var xp = x.mod(this.p).modPow(this.dmp1, this.p); // xp=cp? + var xq = x.mod(this.q).modPow(this.dmq1, this.q); // xq=cq? + + while(xp.compareTo(xq) < 0) + xp = xp.add(this.p); + // NOTE: + // xp.subtract(xq) => cp -cq + // xp.subtract(xq).multiply(this.coeff).mod(this.p) => (cp - cq) * u mod p = h + // xp.subtract(xq).multiply(this.coeff).mod(this.p).multiply(this.q).add(xq) => cq + (h * q) = M + return xp.subtract(xq).multiply(this.coeff).mod(this.p).multiply(this.q).add(xq); +} + +// Return the PKCS#1 RSA decryption of "ctext". +// "ctext" is an even-length hex string and the output is a plain string. +function RSADecrypt(ctext) { + var c = parseBigInt(ctext, 16); + var m = this.doPrivate(c); + if(m == null) return null; + return pkcs1unpad2(m, (this.n.bitLength()+7)>>3); +} + +// Return the PKCS#1 OAEP RSA decryption of "ctext". +// "ctext" is an even-length hex string and the output is a plain string. +function RSADecryptOAEP(ctext, hash) { + var c = parseBigInt(ctext, 16); + var m = this.doPrivate(c); + if(m == null) return null; + return oaep_unpad(m, (this.n.bitLength()+7)>>3, hash); +} + +// Return the PKCS#1 RSA decryption of "ctext". +// "ctext" is a Base64-encoded string and the output is a plain string. +//function RSAB64Decrypt(ctext) { +// var h = b64tohex(ctext); +// if(h) return this.decrypt(h); else return null; +//} + +// protected +RSAKey.prototype.doPrivate = RSADoPrivate; + +// public +RSAKey.prototype.setPrivate = RSASetPrivate; +RSAKey.prototype.setPrivateEx = RSASetPrivateEx; +RSAKey.prototype.generate = RSAGenerate; +RSAKey.prototype.decrypt = RSADecrypt; +RSAKey.prototype.decryptOAEP = RSADecryptOAEP; +//RSAKey.prototype.b64_decrypt = RSAB64Decrypt; +/*! rsapem-1.1.js (c) 2012 Kenji Urushima | kjur.github.com/jsrsasign/license + */ +// +// rsa-pem.js - adding function for reading/writing PKCS#1 PEM private key +// to RSAKey class. +// +// version: 1.1.1 (2013-Apr-12) +// +// Copyright (c) 2010-2013 Kenji Urushima (kenji.urushima@gmail.com) +// +// This software is licensed under the terms of the MIT License. +// http://kjur.github.com/jsrsasign/license/ +// +// The above copyright and license notice shall be +// included in all copies or substantial portions of the Software. +// +// +// Depends on: +// +// +// +// _RSApem_pemToBase64(sPEM) +// +// removing PEM header, PEM footer and space characters including +// new lines from PEM formatted RSA private key string. +// + +/** + * @fileOverview + * @name rsapem-1.1.js + * @author Kenji Urushima kenji.urushima@gmail.com + * @version 1.1 + * @license MIT License + */ +function _rsapem_pemToBase64(sPEMPrivateKey) { + var s = sPEMPrivateKey; + s = s.replace("-----BEGIN RSA PRIVATE KEY-----", ""); + s = s.replace("-----END RSA PRIVATE KEY-----", ""); + s = s.replace(/[ \n]+/g, ""); + return s; +} + +function _rsapem_getPosArrayOfChildrenFromHex(hPrivateKey) { + var a = new Array(); + var v1 = ASN1HEX.getStartPosOfV_AtObj(hPrivateKey, 0); + var n1 = ASN1HEX.getPosOfNextSibling_AtObj(hPrivateKey, v1); + var e1 = ASN1HEX.getPosOfNextSibling_AtObj(hPrivateKey, n1); + var d1 = ASN1HEX.getPosOfNextSibling_AtObj(hPrivateKey, e1); + var p1 = ASN1HEX.getPosOfNextSibling_AtObj(hPrivateKey, d1); + var q1 = ASN1HEX.getPosOfNextSibling_AtObj(hPrivateKey, p1); + var dp1 = ASN1HEX.getPosOfNextSibling_AtObj(hPrivateKey, q1); + var dq1 = ASN1HEX.getPosOfNextSibling_AtObj(hPrivateKey, dp1); + var co1 = ASN1HEX.getPosOfNextSibling_AtObj(hPrivateKey, dq1); + a.push(v1, n1, e1, d1, p1, q1, dp1, dq1, co1); + return a; +} + +function _rsapem_getHexValueArrayOfChildrenFromHex(hPrivateKey) { + var posArray = _rsapem_getPosArrayOfChildrenFromHex(hPrivateKey); + var v = ASN1HEX.getHexOfV_AtObj(hPrivateKey, posArray[0]); + var n = ASN1HEX.getHexOfV_AtObj(hPrivateKey, posArray[1]); + var e = ASN1HEX.getHexOfV_AtObj(hPrivateKey, posArray[2]); + var d = ASN1HEX.getHexOfV_AtObj(hPrivateKey, posArray[3]); + var p = ASN1HEX.getHexOfV_AtObj(hPrivateKey, posArray[4]); + var q = ASN1HEX.getHexOfV_AtObj(hPrivateKey, posArray[5]); + var dp = ASN1HEX.getHexOfV_AtObj(hPrivateKey, posArray[6]); + var dq = ASN1HEX.getHexOfV_AtObj(hPrivateKey, posArray[7]); + var co = ASN1HEX.getHexOfV_AtObj(hPrivateKey, posArray[8]); + var a = new Array(); + a.push(v, n, e, d, p, q, dp, dq, co); + return a; +} + +/** + * read RSA private key from a ASN.1 hexadecimal string + * @name readPrivateKeyFromASN1HexString + * @memberOf RSAKey# + * @function + * @param {String} keyHex ASN.1 hexadecimal string of PKCS#1 private key. + * @since 1.1.1 + */ +function _rsapem_readPrivateKeyFromASN1HexString(keyHex) { + var a = _rsapem_getHexValueArrayOfChildrenFromHex(keyHex); + this.setPrivateEx(a[1],a[2],a[3],a[4],a[5],a[6],a[7],a[8]); +} + +/** + * read PKCS#1 private key from a string + * @name readPrivateKeyFromPEMString + * @memberOf RSAKey# + * @function + * @param {String} keyPEM string of PKCS#1 private key. + */ +function _rsapem_readPrivateKeyFromPEMString(keyPEM) { + var keyB64 = _rsapem_pemToBase64(keyPEM); + var keyHex = b64tohex(keyB64) // depends base64.js + var a = _rsapem_getHexValueArrayOfChildrenFromHex(keyHex); + this.setPrivateEx(a[1],a[2],a[3],a[4],a[5],a[6],a[7],a[8]); +} + +RSAKey.prototype.readPrivateKeyFromPEMString = _rsapem_readPrivateKeyFromPEMString; +RSAKey.prototype.readPrivateKeyFromASN1HexString = _rsapem_readPrivateKeyFromASN1HexString; +/*! rsasign-1.2.7.js (c) 2012 Kenji Urushima | kjur.github.com/jsrsasign/license + */ +var _RE_HEXDECONLY=new RegExp("");_RE_HEXDECONLY.compile("[^0-9a-f]","gi");function _rsasign_getHexPaddedDigestInfoForString(d,e,a){var b=function(f){return KJUR.crypto.Util.hashString(f,a)};var c=b(d);return KJUR.crypto.Util.getPaddedDigestInfoHex(c,a,e)}function _zeroPaddingOfSignature(e,d){var c="";var a=d/4-e.length;for(var b=0;b>24,(d&16711680)>>16,(d&65280)>>8,d&255]))));d+=1}return b}function _rsasign_signStringPSS(e,a,d){var c=function(f){return KJUR.crypto.Util.hashHex(f,a)};var b=c(rstrtohex(e));if(d===undefined){d=-1}return this.signWithMessageHashPSS(b,a,d)}function _rsasign_signWithMessageHashPSS(l,a,k){var b=hextorstr(l);var g=b.length;var m=this.n.bitLength()-1;var c=Math.ceil(m/8);var d;var o=function(i){return KJUR.crypto.Util.hashHex(i,a)};if(k===-1||k===undefined){k=g}else{if(k===-2){k=c-g-2}else{if(k<-2){throw"invalid salt length"}}}if(c<(g+k+2)){throw"data too long"}var f="";if(k>0){f=new Array(k);new SecureRandom().nextBytes(f);f=String.fromCharCode.apply(String,f)}var n=hextorstr(o(rstrtohex("\x00\x00\x00\x00\x00\x00\x00\x00"+b+f)));var j=[];for(d=0;d>(8*c-m))&255;q[0]&=~p;for(d=0;dthis.n.bitLength()){return 0}var i=this.doPublic(b);var e=i.toString(16).replace(/^1f+00/,"");var g=_rsasign_getAlgNameAndHashFromHexDisgestInfo(e);if(g.length==0){return false}var d=g[0];var h=g[1];var a=function(k){return KJUR.crypto.Util.hashString(k,d)};var c=a(f);return(h==c)}function _rsasign_verifyWithMessageHash(e,a){a=a.replace(_RE_HEXDECONLY,"");a=a.replace(/[ \n]+/g,"");var b=parseBigInt(a,16);if(b.bitLength()>this.n.bitLength()){return 0}var h=this.doPublic(b);var g=h.toString(16).replace(/^1f+00/,"");var c=_rsasign_getAlgNameAndHashFromHexDisgestInfo(g);if(c.length==0){return false}var d=c[0];var f=c[1];return(f==e)}function _rsasign_verifyStringPSS(c,b,a,f){var e=function(g){return KJUR.crypto.Util.hashHex(g,a)};var d=e(rstrtohex(c));if(f===undefined){f=-1}return this.verifyWithMessageHashPSS(d,b,a,f)}function _rsasign_verifyWithMessageHashPSS(f,s,l,c){var k=new BigInteger(s,16);if(k.bitLength()>this.n.bitLength()){return false}var r=function(i){return KJUR.crypto.Util.hashHex(i,l)};var j=hextorstr(f);var h=j.length;var g=this.n.bitLength()-1;var m=Math.ceil(g/8);var q;if(c===-1||c===undefined){c=h}else{if(c===-2){c=m-h-2}else{if(c<-2){throw"invalid salt length"}}}if(m<(h+c+2)){throw"data too long"}var a=this.doPublic(k).toByteArray();for(q=0;q>(8*m-g))&255;if((d.charCodeAt(0)&p)!==0){throw"bits beyond keysize not zero"}var n=pss_mgf1_str(e,d.length,r);var o=[];for(q=0;qMIT License + */ + +/* + * MEMO: + * f('3082025b02...', 2) ... 82025b ... 3bytes + * f('020100', 2) ... 01 ... 1byte + * f('0203001...', 2) ... 03 ... 1byte + * f('02818003...', 2) ... 8180 ... 2bytes + * f('3080....0000', 2) ... 80 ... -1 + * + * Requirements: + * - ASN.1 type octet length MUST be 1. + * (i.e. ASN.1 primitives like SET, SEQUENCE, INTEGER, OCTETSTRING ...) + */ + +/** + * ASN.1 DER encoded hexadecimal string utility class + * @name ASN1HEX + * @class ASN.1 DER encoded hexadecimal string utility class + * @since jsrsasign 1.1 + */ +var ASN1HEX = new function() { + /** + * get byte length for ASN.1 L(length) bytes + * @name getByteLengthOfL_AtObj + * @memberOf ASN1HEX + * @function + * @param {String} s hexadecimal string of ASN.1 DER encoded data + * @param {Number} pos string index + * @return byte length for ASN.1 L(length) bytes + */ + this.getByteLengthOfL_AtObj = function(s, pos) { + if (s.substring(pos + 2, pos + 3) != '8') return 1; + var i = parseInt(s.substring(pos + 3, pos + 4)); + if (i == 0) return -1; // length octet '80' indefinite length + if (0 < i && i < 10) return i + 1; // including '8?' octet; + return -2; // malformed format + }; + + /** + * get hexadecimal string for ASN.1 L(length) bytes + * @name getHexOfL_AtObj + * @memberOf ASN1HEX + * @function + * @param {String} s hexadecimal string of ASN.1 DER encoded data + * @param {Number} pos string index + * @return {String} hexadecimal string for ASN.1 L(length) bytes + */ + this.getHexOfL_AtObj = function(s, pos) { + var len = this.getByteLengthOfL_AtObj(s, pos); + if (len < 1) return ''; + return s.substring(pos + 2, pos + 2 + len * 2); + }; + + // getting ASN.1 length value at the position 'idx' of + // hexa decimal string 's'. + // + // f('3082025b02...', 0) ... 82025b ... ??? + // f('020100', 0) ... 01 ... 1 + // f('0203001...', 0) ... 03 ... 3 + // f('02818003...', 0) ... 8180 ... 128 + /** + * get integer value of ASN.1 length for ASN.1 data + * @name getIntOfL_AtObj + * @memberOf ASN1HEX + * @function + * @param {String} s hexadecimal string of ASN.1 DER encoded data + * @param {Number} pos string index + * @return ASN.1 L(length) integer value + */ + this.getIntOfL_AtObj = function(s, pos) { + var hLength = this.getHexOfL_AtObj(s, pos); + if (hLength == '') return -1; + var bi; + if (parseInt(hLength.substring(0, 1)) < 8) { + bi = new BigInteger(hLength, 16); + } else { + bi = new BigInteger(hLength.substring(2), 16); + } + return bi.intValue(); + }; + + /** + * get ASN.1 value starting string position for ASN.1 object refered by index 'idx'. + * @name getStartPosOfV_AtObj + * @memberOf ASN1HEX + * @function + * @param {String} s hexadecimal string of ASN.1 DER encoded data + * @param {Number} pos string index + */ + this.getStartPosOfV_AtObj = function(s, pos) { + var l_len = this.getByteLengthOfL_AtObj(s, pos); + if (l_len < 0) return l_len; + return pos + (l_len + 1) * 2; + }; + + /** + * get hexadecimal string of ASN.1 V(value) + * @name getHexOfV_AtObj + * @memberOf ASN1HEX + * @function + * @param {String} s hexadecimal string of ASN.1 DER encoded data + * @param {Number} pos string index + * @return {String} hexadecimal string of ASN.1 value. + */ + this.getHexOfV_AtObj = function(s, pos) { + var pos1 = this.getStartPosOfV_AtObj(s, pos); + var len = this.getIntOfL_AtObj(s, pos); + return s.substring(pos1, pos1 + len * 2); + }; + + /** + * get hexadecimal string of ASN.1 TLV at + * @name getHexOfTLV_AtObj + * @memberOf ASN1HEX + * @function + * @param {String} s hexadecimal string of ASN.1 DER encoded data + * @param {Number} pos string index + * @return {String} hexadecimal string of ASN.1 TLV. + * @since 1.1 + */ + this.getHexOfTLV_AtObj = function(s, pos) { + var hT = s.substr(pos, 2); + var hL = this.getHexOfL_AtObj(s, pos); + var hV = this.getHexOfV_AtObj(s, pos); + return hT + hL + hV; + }; + + /** + * get next sibling starting index for ASN.1 object string + * @name getPosOfNextSibling_AtObj + * @memberOf ASN1HEX + * @function + * @param {String} s hexadecimal string of ASN.1 DER encoded data + * @param {Number} pos string index + * @return next sibling starting index for ASN.1 object string + */ + this.getPosOfNextSibling_AtObj = function(s, pos) { + var pos1 = this.getStartPosOfV_AtObj(s, pos); + var len = this.getIntOfL_AtObj(s, pos); + return pos1 + len * 2; + }; + + /** + * get array of indexes of child ASN.1 objects + * @name getPosArrayOfChildren_AtObj + * @memberOf ASN1HEX + * @function + * @param {String} s hexadecimal string of ASN.1 DER encoded data + * @param {Number} start string index of ASN.1 object + * @return {Array of Number} array of indexes for childen of ASN.1 objects + */ + this.getPosArrayOfChildren_AtObj = function(h, pos) { + var a = new Array(); + var p0 = this.getStartPosOfV_AtObj(h, pos); + a.push(p0); + + var len = this.getIntOfL_AtObj(h, pos); + var p = p0; + var k = 0; + while (1) { + var pNext = this.getPosOfNextSibling_AtObj(h, p); + if (pNext == null || (pNext - p0 >= (len * 2))) break; + if (k >= 200) break; + + a.push(pNext); + p = pNext; + + k++; + } + + return a; + }; + + /** + * get string index of nth child object of ASN.1 object refered by h, idx + * @name getNthChildIndex_AtObj + * @memberOf ASN1HEX + * @function + * @param {String} h hexadecimal string of ASN.1 DER encoded data + * @param {Number} idx start string index of ASN.1 object + * @param {Number} nth for child + * @return {Number} string index of nth child. + * @since 1.1 + */ + this.getNthChildIndex_AtObj = function(h, idx, nth) { + var a = this.getPosArrayOfChildren_AtObj(h, idx); + return a[nth]; + }; + + // ========== decendant methods ============================== + /** + * get string index of nth child object of ASN.1 object refered by h, idx + * @name getDecendantIndexByNthList + * @memberOf ASN1HEX + * @function + * @param {String} h hexadecimal string of ASN.1 DER encoded data + * @param {Number} currentIndex start string index of ASN.1 object + * @param {Array of Number} nthList array list of nth + * @return {Number} string index refered by nthList + * @since 1.1 + * @example + * The "nthList" is a index list of structured ASN.1 object + * reference. Here is a sample structure and "nthList"s which + * refers each objects. + * + * SQUENCE - + * SEQUENCE - [0] + * IA5STRING 000 - [0, 0] + * UTF8STRING 001 - [0, 1] + * SET - [1] + * IA5STRING 010 - [1, 0] + * UTF8STRING 011 - [1, 1] + */ + this.getDecendantIndexByNthList = function(h, currentIndex, nthList) { + if (nthList.length == 0) { + return currentIndex; + } + var firstNth = nthList.shift(); + var a = this.getPosArrayOfChildren_AtObj(h, currentIndex); + return this.getDecendantIndexByNthList(h, a[firstNth], nthList); + }; + + /** + * get hexadecimal string of ASN.1 TLV refered by current index and nth index list. + * @name getDecendantHexTLVByNthList + * @memberOf ASN1HEX + * @function + * @param {String} h hexadecimal string of ASN.1 DER encoded data + * @param {Number} currentIndex start string index of ASN.1 object + * @param {Array of Number} nthList array list of nth + * @return {Number} hexadecimal string of ASN.1 TLV refered by nthList + * @since 1.1 + */ + this.getDecendantHexTLVByNthList = function(h, currentIndex, nthList) { + var idx = this.getDecendantIndexByNthList(h, currentIndex, nthList); + return this.getHexOfTLV_AtObj(h, idx); + }; + + /** + * get hexadecimal string of ASN.1 V refered by current index and nth index list. + * @name getDecendantHexVByNthList + * @memberOf ASN1HEX + * @function + * @param {String} h hexadecimal string of ASN.1 DER encoded data + * @param {Number} currentIndex start string index of ASN.1 object + * @param {Array of Number} nthList array list of nth + * @return {Number} hexadecimal string of ASN.1 V refered by nthList + * @since 1.1 + */ + this.getDecendantHexVByNthList = function(h, currentIndex, nthList) { + var idx = this.getDecendantIndexByNthList(h, currentIndex, nthList); + return this.getHexOfV_AtObj(h, idx); + }; +}; + +/* + * @since asn1hex 1.1.4 + */ +ASN1HEX.getVbyList = function(h, currentIndex, nthList, checkingTag) { + var idx = this.getDecendantIndexByNthList(h, currentIndex, nthList); + if (idx === undefined) { + throw "can't find nthList object"; + } + if (checkingTag !== undefined) { + if (h.substr(idx, 2) != checkingTag) { + throw "checking tag doesn't match: " + + h.substr(idx,2) + "!=" + checkingTag; + } + } + return this.getHexOfV_AtObj(h, idx); +}; + +/** + * get OID string from hexadecimal encoded value + * @name hextooidstr + * @memberOf ASN1HEX + * @function + * @param {String} hex hexadecmal string of ASN.1 DER encoded OID value + * @return {String} OID string (ex. '1.2.3.4.567') + * @since asn1hex 1.1.5 + */ +ASN1HEX.hextooidstr = function(hex) { + var zeroPadding = function(s, len) { + if (s.length >= len) return s; + return new Array(len - s.length + 1).join('0') + s; + }; + + var a = []; + + // a[0], a[1] + var hex0 = hex.substr(0, 2); + var i0 = parseInt(hex0, 16); + a[0] = new String(Math.floor(i0 / 40)); + a[1] = new String(i0 % 40); + + // a[2]..a[n] + var hex1 = hex.substr(2); + var b = []; + for (var i = 0; i < hex1.length / 2; i++) { + b.push(parseInt(hex1.substr(i * 2, 2), 16)); + } + var c = []; + var cbin = ""; + for (var i = 0; i < b.length; i++) { + if (b[i] & 0x80) { + cbin = cbin + zeroPadding((b[i] & 0x7f).toString(2), 7); + } else { + cbin = cbin + zeroPadding((b[i] & 0x7f).toString(2), 7); + c.push(new String(parseInt(cbin, 2))); + cbin = ""; + } + } + + var s = a.join("."); + if (c.length > 0) s = s + "." + c.join("."); + return s; +}; + +/*! x509-1.1.3.js (c) 2012-2014 Kenji Urushima | kjur.github.com/jsrsasign/license + */ +/* + * x509.js - X509 class to read subject public key from certificate. + * + * Copyright (c) 2010-2014 Kenji Urushima (kenji.urushima@gmail.com) + * + * This software is licensed under the terms of the MIT License. + * http://kjur.github.com/jsrsasign/license + * + * The above copyright and license notice shall be + * included in all copies or substantial portions of the Software. + */ + +/** + * @fileOverview + * @name x509-1.1.js + * @author Kenji Urushima kenji.urushima@gmail.com + * @version x509 1.1.3 (2014-May-17) + * @since jsrsasign 1.x.x + * @license MIT License + */ + +/* + * Depends: + * base64.js + * rsa.js + * asn1hex.js + */ + +/** + * X.509 certificate class.
+ * @class X.509 certificate class + * @property {RSAKey} subjectPublicKeyRSA Tom Wu's RSAKey object + * @property {String} subjectPublicKeyRSA_hN hexadecimal string for modulus of RSA public key + * @property {String} subjectPublicKeyRSA_hE hexadecimal string for public exponent of RSA public key + * @property {String} hex hexacedimal string for X.509 certificate. + * @author Kenji Urushima + * @version 1.0.1 (08 May 2012) + * @see 'jwrsasign'(RSA Sign JavaScript Library) home page http://kjur.github.com/jsrsasign/ + */ +function X509() { + this.subjectPublicKeyRSA = null; + this.subjectPublicKeyRSA_hN = null; + this.subjectPublicKeyRSA_hE = null; + this.hex = null; + + // ===== get basic fields from hex ===================================== + + /** + * get hexadecimal string of serialNumber field of certificate.
+ * @name getSerialNumberHex + * @memberOf X509# + * @function + */ + this.getSerialNumberHex = function() { + return ASN1HEX.getDecendantHexVByNthList(this.hex, 0, [0, 1]); + }; + + /** + * get hexadecimal string of issuer field TLV of certificate.
+ * @name getIssuerHex + * @memberOf X509# + * @function + */ + this.getIssuerHex = function() { + return ASN1HEX.getDecendantHexTLVByNthList(this.hex, 0, [0, 3]); + }; + + /** + * get string of issuer field of certificate.
+ * @name getIssuerString + * @memberOf X509# + * @function + */ + this.getIssuerString = function() { + return X509.hex2dn(ASN1HEX.getDecendantHexTLVByNthList(this.hex, 0, [0, 3])); + }; + + /** + * get hexadecimal string of subject field of certificate.
+ * @name getSubjectHex + * @memberOf X509# + * @function + */ + this.getSubjectHex = function() { + return ASN1HEX.getDecendantHexTLVByNthList(this.hex, 0, [0, 5]); + }; + + /** + * get string of subject field of certificate.
+ * @name getSubjectString + * @memberOf X509# + * @function + */ + this.getSubjectString = function() { + return X509.hex2dn(ASN1HEX.getDecendantHexTLVByNthList(this.hex, 0, [0, 5])); + }; + + /** + * get notBefore field string of certificate.
+ * @name getNotBefore + * @memberOf X509# + * @function + */ + this.getNotBefore = function() { + var s = ASN1HEX.getDecendantHexVByNthList(this.hex, 0, [0, 4, 0]); + s = s.replace(/(..)/g, "%$1"); + s = decodeURIComponent(s); + return s; + }; + + /** + * get notAfter field string of certificate.
+ * @name getNotAfter + * @memberOf X509# + * @function + */ + this.getNotAfter = function() { + var s = ASN1HEX.getDecendantHexVByNthList(this.hex, 0, [0, 4, 1]); + s = s.replace(/(..)/g, "%$1"); + s = decodeURIComponent(s); + return s; + }; + + // ===== read certificate public key ========================== + + // ===== read certificate ===================================== + /** + * read PEM formatted X.509 certificate from string.
+ * @name readCertPEM + * @memberOf X509# + * @function + * @param {String} sCertPEM string for PEM formatted X.509 certificate + */ + this.readCertPEM = function(sCertPEM) { + var hCert = X509.pemToHex(sCertPEM); + var a = X509.getPublicKeyHexArrayFromCertHex(hCert); + var rsa = new RSAKey(); + rsa.setPublic(a[0], a[1]); + this.subjectPublicKeyRSA = rsa; + this.subjectPublicKeyRSA_hN = a[0]; + this.subjectPublicKeyRSA_hE = a[1]; + this.hex = hCert; + }; + + this.readCertPEMWithoutRSAInit = function(sCertPEM) { + var hCert = X509.pemToHex(sCertPEM); + var a = X509.getPublicKeyHexArrayFromCertHex(hCert); + this.subjectPublicKeyRSA.setPublic(a[0], a[1]); + this.subjectPublicKeyRSA_hN = a[0]; + this.subjectPublicKeyRSA_hE = a[1]; + this.hex = hCert; + }; +}; + +X509.pemToBase64 = function(sCertPEM) { + var s = sCertPEM; + s = s.replace("-----BEGIN CERTIFICATE-----", ""); + s = s.replace("-----END CERTIFICATE-----", ""); + s = s.replace(/[ \n]+/g, ""); + return s; +}; + +X509.pemToHex = function(sCertPEM) { + var b64Cert = X509.pemToBase64(sCertPEM); + var hCert = b64tohex(b64Cert); + return hCert; +}; + +// NOTE: Without BITSTRING encapsulation. +X509.getSubjectPublicKeyPosFromCertHex = function(hCert) { + var pInfo = X509.getSubjectPublicKeyInfoPosFromCertHex(hCert); + if (pInfo == -1) return -1; + var a = ASN1HEX.getPosArrayOfChildren_AtObj(hCert, pInfo); + if (a.length != 2) return -1; + var pBitString = a[1]; + if (hCert.substring(pBitString, pBitString + 2) != '03') return -1; + var pBitStringV = ASN1HEX.getStartPosOfV_AtObj(hCert, pBitString); + + if (hCert.substring(pBitStringV, pBitStringV + 2) != '00') return -1; + return pBitStringV + 2; +}; + +// NOTE: privateKeyUsagePeriod field of X509v2 not supported. +// NOTE: v1 and v3 supported +X509.getSubjectPublicKeyInfoPosFromCertHex = function(hCert) { + var pTbsCert = ASN1HEX.getStartPosOfV_AtObj(hCert, 0); + var a = ASN1HEX.getPosArrayOfChildren_AtObj(hCert, pTbsCert); + if (a.length < 1) return -1; + if (hCert.substring(a[0], a[0] + 10) == "a003020102") { // v3 + if (a.length < 6) return -1; + return a[6]; + } else { + if (a.length < 5) return -1; + return a[5]; + } +}; + +X509.getPublicKeyHexArrayFromCertHex = function(hCert) { + var p = X509.getSubjectPublicKeyPosFromCertHex(hCert); + var a = ASN1HEX.getPosArrayOfChildren_AtObj(hCert, p); + if (a.length != 2) return []; + var hN = ASN1HEX.getHexOfV_AtObj(hCert, a[0]); + var hE = ASN1HEX.getHexOfV_AtObj(hCert, a[1]); + if (hN != null && hE != null) { + return [hN, hE]; + } else { + return []; + } +}; + +X509.getHexTbsCertificateFromCert = function(hCert) { + var pTbsCert = ASN1HEX.getStartPosOfV_AtObj(hCert, 0); + return pTbsCert; +}; + +X509.getPublicKeyHexArrayFromCertPEM = function(sCertPEM) { + var hCert = X509.pemToHex(sCertPEM); + var a = X509.getPublicKeyHexArrayFromCertHex(hCert); + return a; +}; + +X509.hex2dn = function(hDN) { + var s = ""; + var a = ASN1HEX.getPosArrayOfChildren_AtObj(hDN, 0); + for (var i = 0; i < a.length; i++) { + var hRDN = ASN1HEX.getHexOfTLV_AtObj(hDN, a[i]); + s = s + "/" + X509.hex2rdn(hRDN); + } + return s; +}; + +X509.hex2rdn = function(hRDN) { + var hType = ASN1HEX.getDecendantHexTLVByNthList(hRDN, 0, [0, 0]); + var hValue = ASN1HEX.getDecendantHexVByNthList(hRDN, 0, [0, 1]); + var type = ""; + try { type = X509.DN_ATTRHEX[hType]; } catch (ex) { type = hType; } + hValue = hValue.replace(/(..)/g, "%$1"); + var value = decodeURIComponent(hValue); + return type + "=" + value; +}; + +X509.DN_ATTRHEX = { + "0603550406": "C", + "060355040a": "O", + "060355040b": "OU", + "0603550403": "CN", + "0603550405": "SN", + "0603550408": "ST", + "0603550407": "L", +}; + +/** + * get RSAKey/ECDSA public key object from PEM certificate string + * @name getPublicKeyFromCertPEM + * @memberOf X509 + * @function + * @param {String} sCertPEM PEM formatted RSA/ECDSA/DSA X.509 certificate + * @return returns RSAKey/KJUR.crypto.{ECDSA,DSA} object of public key + * @since x509 1.1.1 + * @description + * NOTE: DSA is also supported since x509 1.1.2. + */ +X509.getPublicKeyFromCertPEM = function(sCertPEM) { + var info = X509.getPublicKeyInfoPropOfCertPEM(sCertPEM); + + if (info.algoid == "2a864886f70d010101") { // RSA + var aRSA = KEYUTIL.parsePublicRawRSAKeyHex(info.keyhex); + var key = new RSAKey(); + key.setPublic(aRSA.n, aRSA.e); + return key; + } else if (info.algoid == "2a8648ce3d0201") { // ECC + var curveName = KJUR.crypto.OID.oidhex2name[info.algparam]; + var key = new KJUR.crypto.ECDSA({'curve': curveName, 'info': info.keyhex}); + key.setPublicKeyHex(info.keyhex); + return key; + } else if (info.algoid == "2a8648ce380401") { // DSA 1.2.840.10040.4.1 + var p = ASN1HEX.getVbyList(info.algparam, 0, [0], "02"); + var q = ASN1HEX.getVbyList(info.algparam, 0, [1], "02"); + var g = ASN1HEX.getVbyList(info.algparam, 0, [2], "02"); + var y = ASN1HEX.getHexOfV_AtObj(info.keyhex, 0); + y = y.substr(2); + var key = new KJUR.crypto.DSA(); + key.setPublic(new BigInteger(p, 16), + new BigInteger(q, 16), + new BigInteger(g, 16), + new BigInteger(y, 16)); + return key; + } else { + throw "unsupported key"; + } +}; + +/** + * get public key information from PEM certificate + * @name getPublicKeyInfoPropOfCertPEM + * @memberOf X509 + * @function + * @param {String} sCertPEM string of PEM formatted certificate + * @return {Hash} hash of information for public key + * @since x509 1.1.1 + * @description + * Resulted associative array has following properties: + *
    + *
  • algoid - hexadecimal string of OID of asymmetric key algorithm
  • + *
  • algparam - hexadecimal string of OID of ECC curve name or null
  • + *
  • keyhex - hexadecimal string of key in the certificate
  • + *
+ * @since x509 1.1.1 + */ +X509.getPublicKeyInfoPropOfCertPEM = function(sCertPEM) { + var result = {}; + result.algparam = null; + var hCert = X509.pemToHex(sCertPEM); + + // 1. Certificate ASN.1 + var a1 = ASN1HEX.getPosArrayOfChildren_AtObj(hCert, 0); + if (a1.length != 3) + throw "malformed X.509 certificate PEM (code:001)"; // not 3 item of seq Cert + + // 2. tbsCertificate + if (hCert.substr(a1[0], 2) != "30") + throw "malformed X.509 certificate PEM (code:002)"; // tbsCert not seq + + var a2 = ASN1HEX.getPosArrayOfChildren_AtObj(hCert, a1[0]); + + // 3. subjectPublicKeyInfo + if (a2.length < 7) + throw "malformed X.509 certificate PEM (code:003)"; // no subjPubKeyInfo + + var a3 = ASN1HEX.getPosArrayOfChildren_AtObj(hCert, a2[6]); + + if (a3.length != 2) + throw "malformed X.509 certificate PEM (code:004)"; // not AlgId and PubKey + + // 4. AlgId + var a4 = ASN1HEX.getPosArrayOfChildren_AtObj(hCert, a3[0]); + + if (a4.length != 2) + throw "malformed X.509 certificate PEM (code:005)"; // not 2 item in AlgId + + result.algoid = ASN1HEX.getHexOfV_AtObj(hCert, a4[0]); + + if (hCert.substr(a4[1], 2) == "06") { // EC + result.algparam = ASN1HEX.getHexOfV_AtObj(hCert, a4[1]); + } else if (hCert.substr(a4[1], 2) == "30") { // DSA + result.algparam = ASN1HEX.getHexOfTLV_AtObj(hCert, a4[1]); + } + + // 5. Public Key Hex + if (hCert.substr(a3[1], 2) != "03") + throw "malformed X.509 certificate PEM (code:006)"; // not bitstring + + var unusedBitAndKeyHex = ASN1HEX.getHexOfV_AtObj(hCert, a3[1]); + result.keyhex = unusedBitAndKeyHex.substr(2); + + return result; +}; + +/* + X509.prototype.readCertPEM = _x509_readCertPEM; + X509.prototype.readCertPEMWithoutRSAInit = _x509_readCertPEMWithoutRSAInit; + X509.prototype.getSerialNumberHex = _x509_getSerialNumberHex; + X509.prototype.getIssuerHex = _x509_getIssuerHex; + X509.prototype.getSubjectHex = _x509_getSubjectHex; + X509.prototype.getIssuerString = _x509_getIssuerString; + X509.prototype.getSubjectString = _x509_getSubjectString; + X509.prototype.getNotBefore = _x509_getNotBefore; + X509.prototype.getNotAfter = _x509_getNotAfter; +*/ +/*! crypto-1.1.5.js (c) 2013 Kenji Urushima | kjur.github.com/jsrsasign/license + */ +/* + * crypto.js - Cryptographic Algorithm Provider class + * + * Copyright (c) 2013 Kenji Urushima (kenji.urushima@gmail.com) + * + * This software is licensed under the terms of the MIT License. + * http://kjur.github.com/jsrsasign/license + * + * The above copyright and license notice shall be + * included in all copies or substantial portions of the Software. + */ + +/** + * @fileOverview + * @name crypto-1.1.js + * @author Kenji Urushima kenji.urushima@gmail.com + * @version 1.1.5 (2013-Oct-06) + * @since jsrsasign 2.2 + * @license MIT License + */ + +/** + * kjur's class library name space + * @name KJUR + * @namespace kjur's class library name space + */ +if (typeof KJUR == "undefined" || !KJUR) KJUR = {}; +/** + * kjur's cryptographic algorithm provider library name space + *

+ * This namespace privides following crytpgrahic classes. + *

    + *
  • {@link KJUR.crypto.MessageDigest} - Java JCE(cryptograhic extension) style MessageDigest class
  • + *
  • {@link KJUR.crypto.Signature} - Java JCE(cryptograhic extension) style Signature class
  • + *
  • {@link KJUR.crypto.Util} - cryptographic utility functions and properties
  • + *
+ * NOTE: Please ignore method summary and document of this namespace. This caused by a bug of jsdoc2. + *

+ * @name KJUR.crypto + * @namespace + */ +if (typeof KJUR.crypto == "undefined" || !KJUR.crypto) KJUR.crypto = {}; + +/** + * static object for cryptographic function utilities + * @name KJUR.crypto.Util + * @class static object for cryptographic function utilities + * @property {Array} DIGESTINFOHEAD PKCS#1 DigestInfo heading hexadecimal bytes for each hash algorithms + * @property {Array} DEFAULTPROVIDER associative array of default provider name for each hash and signature algorithms + * @description + */ +KJUR.crypto.Util = new function() { + this.DIGESTINFOHEAD = { + 'sha1': "3021300906052b0e03021a05000414", + 'sha224': "302d300d06096086480165030402040500041c", + 'sha256': "3031300d060960864801650304020105000420", + 'sha384': "3041300d060960864801650304020205000430", + 'sha512': "3051300d060960864801650304020305000440", + 'md2': "3020300c06082a864886f70d020205000410", + 'md5': "3020300c06082a864886f70d020505000410", + 'ripemd160': "3021300906052b2403020105000414", + }; + + /* + * @since crypto 1.1.1 + */ + this.DEFAULTPROVIDER = { + 'md5': 'cryptojs', + 'sha1': 'cryptojs', + 'sha224': 'cryptojs', + 'sha256': 'cryptojs', + 'sha384': 'cryptojs', + 'sha512': 'cryptojs', + 'ripemd160': 'cryptojs', + 'hmacmd5': 'cryptojs', + 'hmacsha1': 'cryptojs', + 'hmacsha224': 'cryptojs', + 'hmacsha256': 'cryptojs', + 'hmacsha384': 'cryptojs', + 'hmacsha512': 'cryptojs', + 'hmacripemd160': 'cryptojs', + + 'MD5withRSA': 'cryptojs/jsrsa', + 'SHA1withRSA': 'cryptojs/jsrsa', + 'SHA224withRSA': 'cryptojs/jsrsa', + 'SHA256withRSA': 'cryptojs/jsrsa', + 'SHA384withRSA': 'cryptojs/jsrsa', + 'SHA512withRSA': 'cryptojs/jsrsa', + 'RIPEMD160withRSA': 'cryptojs/jsrsa', + + 'MD5withECDSA': 'cryptojs/jsrsa', + 'SHA1withECDSA': 'cryptojs/jsrsa', + 'SHA224withECDSA': 'cryptojs/jsrsa', + 'SHA256withECDSA': 'cryptojs/jsrsa', + 'SHA384withECDSA': 'cryptojs/jsrsa', + 'SHA512withECDSA': 'cryptojs/jsrsa', + 'RIPEMD160withECDSA': 'cryptojs/jsrsa', + + 'SHA1withDSA': 'cryptojs/jsrsa', + 'SHA224withDSA': 'cryptojs/jsrsa', + 'SHA256withDSA': 'cryptojs/jsrsa', + + 'MD5withRSAandMGF1': 'cryptojs/jsrsa', + 'SHA1withRSAandMGF1': 'cryptojs/jsrsa', + 'SHA224withRSAandMGF1': 'cryptojs/jsrsa', + 'SHA256withRSAandMGF1': 'cryptojs/jsrsa', + 'SHA384withRSAandMGF1': 'cryptojs/jsrsa', + 'SHA512withRSAandMGF1': 'cryptojs/jsrsa', + 'RIPEMD160withRSAandMGF1': 'cryptojs/jsrsa', + }; + + /* + * @since crypto 1.1.2 + */ + this.CRYPTOJSMESSAGEDIGESTNAME = { + 'md5': 'CryptoJS.algo.MD5', + 'sha1': 'CryptoJS.algo.SHA1', + 'sha224': 'CryptoJS.algo.SHA224', + 'sha256': 'CryptoJS.algo.SHA256', + 'sha384': 'CryptoJS.algo.SHA384', + 'sha512': 'CryptoJS.algo.SHA512', + 'ripemd160': 'CryptoJS.algo.RIPEMD160' + }; + + /** + * get hexadecimal DigestInfo + * @name getDigestInfoHex + * @memberOf KJUR.crypto.Util + * @function + * @param {String} hHash hexadecimal hash value + * @param {String} alg hash algorithm name (ex. 'sha1') + * @return {String} hexadecimal string DigestInfo ASN.1 structure + */ + this.getDigestInfoHex = function(hHash, alg) { + if (typeof this.DIGESTINFOHEAD[alg] == "undefined") + throw "alg not supported in Util.DIGESTINFOHEAD: " + alg; + return this.DIGESTINFOHEAD[alg] + hHash; + }; + + /** + * get PKCS#1 padded hexadecimal DigestInfo + * @name getPaddedDigestInfoHex + * @memberOf KJUR.crypto.Util + * @function + * @param {String} hHash hexadecimal hash value of message to be signed + * @param {String} alg hash algorithm name (ex. 'sha1') + * @param {Integer} keySize key bit length (ex. 1024) + * @return {String} hexadecimal string of PKCS#1 padded DigestInfo + */ + this.getPaddedDigestInfoHex = function(hHash, alg, keySize) { + var hDigestInfo = this.getDigestInfoHex(hHash, alg); + var pmStrLen = keySize / 4; // minimum PM length + + if (hDigestInfo.length + 22 > pmStrLen) // len(0001+ff(*8)+00+hDigestInfo)=22 + throw "key is too short for SigAlg: keylen=" + keySize + "," + alg; + + var hHead = "0001"; + var hTail = "00" + hDigestInfo; + var hMid = ""; + var fLen = pmStrLen - hHead.length - hTail.length; + for (var i = 0; i < fLen; i += 2) { + hMid += "ff"; + } + var hPaddedMessage = hHead + hMid + hTail; + return hPaddedMessage; + }; + + /** + * get hexadecimal hash of string with specified algorithm + * @name hashString + * @memberOf KJUR.crypto.Util + * @function + * @param {String} s input string to be hashed + * @param {String} alg hash algorithm name + * @return {String} hexadecimal string of hash value + * @since 1.1.1 + */ + this.hashString = function(s, alg) { + var md = new KJUR.crypto.MessageDigest({'alg': alg}); + return md.digestString(s); + }; + + /** + * get hexadecimal hash of hexadecimal string with specified algorithm + * @name hashHex + * @memberOf KJUR.crypto.Util + * @function + * @param {String} sHex input hexadecimal string to be hashed + * @param {String} alg hash algorithm name + * @return {String} hexadecimal string of hash value + * @since 1.1.1 + */ + this.hashHex = function(sHex, alg) { + var md = new KJUR.crypto.MessageDigest({'alg': alg}); + return md.digestHex(sHex); + }; + + /** + * get hexadecimal SHA1 hash of string + * @name sha1 + * @memberOf KJUR.crypto.Util + * @function + * @param {String} s input string to be hashed + * @return {String} hexadecimal string of hash value + * @since 1.0.3 + */ + this.sha1 = function(s) { + var md = new KJUR.crypto.MessageDigest({'alg':'sha1', 'prov':'cryptojs'}); + return md.digestString(s); + }; + + /** + * get hexadecimal SHA256 hash of string + * @name sha256 + * @memberOf KJUR.crypto.Util + * @function + * @param {String} s input string to be hashed + * @return {String} hexadecimal string of hash value + * @since 1.0.3 + */ + this.sha256 = function(s) { + var md = new KJUR.crypto.MessageDigest({'alg':'sha256', 'prov':'cryptojs'}); + return md.digestString(s); + }; + + this.sha256Hex = function(s) { + var md = new KJUR.crypto.MessageDigest({'alg':'sha256', 'prov':'cryptojs'}); + return md.digestHex(s); + }; + + /** + * get hexadecimal SHA512 hash of string + * @name sha512 + * @memberOf KJUR.crypto.Util + * @function + * @param {String} s input string to be hashed + * @return {String} hexadecimal string of hash value + * @since 1.0.3 + */ + this.sha512 = function(s) { + var md = new KJUR.crypto.MessageDigest({'alg':'sha512', 'prov':'cryptojs'}); + return md.digestString(s); + }; + + this.sha512Hex = function(s) { + var md = new KJUR.crypto.MessageDigest({'alg':'sha512', 'prov':'cryptojs'}); + return md.digestHex(s); + }; + + /** + * get hexadecimal MD5 hash of string + * @name md5 + * @memberOf KJUR.crypto.Util + * @function + * @param {String} s input string to be hashed + * @return {String} hexadecimal string of hash value + * @since 1.0.3 + */ + this.md5 = function(s) { + var md = new KJUR.crypto.MessageDigest({'alg':'md5', 'prov':'cryptojs'}); + return md.digestString(s); + }; + + /** + * get hexadecimal RIPEMD160 hash of string + * @name ripemd160 + * @memberOf KJUR.crypto.Util + * @function + * @param {String} s input string to be hashed + * @return {String} hexadecimal string of hash value + * @since 1.0.3 + */ + this.ripemd160 = function(s) { + var md = new KJUR.crypto.MessageDigest({'alg':'ripemd160', 'prov':'cryptojs'}); + return md.digestString(s); + }; + + /* + * @since 1.1.2 + */ + this.getCryptoJSMDByName = function(s) { + + }; +}; + +/** + * MessageDigest class which is very similar to java.security.MessageDigest class + * @name KJUR.crypto.MessageDigest + * @class MessageDigest class which is very similar to java.security.MessageDigest class + * @param {Array} params parameters for constructor + * @description + *
+ * Currently this supports following algorithm and providers combination: + *
    + *
  • md5 - cryptojs
  • + *
  • sha1 - cryptojs
  • + *
  • sha224 - cryptojs
  • + *
  • sha256 - cryptojs
  • + *
  • sha384 - cryptojs
  • + *
  • sha512 - cryptojs
  • + *
  • ripemd160 - cryptojs
  • + *
  • sha256 - sjcl (NEW from crypto.js 1.0.4)
  • + *
+ * @example + * // CryptoJS provider sample + * <script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/components/core.js"></script> + * <script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/components/sha1.js"></script> + * <script src="crypto-1.0.js"></script> + * var md = new KJUR.crypto.MessageDigest({alg: "sha1", prov: "cryptojs"}); + * md.updateString('aaa') + * var mdHex = md.digest() + * + * // SJCL(Stanford JavaScript Crypto Library) provider sample + * <script src="http://bitwiseshiftleft.github.io/sjcl/sjcl.js"></script> + * <script src="crypto-1.0.js"></script> + * var md = new KJUR.crypto.MessageDigest({alg: "sha256", prov: "sjcl"}); // sjcl supports sha256 only + * md.updateString('aaa') + * var mdHex = md.digest() + */ +KJUR.crypto.MessageDigest = function(params) { + var md = null; + var algName = null; + var provName = null; + + /** + * set hash algorithm and provider + * @name setAlgAndProvider + * @memberOf KJUR.crypto.MessageDigest + * @function + * @param {String} alg hash algorithm name + * @param {String} prov provider name + * @description + * @example + * // for SHA1 + * md.setAlgAndProvider('sha1', 'cryptojs'); + * // for RIPEMD160 + * md.setAlgAndProvider('ripemd160', 'cryptojs'); + */ + this.setAlgAndProvider = function(alg, prov) { + if (alg != null && prov === undefined) prov = KJUR.crypto.Util.DEFAULTPROVIDER[alg]; + + // for cryptojs + if (':md5:sha1:sha224:sha256:sha384:sha512:ripemd160:'.indexOf(alg) != -1 && + prov == 'cryptojs') { + try { + this.md = eval(KJUR.crypto.Util.CRYPTOJSMESSAGEDIGESTNAME[alg]).create(); + } catch (ex) { + throw "setAlgAndProvider hash alg set fail alg=" + alg + "/" + ex; + } + this.updateString = function(str) { + this.md.update(str); + }; + this.updateHex = function(hex) { + var wHex = CryptoJS.enc.Hex.parse(hex); + this.md.update(wHex); + }; + this.digest = function() { + var hash = this.md.finalize(); + return hash.toString(CryptoJS.enc.Hex); + }; + this.digestString = function(str) { + this.updateString(str); + return this.digest(); + }; + this.digestHex = function(hex) { + this.updateHex(hex); + return this.digest(); + }; + } + if (':sha256:'.indexOf(alg) != -1 && + prov == 'sjcl') { + try { + this.md = new sjcl.hash.sha256(); + } catch (ex) { + throw "setAlgAndProvider hash alg set fail alg=" + alg + "/" + ex; + } + this.updateString = function(str) { + this.md.update(str); + }; + this.updateHex = function(hex) { + var baHex = sjcl.codec.hex.toBits(hex); + this.md.update(baHex); + }; + this.digest = function() { + var hash = this.md.finalize(); + return sjcl.codec.hex.fromBits(hash); + }; + this.digestString = function(str) { + this.updateString(str); + return this.digest(); + }; + this.digestHex = function(hex) { + this.updateHex(hex); + return this.digest(); + }; + } + }; + + /** + * update digest by specified string + * @name updateString + * @memberOf KJUR.crypto.MessageDigest + * @function + * @param {String} str string to update + * @description + * @example + * md.updateString('New York'); + */ + this.updateString = function(str) { + throw "updateString(str) not supported for this alg/prov: " + this.algName + "/" + this.provName; + }; + + /** + * update digest by specified hexadecimal string + * @name updateHex + * @memberOf KJUR.crypto.MessageDigest + * @function + * @param {String} hex hexadecimal string to update + * @description + * @example + * md.updateHex('0afe36'); + */ + this.updateHex = function(hex) { + throw "updateHex(hex) not supported for this alg/prov: " + this.algName + "/" + this.provName; + }; + + /** + * completes hash calculation and returns hash result + * @name digest + * @memberOf KJUR.crypto.MessageDigest + * @function + * @description + * @example + * md.digest() + */ + this.digest = function() { + throw "digest() not supported for this alg/prov: " + this.algName + "/" + this.provName; + }; + + /** + * performs final update on the digest using string, then completes the digest computation + * @name digestString + * @memberOf KJUR.crypto.MessageDigest + * @function + * @param {String} str string to final update + * @description + * @example + * md.digestString('aaa') + */ + this.digestString = function(str) { + throw "digestString(str) not supported for this alg/prov: " + this.algName + "/" + this.provName; + }; + + /** + * performs final update on the digest using hexadecimal string, then completes the digest computation + * @name digestHex + * @memberOf KJUR.crypto.MessageDigest + * @function + * @param {String} hex hexadecimal string to final update + * @description + * @example + * md.digestHex('0f2abd') + */ + this.digestHex = function(hex) { + throw "digestHex(hex) not supported for this alg/prov: " + this.algName + "/" + this.provName; + }; + + if (params !== undefined) { + if (params['alg'] !== undefined) { + this.algName = params['alg']; + if (params['prov'] === undefined) + this.provName = KJUR.crypto.Util.DEFAULTPROVIDER[this.algName]; + this.setAlgAndProvider(this.algName, this.provName); + } + } +}; + +/** + * Mac(Message Authentication Code) class which is very similar to java.security.Mac class + * @name KJUR.crypto.Mac + * @class Mac class which is very similar to java.security.Mac class + * @param {Array} params parameters for constructor + * @description + *
+ * Currently this supports following algorithm and providers combination: + *
    + *
  • hmacmd5 - cryptojs
  • + *
  • hmacsha1 - cryptojs
  • + *
  • hmacsha224 - cryptojs
  • + *
  • hmacsha256 - cryptojs
  • + *
  • hmacsha384 - cryptojs
  • + *
  • hmacsha512 - cryptojs
  • + *
+ * NOTE: HmacSHA224 and HmacSHA384 issue was fixed since jsrsasign 4.1.4. + * Please use 'ext/cryptojs-312-core-fix*.js' instead of 'core.js' of original CryptoJS + * to avoid those issue. + * @example + * var mac = new KJUR.crypto.Mac({alg: "HmacSHA1", prov: "cryptojs", "pass": "pass"}); + * mac.updateString('aaa') + * var macHex = md.doFinal() + */ +KJUR.crypto.Mac = function(params) { + var mac = null; + var pass = null; + var algName = null; + var provName = null; + var algProv = null; + + this.setAlgAndProvider = function(alg, prov) { + if (alg == null) alg = "hmacsha1"; + + alg = alg.toLowerCase(); + if (alg.substr(0, 4) != "hmac") { + throw "setAlgAndProvider unsupported HMAC alg: " + alg; + } + + if (prov === undefined) prov = KJUR.crypto.Util.DEFAULTPROVIDER[alg]; + this.algProv = alg + "/" + prov; + + var hashAlg = alg.substr(4); + + // for cryptojs + if (':md5:sha1:sha224:sha256:sha384:sha512:ripemd160:'.indexOf(hashAlg) != -1 && + prov == 'cryptojs') { + try { + var mdObj = eval(KJUR.crypto.Util.CRYPTOJSMESSAGEDIGESTNAME[hashAlg]); + this.mac = CryptoJS.algo.HMAC.create(mdObj, this.pass); + } catch (ex) { + throw "setAlgAndProvider hash alg set fail hashAlg=" + hashAlg + "/" + ex; + } + this.updateString = function(str) { + this.mac.update(str); + }; + this.updateHex = function(hex) { + var wHex = CryptoJS.enc.Hex.parse(hex); + this.mac.update(wHex); + }; + this.doFinal = function() { + var hash = this.mac.finalize(); + return hash.toString(CryptoJS.enc.Hex); + }; + this.doFinalString = function(str) { + this.updateString(str); + return this.doFinal(); + }; + this.doFinalHex = function(hex) { + this.updateHex(hex); + return this.doFinal(); + }; + } + }; + + /** + * update digest by specified string + * @name updateString + * @memberOf KJUR.crypto.Mac + * @function + * @param {String} str string to update + * @description + * @example + * md.updateString('New York'); + */ + this.updateString = function(str) { + throw "updateString(str) not supported for this alg/prov: " + this.algProv; + }; + + /** + * update digest by specified hexadecimal string + * @name updateHex + * @memberOf KJUR.crypto.Mac + * @function + * @param {String} hex hexadecimal string to update + * @description + * @example + * md.updateHex('0afe36'); + */ + this.updateHex = function(hex) { + throw "updateHex(hex) not supported for this alg/prov: " + this.algProv; + }; + + /** + * completes hash calculation and returns hash result + * @name doFinal + * @memberOf KJUR.crypto.Mac + * @function + * @description + * @example + * md.digest() + */ + this.doFinal = function() { + throw "digest() not supported for this alg/prov: " + this.algProv; + }; + + /** + * performs final update on the digest using string, then completes the digest computation + * @name doFinalString + * @memberOf KJUR.crypto.Mac + * @function + * @param {String} str string to final update + * @description + * @example + * md.digestString('aaa') + */ + this.doFinalString = function(str) { + throw "digestString(str) not supported for this alg/prov: " + this.algProv; + }; + + /** + * performs final update on the digest using hexadecimal string, + * then completes the digest computation + * @name doFinalHex + * @memberOf KJUR.crypto.Mac + * @function + * @param {String} hex hexadecimal string to final update + * @description + * @example + * md.digestHex('0f2abd') + */ + this.doFinalHex = function(hex) { + throw "digestHex(hex) not supported for this alg/prov: " + this.algProv; + }; + + if (params !== undefined) { + if (params['pass'] !== undefined) { + this.pass = params['pass']; + } + if (params['alg'] !== undefined) { + this.algName = params['alg']; + if (params['prov'] === undefined) + this.provName = KJUR.crypto.Util.DEFAULTPROVIDER[this.algName]; + this.setAlgAndProvider(this.algName, this.provName); + } + } +}; + +/** + * Signature class which is very similar to java.security.Signature class + * @name KJUR.crypto.Signature + * @class Signature class which is very similar to java.security.Signature class + * @param {Array} params parameters for constructor + * @property {String} state Current state of this signature object whether 'SIGN', 'VERIFY' or null + * @description + *
+ * As for params of constructor's argument, it can be specify following attributes: + *
    + *
  • alg - signature algorithm name (ex. {MD5,SHA1,SHA224,SHA256,SHA384,SHA512,RIPEMD160}with{RSA,ECDSA,DSA})
  • + *
  • provider - currently 'cryptojs/jsrsa' only
  • + *
+ *

SUPPORTED ALGORITHMS AND PROVIDERS

+ * This Signature class supports following signature algorithm and provider names: + *
    + *
  • MD5withRSA - cryptojs/jsrsa
  • + *
  • SHA1withRSA - cryptojs/jsrsa
  • + *
  • SHA224withRSA - cryptojs/jsrsa
  • + *
  • SHA256withRSA - cryptojs/jsrsa
  • + *
  • SHA384withRSA - cryptojs/jsrsa
  • + *
  • SHA512withRSA - cryptojs/jsrsa
  • + *
  • RIPEMD160withRSA - cryptojs/jsrsa
  • + *
  • MD5withECDSA - cryptojs/jsrsa
  • + *
  • SHA1withECDSA - cryptojs/jsrsa
  • + *
  • SHA224withECDSA - cryptojs/jsrsa
  • + *
  • SHA256withECDSA - cryptojs/jsrsa
  • + *
  • SHA384withECDSA - cryptojs/jsrsa
  • + *
  • SHA512withECDSA - cryptojs/jsrsa
  • + *
  • RIPEMD160withECDSA - cryptojs/jsrsa
  • + *
  • MD5withRSAandMGF1 - cryptojs/jsrsa
  • + *
  • SHA1withRSAandMGF1 - cryptojs/jsrsa
  • + *
  • SHA224withRSAandMGF1 - cryptojs/jsrsa
  • + *
  • SHA256withRSAandMGF1 - cryptojs/jsrsa
  • + *
  • SHA384withRSAandMGF1 - cryptojs/jsrsa
  • + *
  • SHA512withRSAandMGF1 - cryptojs/jsrsa
  • + *
  • RIPEMD160withRSAandMGF1 - cryptojs/jsrsa
  • + *
  • SHA1withDSA - cryptojs/jsrsa
  • + *
  • SHA224withDSA - cryptojs/jsrsa
  • + *
  • SHA256withDSA - cryptojs/jsrsa
  • + *
+ * Here are supported elliptic cryptographic curve names and their aliases for ECDSA: + *
    + *
  • secp256k1
  • + *
  • secp256r1, NIST P-256, P-256, prime256v1
  • + *
  • secp384r1, NIST P-384, P-384
  • + *
+ * NOTE1: DSA signing algorithm is also supported since crypto 1.1.5. + *

EXAMPLES

+ * @example + * // RSA signature generation + * var sig = new KJUR.crypto.Signature({"alg": "SHA1withRSA"}); + * sig.init(prvKeyPEM); + * sig.updateString('aaa'); + * var hSigVal = sig.sign(); + * + * // DSA signature validation + * var sig2 = new KJUR.crypto.Signature({"alg": "SHA1withDSA"}); + * sig2.init(certPEM); + * sig.updateString('aaa'); + * var isValid = sig2.verify(hSigVal); + * + * // ECDSA signing + * var sig = new KJUR.crypto.Signature({'alg':'SHA1withECDSA'}); + * sig.init(prvKeyPEM); + * sig.updateString('aaa'); + * var sigValueHex = sig.sign(); + * + * // ECDSA verifying + * var sig2 = new KJUR.crypto.Signature({'alg':'SHA1withECDSA'}); + * sig.init(certPEM); + * sig.updateString('aaa'); + * var isValid = sig.verify(sigValueHex); + */ +KJUR.crypto.Signature = function(params) { + var prvKey = null; // RSAKey/KJUR.crypto.{ECDSA,DSA} object for signing + var pubKey = null; // RSAKey/KJUR.crypto.{ECDSA,DSA} object for verifying + + var md = null; // KJUR.crypto.MessageDigest object + var sig = null; + var algName = null; + var provName = null; + var algProvName = null; + var mdAlgName = null; + var pubkeyAlgName = null; // rsa,ecdsa,rsaandmgf1(=rsapss) + var state = null; + var pssSaltLen = -1; + var initParams = null; + + var sHashHex = null; // hex hash value for hex + var hDigestInfo = null; + var hPaddedDigestInfo = null; + var hSign = null; + + this._setAlgNames = function() { + if (this.algName.match(/^(.+)with(.+)$/)) { + this.mdAlgName = RegExp.$1.toLowerCase(); + this.pubkeyAlgName = RegExp.$2.toLowerCase(); + } + }; + + this._zeroPaddingOfSignature = function(hex, bitLength) { + var s = ""; + var nZero = bitLength / 4 - hex.length; + for (var i = 0; i < nZero; i++) { + s = s + "0"; + } + return s + hex; + }; + + /** + * set signature algorithm and provider + * @name setAlgAndProvider + * @memberOf KJUR.crypto.Signature + * @function + * @param {String} alg signature algorithm name + * @param {String} prov provider name + * @description + * @example + * md.setAlgAndProvider('SHA1withRSA', 'cryptojs/jsrsa'); + */ + this.setAlgAndProvider = function(alg, prov) { + this._setAlgNames(); + if (prov != 'cryptojs/jsrsa') + throw "provider not supported: " + prov; + + if (':md5:sha1:sha224:sha256:sha384:sha512:ripemd160:'.indexOf(this.mdAlgName) != -1) { + try { + this.md = new KJUR.crypto.MessageDigest({'alg':this.mdAlgName}); + } catch (ex) { + throw "setAlgAndProvider hash alg set fail alg=" + + this.mdAlgName + "/" + ex; + } + + this.init = function(keyparam, pass) { + var keyObj = null; + try { + if (pass === undefined) { + keyObj = KEYUTIL.getKey(keyparam); + } else { + keyObj = KEYUTIL.getKey(keyparam, pass); + } + } catch (ex) { + throw "init failed:" + ex; + } + + if (keyObj.isPrivate === true) { + this.prvKey = keyObj; + this.state = "SIGN"; + } else if (keyObj.isPublic === true) { + this.pubKey = keyObj; + this.state = "VERIFY"; + } else { + throw "init failed.:" + keyObj; + } + }; + + this.initSign = function(params) { + if (typeof params['ecprvhex'] == 'string' && + typeof params['eccurvename'] == 'string') { + this.ecprvhex = params['ecprvhex']; + this.eccurvename = params['eccurvename']; + } else { + this.prvKey = params; + } + this.state = "SIGN"; + }; + + this.initVerifyByPublicKey = function(params) { + if (typeof params['ecpubhex'] == 'string' && + typeof params['eccurvename'] == 'string') { + this.ecpubhex = params['ecpubhex']; + this.eccurvename = params['eccurvename']; + } else if (params instanceof KJUR.crypto.ECDSA) { + this.pubKey = params; + } else if (params instanceof RSAKey) { + this.pubKey = params; + } + this.state = "VERIFY"; + }; + + this.initVerifyByCertificatePEM = function(certPEM) { + var x509 = new X509(); + x509.readCertPEM(certPEM); + this.pubKey = x509.subjectPublicKeyRSA; + this.state = "VERIFY"; + }; + + this.updateString = function(str) { + this.md.updateString(str); + }; + this.updateHex = function(hex) { + this.md.updateHex(hex); + }; + + this.sign = function() { + this.sHashHex = this.md.digest(); + if (typeof this.ecprvhex != "undefined" && + typeof this.eccurvename != "undefined") { + var ec = new KJUR.crypto.ECDSA({'curve': this.eccurvename}); + this.hSign = ec.signHex(this.sHashHex, this.ecprvhex); + } else if (this.pubkeyAlgName == "rsaandmgf1") { + this.hSign = this.prvKey.signWithMessageHashPSS(this.sHashHex, + this.mdAlgName, + this.pssSaltLen); + } else if (this.pubkeyAlgName == "rsa") { + this.hSign = this.prvKey.signWithMessageHash(this.sHashHex, + this.mdAlgName); + } else if (this.prvKey instanceof KJUR.crypto.ECDSA) { + this.hSign = this.prvKey.signWithMessageHash(this.sHashHex); + } else if (this.prvKey instanceof KJUR.crypto.DSA) { + this.hSign = this.prvKey.signWithMessageHash(this.sHashHex); + } else { + throw "Signature: unsupported public key alg: " + this.pubkeyAlgName; + } + return this.hSign; + }; + this.signString = function(str) { + this.updateString(str); + return this.sign(); + }; + this.signHex = function(hex) { + this.updateHex(hex); + return this.sign(); + }; + this.verify = function(hSigVal) { + this.sHashHex = this.md.digest(); + if (typeof this.ecpubhex != "undefined" && + typeof this.eccurvename != "undefined") { + var ec = new KJUR.crypto.ECDSA({curve: this.eccurvename}); + return ec.verifyHex(this.sHashHex, hSigVal, this.ecpubhex); + } else if (this.pubkeyAlgName == "rsaandmgf1") { + return this.pubKey.verifyWithMessageHashPSS(this.sHashHex, hSigVal, + this.mdAlgName, + this.pssSaltLen); + } else if (this.pubkeyAlgName == "rsa") { + return this.pubKey.verifyWithMessageHash(this.sHashHex, hSigVal); + } else if (this.pubKey instanceof KJUR.crypto.ECDSA) { + return this.pubKey.verifyWithMessageHash(this.sHashHex, hSigVal); + } else if (this.pubKey instanceof KJUR.crypto.DSA) { + return this.pubKey.verifyWithMessageHash(this.sHashHex, hSigVal); + } else { + throw "Signature: unsupported public key alg: " + this.pubkeyAlgName; + } + }; + } + }; + + /** + * Initialize this object for signing or verifying depends on key + * @name init + * @memberOf KJUR.crypto.Signature + * @function + * @param {Object} key specifying public or private key as plain/encrypted PKCS#5/8 PEM file, certificate PEM or {@link RSAKey}, {@link KJUR.crypto.DSA} or {@link KJUR.crypto.ECDSA} object + * @param {String} pass (OPTION) passcode for encrypted private key + * @since crypto 1.1.3 + * @description + * This method is very useful initialize method for Signature class since + * you just specify key then this method will automatically initialize it + * using {@link KEYUTIL.getKey} method. + * As for 'key', following argument type are supported: + *
signing
+ *
    + *
  • PEM formatted PKCS#8 encrypted RSA/ECDSA private key concluding "BEGIN ENCRYPTED PRIVATE KEY"
  • + *
  • PEM formatted PKCS#5 encrypted RSA/DSA private key concluding "BEGIN RSA/DSA PRIVATE KEY" and ",ENCRYPTED"
  • + *
  • PEM formatted PKCS#8 plain RSA/ECDSA private key concluding "BEGIN PRIVATE KEY"
  • + *
  • PEM formatted PKCS#5 plain RSA/DSA private key concluding "BEGIN RSA/DSA PRIVATE KEY" without ",ENCRYPTED"
  • + *
  • RSAKey object of private key
  • + *
  • KJUR.crypto.ECDSA object of private key
  • + *
  • KJUR.crypto.DSA object of private key
  • + *
+ *
verification
+ *
    + *
  • PEM formatted PKCS#8 RSA/EC/DSA public key concluding "BEGIN PUBLIC KEY"
  • + *
  • PEM formatted X.509 certificate with RSA/EC/DSA public key concluding + * "BEGIN CERTIFICATE", "BEGIN X509 CERTIFICATE" or "BEGIN TRUSTED CERTIFICATE".
  • + *
  • RSAKey object of public key
  • + *
  • KJUR.crypto.ECDSA object of public key
  • + *
  • KJUR.crypto.DSA object of public key
  • + *
+ * @example + * sig.init(sCertPEM) + */ + this.init = function(key, pass) { + throw "init(key, pass) not supported for this alg:prov=" + + this.algProvName; + }; + + /** + * Initialize this object for verifying with a public key + * @name initVerifyByPublicKey + * @memberOf KJUR.crypto.Signature + * @function + * @param {Object} param RSAKey object of public key or associative array for ECDSA + * @since 1.0.2 + * @deprecated from crypto 1.1.5. please use init() method instead. + * @description + * Public key information will be provided as 'param' parameter and the value will be + * following: + *
    + *
  • {@link RSAKey} object for RSA verification
  • + *
  • associative array for ECDSA verification + * (ex. {'ecpubhex': '041f..', 'eccurvename': 'secp256r1'}) + *
  • + *
+ * @example + * sig.initVerifyByPublicKey(rsaPrvKey) + */ + this.initVerifyByPublicKey = function(rsaPubKey) { + throw "initVerifyByPublicKey(rsaPubKeyy) not supported for this alg:prov=" + + this.algProvName; + }; + + /** + * Initialize this object for verifying with a certficate + * @name initVerifyByCertificatePEM + * @memberOf KJUR.crypto.Signature + * @function + * @param {String} certPEM PEM formatted string of certificate + * @since 1.0.2 + * @deprecated from crypto 1.1.5. please use init() method instead. + * @description + * @example + * sig.initVerifyByCertificatePEM(certPEM) + */ + this.initVerifyByCertificatePEM = function(certPEM) { + throw "initVerifyByCertificatePEM(certPEM) not supported for this alg:prov=" + + this.algProvName; + }; + + /** + * Initialize this object for signing + * @name initSign + * @memberOf KJUR.crypto.Signature + * @function + * @param {Object} param RSAKey object of public key or associative array for ECDSA + * @deprecated from crypto 1.1.5. please use init() method instead. + * @description + * Private key information will be provided as 'param' parameter and the value will be + * following: + *
    + *
  • {@link RSAKey} object for RSA signing
  • + *
  • associative array for ECDSA signing + * (ex. {'ecprvhex': '1d3f..', 'eccurvename': 'secp256r1'})
  • + *
+ * @example + * sig.initSign(prvKey) + */ + this.initSign = function(prvKey) { + throw "initSign(prvKey) not supported for this alg:prov=" + this.algProvName; + }; + + /** + * Updates the data to be signed or verified by a string + * @name updateString + * @memberOf KJUR.crypto.Signature + * @function + * @param {String} str string to use for the update + * @description + * @example + * sig.updateString('aaa') + */ + this.updateString = function(str) { + throw "updateString(str) not supported for this alg:prov=" + this.algProvName; + }; + + /** + * Updates the data to be signed or verified by a hexadecimal string + * @name updateHex + * @memberOf KJUR.crypto.Signature + * @function + * @param {String} hex hexadecimal string to use for the update + * @description + * @example + * sig.updateHex('1f2f3f') + */ + this.updateHex = function(hex) { + throw "updateHex(hex) not supported for this alg:prov=" + this.algProvName; + }; + + /** + * Returns the signature bytes of all data updates as a hexadecimal string + * @name sign + * @memberOf KJUR.crypto.Signature + * @function + * @return the signature bytes as a hexadecimal string + * @description + * @example + * var hSigValue = sig.sign() + */ + this.sign = function() { + throw "sign() not supported for this alg:prov=" + this.algProvName; + }; + + /** + * performs final update on the sign using string, then returns the signature bytes of all data updates as a hexadecimal string + * @name signString + * @memberOf KJUR.crypto.Signature + * @function + * @param {String} str string to final update + * @return the signature bytes of a hexadecimal string + * @description + * @example + * var hSigValue = sig.signString('aaa') + */ + this.signString = function(str) { + throw "digestString(str) not supported for this alg:prov=" + this.algProvName; + }; + + /** + * performs final update on the sign using hexadecimal string, then returns the signature bytes of all data updates as a hexadecimal string + * @name signHex + * @memberOf KJUR.crypto.Signature + * @function + * @param {String} hex hexadecimal string to final update + * @return the signature bytes of a hexadecimal string + * @description + * @example + * var hSigValue = sig.signHex('1fdc33') + */ + this.signHex = function(hex) { + throw "digestHex(hex) not supported for this alg:prov=" + this.algProvName; + }; + + /** + * verifies the passed-in signature. + * @name verify + * @memberOf KJUR.crypto.Signature + * @function + * @param {String} str string to final update + * @return {Boolean} true if the signature was verified, otherwise false + * @description + * @example + * var isValid = sig.verify('1fbcefdca4823a7(snip)') + */ + this.verify = function(hSigVal) { + throw "verify(hSigVal) not supported for this alg:prov=" + this.algProvName; + }; + + this.initParams = params; + + if (params !== undefined) { + if (params['alg'] !== undefined) { + this.algName = params['alg']; + if (params['prov'] === undefined) { + this.provName = KJUR.crypto.Util.DEFAULTPROVIDER[this.algName]; + } else { + this.provName = params['prov']; + } + this.algProvName = this.algName + ":" + this.provName; + this.setAlgAndProvider(this.algName, this.provName); + this._setAlgNames(); + } + + if (params['psssaltlen'] !== undefined) this.pssSaltLen = params['psssaltlen']; + + if (params['prvkeypem'] !== undefined) { + if (params['prvkeypas'] !== undefined) { + throw "both prvkeypem and prvkeypas parameters not supported"; + } else { + try { + var prvKey = new RSAKey(); + prvKey.readPrivateKeyFromPEMString(params['prvkeypem']); + this.initSign(prvKey); + } catch (ex) { + throw "fatal error to load pem private key: " + ex; + } + } + } + } +}; + +/** + * static object for cryptographic function utilities + * @name KJUR.crypto.OID + * @class static object for cryptography related OIDs + * @property {Array} oidhex2name key value of hexadecimal OID and its name + * (ex. '2a8648ce3d030107' and 'secp256r1') + * @since crypto 1.1.3 + * @description + */ + + +KJUR.crypto.OID = new function() { + this.oidhex2name = { + '2a864886f70d010101': 'rsaEncryption', + '2a8648ce3d0201': 'ecPublicKey', + '2a8648ce380401': 'dsa', + '2a8648ce3d030107': 'secp256r1', + '2b8104001f': 'secp192k1', + '2b81040021': 'secp224r1', + '2b8104000a': 'secp256k1', + '2b81040023': 'secp521r1', + '2b81040022': 'secp384r1', + '2a8648ce380403': 'SHA1withDSA', // 1.2.840.10040.4.3 + '608648016503040301': 'SHA224withDSA', // 2.16.840.1.101.3.4.3.1 + '608648016503040302': 'SHA256withDSA', // 2.16.840.1.101.3.4.3.2 + }; +}; + +/*! base64x-1.1.3 (c) 2012-2014 Kenji Urushima | kjur.github.com/jsjws/license + */ +/* + * base64x.js - Base64url and supplementary functions for Tom Wu's base64.js library + * + * version: 1.1.3 (2014 May 25) + * + * Copyright (c) 2012-2014 Kenji Urushima (kenji.urushima@gmail.com) + * + * This software is licensed under the terms of the MIT License. + * http://kjur.github.com/jsjws/license/ + * + * The above copyright and license notice shall be + * included in all copies or substantial portions of the Software. + * + * DEPENDS ON: + * - base64.js - Tom Wu's Base64 library + */ + +/** + * Base64URL and supplementary functions for Tom Wu's base64.js library.
+ * This class is just provide information about global functions + * defined in 'base64x.js'. The 'base64x.js' script file provides + * global functions for converting following data each other. + *
    + *
  • (ASCII) String
  • + *
  • UTF8 String including CJK, Latin and other characters
  • + *
  • byte array
  • + *
  • hexadecimal encoded String
  • + *
  • Full URIComponent encoded String (such like "%69%94")
  • + *
  • Base64 encoded String
  • + *
  • Base64URL encoded String
  • + *
+ * All functions in 'base64x.js' are defined in {@link _global_} and not + * in this class. + * + * @class Base64URL and supplementary functions for Tom Wu's base64.js library + * @author Kenji Urushima + * @version 1.1 (07 May 2012) + * @requires base64.js + * @see 'jwjws'(JWS JavaScript Library) home page http://kjur.github.com/jsjws/ + * @see 'jwrsasign'(RSA Sign JavaScript Library) home page http://kjur.github.com/jsrsasign/ + */ +function Base64x() { +} + +// ==== string / byte array ================================ +/** + * convert a string to an array of character codes + * @param {String} s + * @return {Array of Numbers} + */ +function stoBA(s) { + var a = new Array(); + for (var i = 0; i < s.length; i++) { + a[i] = s.charCodeAt(i); + } + return a; +} + +/** + * convert an array of character codes to a string + * @param {Array of Numbers} a array of character codes + * @return {String} s + */ +function BAtos(a) { + var s = ""; + for (var i = 0; i < a.length; i++) { + s = s + String.fromCharCode(a[i]); + } + return s; +} + +// ==== byte array / hex ================================ +/** + * convert an array of bytes(Number) to hexadecimal string.
+ * @param {Array of Numbers} a array of bytes + * @return {String} hexadecimal string + */ +function BAtohex(a) { + var s = ""; + for (var i = 0; i < a.length; i++) { + var hex1 = a[i].toString(16); + if (hex1.length == 1) hex1 = "0" + hex1; + s = s + hex1; + } + return s; +} + +// ==== string / hex ================================ +/** + * convert a ASCII string to a hexadecimal string of ASCII codes.
+ * NOTE: This can't be used for non ASCII characters. + * @param {s} s ASCII string + * @return {String} hexadecimal string + */ +function stohex(s) { + return BAtohex(stoBA(s)); +} + +// ==== string / base64 ================================ +/** + * convert a ASCII string to a Base64 encoded string.
+ * NOTE: This can't be used for non ASCII characters. + * @param {s} s ASCII string + * @return {String} Base64 encoded string + */ +function stob64(s) { + return hex2b64(stohex(s)); +} + +// ==== string / base64url ================================ +/** + * convert a ASCII string to a Base64URL encoded string.
+ * NOTE: This can't be used for non ASCII characters. + * @param {s} s ASCII string + * @return {String} Base64URL encoded string + */ +function stob64u(s) { + return b64tob64u(hex2b64(stohex(s))); +} + +/** + * convert a Base64URL encoded string to a ASCII string.
+ * NOTE: This can't be used for Base64URL encoded non ASCII characters. + * @param {s} s Base64URL encoded string + * @return {String} ASCII string + */ +function b64utos(s) { + return BAtos(b64toBA(b64utob64(s))); +} + +// ==== base64 / base64url ================================ +/** + * convert a Base64 encoded string to a Base64URL encoded string.
+ * Example: "ab+c3f/==" → "ab-c3f_" + * @param {String} s Base64 encoded string + * @return {String} Base64URL encoded string + */ +function b64tob64u(s) { + s = s.replace(/\=/g, ""); + s = s.replace(/\+/g, "-"); + s = s.replace(/\//g, "_"); + return s; +} + +/** + * convert a Base64URL encoded string to a Base64 encoded string.
+ * Example: "ab-c3f_" → "ab+c3f/==" + * @param {String} s Base64URL encoded string + * @return {String} Base64 encoded string + */ +function b64utob64(s) { + if (s.length % 4 == 2) s = s + "=="; + else if (s.length % 4 == 3) s = s + "="; + s = s.replace(/-/g, "+"); + s = s.replace(/_/g, "/"); + return s; +} + +// ==== hex / base64url ================================ +/** + * convert a hexadecimal string to a Base64URL encoded string.
+ * @param {String} s hexadecimal string + * @return {String} Base64URL encoded string + */ +function hextob64u(s) { + return b64tob64u(hex2b64(s)); +} + +/** + * convert a Base64URL encoded string to a hexadecimal string.
+ * @param {String} s Base64URL encoded string + * @return {String} hexadecimal string + */ +function b64utohex(s) { + return b64tohex(b64utob64(s)); +} + +var utf8tob64u, b64utoutf8; + +if (typeof Buffer === 'function') +{ + utf8tob64u = function (s) + { + return b64tob64u(new Buffer(s, 'utf8').toString('base64')); + }; + + b64utoutf8 = function (s) + { + return new Buffer(b64utob64(s), 'base64').toString('utf8'); + }; +} +else +{ +// ==== utf8 / base64url ================================ +/** + * convert a UTF-8 encoded string including CJK or Latin to a Base64URL encoded string.
+ * @param {String} s UTF-8 encoded string + * @return {String} Base64URL encoded string + * @since 1.1 + */ + utf8tob64u = function (s) + { + return hextob64u(uricmptohex(encodeURIComponentAll(s))); + }; + +/** + * convert a Base64URL encoded string to a UTF-8 encoded string including CJK or Latin.
+ * @param {String} s Base64URL encoded string + * @return {String} UTF-8 encoded string + * @since 1.1 + */ + b64utoutf8 = function (s) + { + return decodeURIComponent(hextouricmp(b64utohex(s))); + }; +} + +// ==== utf8 / base64url ================================ +/** + * convert a UTF-8 encoded string including CJK or Latin to a Base64 encoded string.
+ * @param {String} s UTF-8 encoded string + * @return {String} Base64 encoded string + * @since 1.1.1 + */ +function utf8tob64(s) { + return hex2b64(uricmptohex(encodeURIComponentAll(s))); +} + +/** + * convert a Base64 encoded string to a UTF-8 encoded string including CJK or Latin.
+ * @param {String} s Base64 encoded string + * @return {String} UTF-8 encoded string + * @since 1.1.1 + */ +function b64toutf8(s) { + return decodeURIComponent(hextouricmp(b64tohex(s))); +} + +// ==== utf8 / hex ================================ +/** + * convert a UTF-8 encoded string including CJK or Latin to a hexadecimal encoded string.
+ * @param {String} s UTF-8 encoded string + * @return {String} hexadecimal encoded string + * @since 1.1.1 + */ +function utf8tohex(s) { + return uricmptohex(encodeURIComponentAll(s)); +} + +/** + * convert a hexadecimal encoded string to a UTF-8 encoded string including CJK or Latin.
+ * Note that when input is improper hexadecimal string as UTF-8 string, this function returns + * 'null'. + * @param {String} s hexadecimal encoded string + * @return {String} UTF-8 encoded string or null + * @since 1.1.1 + */ +function hextoutf8(s) { + return decodeURIComponent(hextouricmp(s)); +} + +/** + * convert a hexadecimal encoded string to raw string including non printable characters.
+ * @param {String} s hexadecimal encoded string + * @return {String} raw string + * @since 1.1.2 + * @example + * hextorstr("610061") → "a\x00a" + */ +function hextorstr(sHex) { + var s = ""; + for (var i = 0; i < sHex.length - 1; i += 2) { + s += String.fromCharCode(parseInt(sHex.substr(i, 2), 16)); + } + return s; +} + +/** + * convert a raw string including non printable characters to hexadecimal encoded string.
+ * @param {String} s raw string + * @return {String} hexadecimal encoded string + * @since 1.1.2 + * @example + * rstrtohex("a\x00a") → "610061" + */ +function rstrtohex(s) { + var result = ""; + for (var i = 0; i < s.length; i++) { + result += ("0" + s.charCodeAt(i).toString(16)).slice(-2); + } + return result; +} + +// ==== hex / b64nl ======================================= + +/* + * since base64x 1.1.3 + */ +function hextob64(s) { + return hex2b64(s); +} + +/* + * since base64x 1.1.3 + */ +function hextob64nl(s) { + var b64 = hextob64(s); + var b64nl = b64.replace(/(.{64})/g, "$1\r\n"); + b64nl = b64nl.replace(/\r\n$/, ''); + return b64nl; +} + +/* + * since base64x 1.1.3 + */ +function b64nltohex(s) { + var b64 = s.replace(/[^0-9A-Za-z\/+=]*/g, ''); + var hex = b64tohex(b64); + return hex; +} + +// ==== URIComponent / hex ================================ +/** + * convert a URLComponent string such like "%67%68" to a hexadecimal string.
+ * @param {String} s URIComponent string such like "%67%68" + * @return {String} hexadecimal string + * @since 1.1 + */ +function uricmptohex(s) { + return s.replace(/%/g, ""); +} + +/** + * convert a hexadecimal string to a URLComponent string such like "%67%68".
+ * @param {String} s hexadecimal string + * @return {String} URIComponent string such like "%67%68" + * @since 1.1 + */ +function hextouricmp(s) { + return s.replace(/(..)/g, "%$1"); +} + +// ==== URIComponent ================================ +/** + * convert UTFa hexadecimal string to a URLComponent string such like "%67%68".
+ * Note that these "0-9A-Za-z!'()*-._~" characters will not + * converted to "%xx" format by builtin 'encodeURIComponent()' function. + * However this 'encodeURIComponentAll()' function will convert + * all of characters into "%xx" format. + * @param {String} s hexadecimal string + * @return {String} URIComponent string such like "%67%68" + * @since 1.1 + */ +function encodeURIComponentAll(u8) { + var s = encodeURIComponent(u8); + var s2 = ""; + for (var i = 0; i < s.length; i++) { + if (s[i] == "%") { + s2 = s2 + s.substr(i, 3); + i = i + 2; + } else { + s2 = s2 + "%" + stohex(s[i]); + } + } + return s2; +} + +// ==== new lines ================================ +/** + * convert all DOS new line("\r\n") to UNIX new line("\n") in + * a String "s". + * @param {String} s string + * @return {String} converted string + */ +function newline_toUnix(s) { + s = s.replace(/\r\n/mg, "\n"); + return s; +} + +/** + * convert all UNIX new line("\r\n") to DOS new line("\n") in + * a String "s". + * @param {String} s string + * @return {String} converted string + */ +function newline_toDos(s) { + s = s.replace(/\r\n/mg, "\n"); + s = s.replace(/\n/mg, "\r\n"); + return s; +} + +/*! Mike Samuel (c) 2009 | code.google.com/p/json-sans-eval + */ +// This source code is free for use in the public domain. +// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + +// http://code.google.com/p/json-sans-eval/ + +/** + * Parses a string of well-formed JSON text. + * + * If the input is not well-formed, then behavior is undefined, but it is + * deterministic and is guaranteed not to modify any object other than its + * return value. + * + * This does not use `eval` so is less likely to have obscure security bugs than + * json2.js. + * It is optimized for speed, so is much faster than json_parse.js. + * + * This library should be used whenever security is a concern (when JSON may + * come from an untrusted source), speed is a concern, and erroring on malformed + * JSON is *not* a concern. + * + * Pros Cons + * +-----------------------+-----------------------+ + * json_sans_eval.js | Fast, secure | Not validating | + * +-----------------------+-----------------------+ + * json_parse.js | Validating, secure | Slow | + * +-----------------------+-----------------------+ + * json2.js | Fast, some validation | Potentially insecure | + * +-----------------------+-----------------------+ + * + * json2.js is very fast, but potentially insecure since it calls `eval` to + * parse JSON data, so an attacker might be able to supply strange JS that + * looks like JSON, but that executes arbitrary javascript. + * If you do have to use json2.js with untrusted data, make sure you keep + * your version of json2.js up to date so that you get patches as they're + * released. + * + * @param {string} json per RFC 4627 + * @param {function (this:Object, string, *):*} opt_reviver optional function + * that reworks JSON objects post-parse per Chapter 15.12 of EcmaScript3.1. + * If supplied, the function is called with a string key, and a value. + * The value is the property of 'this'. The reviver should return + * the value to use in its place. So if dates were serialized as + * {@code { "type": "Date", "time": 1234 }}, then a reviver might look like + * {@code + * function (key, value) { + * if (value && typeof value === 'object' && 'Date' === value.type) { + * return new Date(value.time); + * } else { + * return value; + * } + * }}. + * If the reviver returns {@code undefined} then the property named by key + * will be deleted from its container. + * {@code this} is bound to the object containing the specified property. + * @return {Object|Array} + * @author Mike Samuel + */ +var jsonParse = (function () { + var number + = '(?:-?\\b(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\\b)'; + var oneChar = '(?:[^\\0-\\x08\\x0a-\\x1f\"\\\\]' + + '|\\\\(?:[\"/\\\\bfnrt]|u[0-9A-Fa-f]{4}))'; + var string = '(?:\"' + oneChar + '*\")'; + + // Will match a value in a well-formed JSON file. + // If the input is not well-formed, may match strangely, but not in an unsafe + // way. + // Since this only matches value tokens, it does not match whitespace, colons, + // or commas. + var jsonToken = new RegExp( + '(?:false|true|null|[\\{\\}\\[\\]]' + + '|' + number + + '|' + string + + ')', 'g'); + + // Matches escape sequences in a string literal + var escapeSequence = new RegExp('\\\\(?:([^u])|u(.{4}))', 'g'); + + // Decodes escape sequences in object literals + var escapes = { + '"': '"', + '/': '/', + '\\': '\\', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }; + function unescapeOne(_, ch, hex) { + return ch ? escapes[ch] : String.fromCharCode(parseInt(hex, 16)); + } + + // A non-falsy value that coerces to the empty string when used as a key. + var EMPTY_STRING = new String(''); + var SLASH = '\\'; + + // Constructor to use based on an open token. + var firstTokenCtors = { '{': Object, '[': Array }; + + var hop = Object.hasOwnProperty; + + return function (json, opt_reviver) { + // Split into tokens + var toks = json.match(jsonToken); + // Construct the object to return + var result; + var tok = toks[0]; + var topLevelPrimitive = false; + if ('{' === tok) { + result = {}; + } else if ('[' === tok) { + result = []; + } else { + // The RFC only allows arrays or objects at the top level, but the JSON.parse + // defined by the EcmaScript 5 draft does allow strings, booleans, numbers, and null + // at the top level. + result = []; + topLevelPrimitive = true; + } + + // If undefined, the key in an object key/value record to use for the next + // value parsed. + var key; + // Loop over remaining tokens maintaining a stack of uncompleted objects and + // arrays. + var stack = [result]; + for (var i = 1 - topLevelPrimitive, n = toks.length; i < n; ++i) { + tok = toks[i]; + + var cont; + switch (tok.charCodeAt(0)) { + default: // sign or digit + cont = stack[0]; + cont[key || cont.length] = +(tok); + key = void 0; + break; + case 0x22: // '"' + tok = tok.substring(1, tok.length - 1); + if (tok.indexOf(SLASH) !== -1) { + tok = tok.replace(escapeSequence, unescapeOne); + } + cont = stack[0]; + if (!key) { + if (cont instanceof Array) { + key = cont.length; + } else { + key = tok || EMPTY_STRING; // Use as key for next value seen. + break; + } + } + cont[key] = tok; + key = void 0; + break; + case 0x5b: // '[' + cont = stack[0]; + stack.unshift(cont[key || cont.length] = []); + key = void 0; + break; + case 0x5d: // ']' + stack.shift(); + break; + case 0x66: // 'f' + cont = stack[0]; + cont[key || cont.length] = false; + key = void 0; + break; + case 0x6e: // 'n' + cont = stack[0]; + cont[key || cont.length] = null; + key = void 0; + break; + case 0x74: // 't' + cont = stack[0]; + cont[key || cont.length] = true; + key = void 0; + break; + case 0x7b: // '{' + cont = stack[0]; + stack.unshift(cont[key || cont.length] = {}); + key = void 0; + break; + case 0x7d: // '}' + stack.shift(); + break; + } + } + // Fail if we've got an uncompleted object. + if (topLevelPrimitive) { + if (stack.length !== 1) { throw new Error(); } + result = result[0]; + } else { + if (stack.length) { throw new Error(); } + } + + if (opt_reviver) { + // Based on walk as implemented in http://www.json.org/json2.js + var walk = function (holder, key) { + var value = holder[key]; + if (value && typeof value === 'object') { + var toDelete = null; + for (var k in value) { + if (hop.call(value, k) && value !== holder) { + // Recurse to properties first. This has the effect of causing + // the reviver to be called on the object graph depth-first. + + // Since 'this' is bound to the holder of the property, the + // reviver can access sibling properties of k including ones + // that have not yet been revived. + + // The value returned by the reviver is used in place of the + // current value of property k. + // If it returns undefined then the property is deleted. + var v = walk(value, k); + if (v !== void 0) { + value[k] = v; + } else { + // Deleting properties inside the loop has vaguely defined + // semantics in ES3 and ES3.1. + if (!toDelete) { toDelete = []; } + toDelete.push(k); + } + } + } + if (toDelete) { + for (var i = toDelete.length; --i >= 0;) { + delete value[toDelete[i]]; + } + } + } + return opt_reviver.call(holder, key, value); + }; + result = walk({ '': result }, ''); + } + + return result; + }; +})(); + +/*! jws-3.0.2 (c) 2013 Kenji Urushima | kjur.github.com/jsjws/license + */ +/* + * jws.js - JSON Web Signature Class + * + * version: 3.0.2 (2013 Sep 24) + * + * Copyright (c) 2010-2013 Kenji Urushima (kenji.urushima@gmail.com) + * + * This software is licensed under the terms of the MIT License. + * http://kjur.github.com/jsjws/license/ + * + * The above copyright and license notice shall be + * included in all copies or substantial portions of the Software. + */ + +/** + * @fileOverview + * @name jws-3.0.js + * @author Kenji Urushima kenji.urushima@gmail.com + * @version 3.0.1 (2013-Sep-24) + * @since jsjws 1.0 + * @license MIT License + */ + +if (typeof KJUR == "undefined" || !KJUR) KJUR = {}; +if (typeof KJUR.jws == "undefined" || !KJUR.jws) KJUR.jws = {}; + +/** + * JSON Web Signature(JWS) class.
+ * @name KJUR.jws.JWS + * @class JSON Web Signature(JWS) class + * @property {Dictionary} parsedJWS This property is set after JWS signature verification.
+ * Following "parsedJWS_*" properties can be accessed as "parsedJWS.*" because of + * JsDoc restriction. + * @property {String} parsedJWS_headB64U string of Encrypted JWS Header + * @property {String} parsedJWS_payloadB64U string of Encrypted JWS Payload + * @property {String} parsedJWS_sigvalB64U string of Encrypted JWS signature value + * @property {String} parsedJWS_si string of Signature Input + * @property {String} parsedJWS_sigvalH hexadecimal string of JWS signature value + * @property {String} parsedJWS_sigvalBI BigInteger(defined in jsbn.js) object of JWS signature value + * @property {String} parsedJWS_headS string of decoded JWS Header + * @property {String} parsedJWS_headS string of decoded JWS Payload + * @requires base64x.js, json-sans-eval.js and jsrsasign library + * @see 'jwjws'(JWS JavaScript Library) home page http://kjur.github.com/jsjws/ + * @see 'jwrsasign'(RSA Sign JavaScript Library) home page http://kjur.github.com/jsrsasign/ + * @see IETF I-D JSON Web Algorithms (JWA) + * @since jsjws 1.0 + * @description + *

Supported Algorithms

+ * Here is supported algorithm names for {@link KJUR.jws.JWS.sign} and {@link KJUR.jws.JWS.verify} + * methods. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
alg valuespec requirementjsjws support
HS256REQUIREDSUPPORTED
HS384OPTIONALSUPPORTED
HS512OPTIONALSUPPORTED
RS256RECOMMENDEDSUPPORTED
RS384OPTIONALSUPPORTED
RS512OPTIONALSUPPORTED
ES256RECOMMENDED+SUPPORTED
ES384OPTIONALSUPPORTED
ES512OPTIONAL-
PS256OPTIONALSUPPORTED
PS384OPTIONALSUPPORTED
PS512OPTIONALSUPPORTED
noneREQUIREDSUPPORTED
+ * NOTE: HS384 is supported since jsjws 3.0.2 with jsrsasign 4.1.4. + */ +KJUR.jws.JWS = function() { + + // === utility ============================================================= + + /** + * parse JWS string and set public property 'parsedJWS' dictionary.
+ * @name parseJWS + * @memberOf KJUR.jws.JWS + * @function + * @param {String} sJWS JWS signature string to be parsed. + * @throws if sJWS is not comma separated string such like "Header.Payload.Signature". + * @throws if JWS Header is a malformed JSON string. + * @since jws 1.1 + */ + this.parseJWS = function(sJWS, sigValNotNeeded) { + if ((this.parsedJWS !== undefined) && + (sigValNotNeeded || (this.parsedJWS.sigvalH !== undefined))) { + return; + } + if (sJWS.match(/^([^.]+)\.([^.]+)\.([^.]+)$/) == null) { + throw "JWS signature is not a form of 'Head.Payload.SigValue'."; + } + var b6Head = RegExp.$1; + var b6Payload = RegExp.$2; + var b6SigVal = RegExp.$3; + var sSI = b6Head + "." + b6Payload; + this.parsedJWS = {}; + this.parsedJWS.headB64U = b6Head; + this.parsedJWS.payloadB64U = b6Payload; + this.parsedJWS.sigvalB64U = b6SigVal; + this.parsedJWS.si = sSI; + + if (!sigValNotNeeded) { + var hSigVal = b64utohex(b6SigVal); + var biSigVal = parseBigInt(hSigVal, 16); + this.parsedJWS.sigvalH = hSigVal; + this.parsedJWS.sigvalBI = biSigVal; + } + + var sHead = b64utoutf8(b6Head); + var sPayload = b64utoutf8(b6Payload); + this.parsedJWS.headS = sHead; + this.parsedJWS.payloadS = sPayload; + + if (!KJUR.jws.JWS.isSafeJSONString(sHead, this.parsedJWS, 'headP')) + throw "malformed JSON string for JWS Head: " + sHead; + }; + + // ==== JWS Validation ========================================================= + function _getSignatureInputByString(sHead, sPayload) { + return utf8tob64u(sHead) + "." + utf8tob64u(sPayload); + }; + + function _getHashBySignatureInput(sSignatureInput, sHashAlg) { + var hashfunc = function(s) { return KJUR.crypto.Util.hashString(s, sHashAlg); }; + if (hashfunc == null) throw "hash function not defined in jsrsasign: " + sHashAlg; + return hashfunc(sSignatureInput); + }; + + function _jws_verifySignature(sHead, sPayload, hSig, hN, hE) { + var sSignatureInput = _getSignatureInputByString(sHead, sPayload); + var biSig = parseBigInt(hSig, 16); + return _rsasign_verifySignatureWithArgs(sSignatureInput, biSig, hN, hE); + }; + + /** + * verify JWS signature with naked RSA public key.
+ * This only supports "RS256" and "RS512" algorithm. + * @name verifyJWSByNE + * @memberOf KJUR.jws.JWS + * @function + * @param {String} sJWS JWS signature string to be verified + * @param {String} hN hexadecimal string for modulus of RSA public key + * @param {String} hE hexadecimal string for public exponent of RSA public key + * @return {String} returns 1 when JWS signature is valid, otherwise returns 0 + * @throws if sJWS is not comma separated string such like "Header.Payload.Signature". + * @throws if JWS Header is a malformed JSON string. + * @deprecated from 3.0.0 please move to {@link KJUR.jws.JWS.verify} + */ + this.verifyJWSByNE = function(sJWS, hN, hE) { + this.parseJWS(sJWS); + return _rsasign_verifySignatureWithArgs(this.parsedJWS.si, this.parsedJWS.sigvalBI, hN, hE); + }; + + /** + * verify JWS signature with RSA public key.
+ * This only supports "RS256", "RS512", "PS256" and "PS512" algorithms. + * @name verifyJWSByKey + * @memberOf KJUR.jws.JWS + * @function + * @param {String} sJWS JWS signature string to be verified + * @param {RSAKey} key RSA public key + * @return {Boolean} returns true when JWS signature is valid, otherwise returns false + * @throws if sJWS is not comma separated string such like "Header.Payload.Signature". + * @throws if JWS Header is a malformed JSON string. + * @deprecated from 3.0.0 please move to {@link KJUR.jws.JWS.verify} + */ + this.verifyJWSByKey = function(sJWS, key) { + this.parseJWS(sJWS); + var hashAlg = _jws_getHashAlgFromParsedHead(this.parsedJWS.headP); + var isPSS = this.parsedJWS.headP['alg'].substr(0, 2) == "PS"; + + if (key.hashAndVerify) { + return key.hashAndVerify(hashAlg, + new Buffer(this.parsedJWS.si, 'utf8').toString('base64'), + b64utob64(this.parsedJWS.sigvalB64U), + 'base64', + isPSS); + } else if (isPSS) { + return key.verifyStringPSS(this.parsedJWS.si, + this.parsedJWS.sigvalH, hashAlg); + } else { + return key.verifyString(this.parsedJWS.si, + this.parsedJWS.sigvalH); + } + }; + + /** + * verify JWS signature by PEM formatted X.509 certificate.
+ * This only supports "RS256" and "RS512" algorithm. + * @name verifyJWSByPemX509Cert + * @memberOf KJUR.jws.JWS + * @function + * @param {String} sJWS JWS signature string to be verified + * @param {String} sPemX509Cert string of PEM formatted X.509 certificate + * @return {String} returns 1 when JWS signature is valid, otherwise returns 0 + * @throws if sJWS is not comma separated string such like "Header.Payload.Signature". + * @throws if JWS Header is a malformed JSON string. + * @since 1.1 + * @deprecated from 3.0.0 please move to {@link KJUR.jws.JWS.verify} + */ + this.verifyJWSByPemX509Cert = function(sJWS, sPemX509Cert) { + this.parseJWS(sJWS); + var x509 = new X509(); + x509.readCertPEM(sPemX509Cert); + return x509.subjectPublicKeyRSA.verifyString(this.parsedJWS.si, this.parsedJWS.sigvalH); + }; + + // ==== JWS Generation ========================================================= + function _jws_getHashAlgFromParsedHead(head) { + var sigAlg = head["alg"]; + var hashAlg = ""; + + if (sigAlg != "RS256" && sigAlg != "RS512" && + sigAlg != "PS256" && sigAlg != "PS512") + throw "JWS signature algorithm not supported: " + sigAlg; + if (sigAlg.substr(2) == "256") hashAlg = "sha256"; + if (sigAlg.substr(2) == "512") hashAlg = "sha512"; + return hashAlg; + }; + + function _jws_getHashAlgFromHead(sHead) { + return _jws_getHashAlgFromParsedHead(jsonParse(sHead)); + }; + + function _jws_generateSignatureValueBySI_NED(sHead, sPayload, sSI, hN, hE, hD) { + var rsa = new RSAKey(); + rsa.setPrivate(hN, hE, hD); + + var hashAlg = _jws_getHashAlgFromHead(sHead); + var sigValue = rsa.signString(sSI, hashAlg); + return sigValue; + }; + + function _jws_generateSignatureValueBySI_Key(sHead, sPayload, sSI, key, head) { + var hashAlg = null; + if (typeof head == "undefined") { + hashAlg = _jws_getHashAlgFromHead(sHead); + } else { + hashAlg = _jws_getHashAlgFromParsedHead(head); + } + + var isPSS = head['alg'].substr(0, 2) == "PS"; + + if (key.hashAndSign) { + return b64tob64u(key.hashAndSign(hashAlg, sSI, 'binary', 'base64', isPSS)); + } else if (isPSS) { + return hextob64u(key.signStringPSS(sSI, hashAlg)); + } else { + return hextob64u(key.signString(sSI, hashAlg)); + } + }; + + function _jws_generateSignatureValueByNED(sHead, sPayload, hN, hE, hD) { + var sSI = _getSignatureInputByString(sHead, sPayload); + return _jws_generateSignatureValueBySI_NED(sHead, sPayload, sSI, hN, hE, hD); + }; + + /** + * generate JWS signature by Header, Payload and a naked RSA private key.
+ * This only supports "RS256" and "RS512" algorithm. + * @name generateJWSByNED + * @memberOf KJUR.jws.JWS + * @function + * @param {String} sHead string of JWS Header + * @param {String} sPayload string of JWS Payload + * @param {String} hN hexadecimal string for modulus of RSA public key + * @param {String} hE hexadecimal string for public exponent of RSA public key + * @param {String} hD hexadecimal string for private exponent of RSA private key + * @return {String} JWS signature string + * @throws if sHead is a malformed JSON string. + * @throws if supported signature algorithm was not specified in JSON Header. + * @deprecated from 3.0.0 please move to {@link KJUR.jws.JWS.sign} + */ + this.generateJWSByNED = function(sHead, sPayload, hN, hE, hD) { + if (!KJUR.jws.JWS.isSafeJSONString(sHead)) throw "JWS Head is not safe JSON string: " + sHead; + var sSI = _getSignatureInputByString(sHead, sPayload); + var hSigValue = _jws_generateSignatureValueBySI_NED(sHead, sPayload, sSI, hN, hE, hD); + var b64SigValue = hextob64u(hSigValue); + + this.parsedJWS = {}; + this.parsedJWS.headB64U = sSI.split(".")[0]; + this.parsedJWS.payloadB64U = sSI.split(".")[1]; + this.parsedJWS.sigvalB64U = b64SigValue; + + return sSI + "." + b64SigValue; + }; + + /** + * generate JWS signature by Header, Payload and a RSA private key.
+ * This only supports "RS256", "RS512", "PS256" and "PS512" algorithms. + * @name generateJWSByKey + * @memberOf KJUR.jws.JWS + * @function + * @param {String} sHead string of JWS Header + * @param {String} sPayload string of JWS Payload + * @param {RSAKey} RSA private key + * @return {String} JWS signature string + * @throws if sHead is a malformed JSON string. + * @throws if supported signature algorithm was not specified in JSON Header. + * @deprecated from 3.0.0 please move to {@link KJUR.jws.JWS.sign} + */ + this.generateJWSByKey = function(sHead, sPayload, key) { + var obj = {}; + if (!KJUR.jws.JWS.isSafeJSONString(sHead, obj, 'headP')) + throw "JWS Head is not safe JSON string: " + sHead; + var sSI = _getSignatureInputByString(sHead, sPayload); + var b64SigValue = _jws_generateSignatureValueBySI_Key(sHead, sPayload, sSI, key, obj.headP); + + this.parsedJWS = {}; + this.parsedJWS.headB64U = sSI.split(".")[0]; + this.parsedJWS.payloadB64U = sSI.split(".")[1]; + this.parsedJWS.sigvalB64U = b64SigValue; + + return sSI + "." + b64SigValue; + }; + + // === sign with PKCS#1 RSA private key ===================================================== + function _jws_generateSignatureValueBySI_PemPrvKey(sHead, sPayload, sSI, sPemPrvKey) { + var rsa = new RSAKey(); + rsa.readPrivateKeyFromPEMString(sPemPrvKey); + var hashAlg = _jws_getHashAlgFromHead(sHead); + var sigValue = rsa.signString(sSI, hashAlg); + return sigValue; + }; + + /** + * generate JWS signature by Header, Payload and a PEM formatted PKCS#1 RSA private key.
+ * This only supports "RS256" and "RS512" algorithm. + * @name generateJWSByP1PrvKey + * @memberOf KJUR.jws.JWS + * @function + * @param {String} sHead string of JWS Header + * @param {String} sPayload string of JWS Payload + * @param {String} string for sPemPrvKey PEM formatted PKCS#1 RSA private key
+ * Heading and trailing space characters in PEM key will be ignored. + * @return {String} JWS signature string + * @throws if sHead is a malformed JSON string. + * @throws if supported signature algorithm was not specified in JSON Header. + * @since 1.1 + * @deprecated from 3.0.0 please move to {@link KJUR.jws.JWS.sign} + */ + this.generateJWSByP1PrvKey = function(sHead, sPayload, sPemPrvKey) { + if (!KJUR.jws.JWS.isSafeJSONString(sHead)) throw "JWS Head is not safe JSON string: " + sHead; + var sSI = _getSignatureInputByString(sHead, sPayload); + var hSigValue = _jws_generateSignatureValueBySI_PemPrvKey(sHead, sPayload, sSI, sPemPrvKey); + var b64SigValue = hextob64u(hSigValue); + + this.parsedJWS = {}; + this.parsedJWS.headB64U = sSI.split(".")[0]; + this.parsedJWS.payloadB64U = sSI.split(".")[1]; + this.parsedJWS.sigvalB64U = b64SigValue; + + return sSI + "." + b64SigValue; + }; +}; + +// === major static method ======================================================== + +/** + * generate JWS signature by specified key
+ * @name sign + * @memberOf KJUR.jws.JWS + * @function + * @static + * @param {String} alg JWS algorithm name to sign and force set to sHead or null + * @param {String} sHead string of JWS Header + * @param {String} sPayload string of JWS Payload + * @param {String} key string of private key or key object to sign + * @param {String} pass (OPTION)passcode to use encrypted private key + * @return {String} JWS signature string + * @since jws 3.0.0 + * @see jsrsasign KJUR.crypto.Signature method + * @see jsrsasign KJUR.crypto.Mac method + * @description + * This method supports following algorithms. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
alg valuespec requirementjsjws support
HS256REQUIREDSUPPORTED
HS384OPTIONAL-
HS512OPTIONALSUPPORTED
RS256RECOMMENDEDSUPPORTED
RS384OPTIONALSUPPORTED
RS512OPTIONALSUPPORTED
ES256RECOMMENDED+SUPPORTED
ES384OPTIONALSUPPORTED
ES512OPTIONAL-
PS256OPTIONALSUPPORTED
PS384OPTIONALSUPPORTED
PS512OPTIONALSUPPORTED
noneREQUIREDSUPPORTED
+ *
+ *
NOTE1: + *
salt length of RSAPSS signature is the same as the hash algorithm length + * because of IETF JOSE ML discussion. + *
NOTE2: + *
The reason of HS384 unsupport is + * CryptoJS HmacSHA384 bug. + *
+ */ +KJUR.jws.JWS.sign = function(alg, sHeader, sPayload, key, pass) { + var ns1 = KJUR.jws.JWS; + + if (! ns1.isSafeJSONString(sHeader)) + throw "JWS Head is not safe JSON string: " + sHead; + + var pHeader = ns1.readSafeJSONString(sHeader); + + // 1. use alg if defined in sHeader + if ((alg == '' || alg == null) && + pHeader['alg'] !== undefined) { + alg = pHeader['alg']; + } + + // 2. set alg in sHeader if undefined + if ((alg != '' && alg != null) && + pHeader['alg'] === undefined) { + pHeader['alg'] = alg; + sHeader = JSON.stringify(pHeader); + } + + // 3. set signature algorithm like SHA1withRSA + var sigAlg = null; + if (ns1.jwsalg2sigalg[alg] === undefined) { + throw "unsupported alg name: " + alg; + } else { + sigAlg = ns1.jwsalg2sigalg[alg]; + } + + var uHeader = utf8tob64u(sHeader); + var uPayload = utf8tob64u(sPayload); + var uSignatureInput = uHeader + "." + uPayload + + // 4. sign + var hSig = ""; + if (sigAlg.substr(0, 4) == "Hmac") { + if (key === undefined) + throw "hexadecimal key shall be specified for HMAC"; + var mac = new KJUR.crypto.Mac({'alg': sigAlg, 'pass': hextorstr(key)}); + mac.updateString(uSignatureInput); + hSig = mac.doFinal(); + } else if (sigAlg.indexOf("withECDSA") != -1) { + var sig = new KJUR.crypto.Signature({'alg': sigAlg}); + sig.init(key, pass); + sig.updateString(uSignatureInput); + hASN1Sig = sig.sign(); + hSig = KJUR.crypto.ECDSA.asn1SigToConcatSig(hASN1Sig); + } else if (sigAlg != "none") { + var sig = new KJUR.crypto.Signature({'alg': sigAlg}); + sig.init(key, pass); + sig.updateString(uSignatureInput); + hSig = sig.sign(); + } + + var uSig = hextob64u(hSig); + return uSignatureInput + "." + uSig; +}; + +/** + * verify JWS signature by specified key or certificate
+ * @name verify + * @memberOf KJUR.jws.JWS + * @function + * @static + * @param {String} sJWS string of JWS signature to verify + * @param {String} key string of public key, certificate or key object to verify + * @return {Boolean} true if the signature is valid otherwise false + * @since jws 3.0.0 + * @see jsrsasign KJUR.crypto.Signature method + * @see jsrsasign KJUR.crypto.Mac method + */ +KJUR.jws.JWS.verify = function(sJWS, key) { + var jws = KJUR.jws.JWS; + var a = sJWS.split("."); + var uHeader = a[0]; + var uPayload = a[1]; + var uSignatureInput = uHeader + "." + uPayload; + var hSig = b64utohex(a[2]); + + var pHeader = jws.readSafeJSONString(b64utoutf8(a[0])); + var alg = null; + if (pHeader.alg === undefined) { + throw "algorithm not specified in header"; + } else { + alg = pHeader.alg; + } + + var sigAlg = null; + if (jws.jwsalg2sigalg[pHeader.alg] === undefined) { + throw "unsupported alg name: " + alg; + } else { + sigAlg = jws.jwsalg2sigalg[alg]; + } + + // x. verify + if (sigAlg == "none") { + return true; + } else if (sigAlg.substr(0, 4) == "Hmac") { + if (key === undefined) + throw "hexadecimal key shall be specified for HMAC"; + var mac = new KJUR.crypto.Mac({'alg': sigAlg, 'pass': hextorstr(key)}); + mac.updateString(uSignatureInput); + hSig2 = mac.doFinal(); + return hSig == hSig2; + } else if (sigAlg.indexOf("withECDSA") != -1) { + var hASN1Sig = null; + try { + hASN1Sig = KJUR.crypto.ECDSA.concatSigToASN1Sig(hSig); + } catch (ex) { + return false; + } + var sig = new KJUR.crypto.Signature({'alg': sigAlg}); + sig.init(key) + sig.updateString(uSignatureInput); + return sig.verify(hASN1Sig); + } else { + var sig = new KJUR.crypto.Signature({'alg': sigAlg}); + sig.init(key) + sig.updateString(uSignatureInput); + return sig.verify(hSig); + } +}; + +/* + * @since jws 3.0.0 + */ +KJUR.jws.JWS.jwsalg2sigalg = { + "HS256": "HmacSHA256", + //"HS384": "HmacSHA384", // unsupported because of CryptoJS bug + "HS512": "HmacSHA512", + "RS256": "SHA256withRSA", + "RS384": "SHA384withRSA", + "RS512": "SHA512withRSA", + "ES256": "SHA256withECDSA", + "ES384": "SHA384withECDSA", + //"ES512": "SHA512withECDSA", // unsupported because of jsrsasign's bug + "PS256": "SHA256withRSAandMGF1", + "PS384": "SHA384withRSAandMGF1", + "PS512": "SHA512withRSAandMGF1", + "none": "none", +}; + +// === utility static method ====================================================== + +/** + * check whether a String "s" is a safe JSON string or not.
+ * If a String "s" is a malformed JSON string or an other object type + * this returns 0, otherwise this returns 1. + * @name isSafeJSONString + * @memberOf KJUR.jws.JWS + * @function + * @static + * @param {String} s JSON string + * @return {Number} 1 or 0 + */ +KJUR.jws.JWS.isSafeJSONString = function(s, h, p) { + var o = null; + try { + o = jsonParse(s); + if (typeof o != "object") return 0; + if (o.constructor === Array) return 0; + if (h) h[p] = o; + return 1; + } catch (ex) { + return 0; + } +}; + +/** + * read a String "s" as JSON object if it is safe.
+ * If a String "s" is a malformed JSON string or not JSON string, + * this returns null, otherwise returns JSON object. + * @name readSafeJSONString + * @memberOf KJUR.jws.JWS + * @function + * @static + * @param {String} s JSON string + * @return {Object} JSON object or null + * @since 1.1.1 + */ +KJUR.jws.JWS.readSafeJSONString = function(s) { + var o = null; + try { + o = jsonParse(s); + if (typeof o != "object") return null; + if (o.constructor === Array) return null; + return o; + } catch (ex) { + return null; + } +}; + +/** + * get Encoed Signature Value from JWS string.
+ * @name getEncodedSignatureValueFromJWS + * @memberOf KJUR.jws.JWS + * @function + * @static + * @param {String} sJWS JWS signature string to be verified + * @return {String} string of Encoded Signature Value + * @throws if sJWS is not comma separated string such like "Header.Payload.Signature". + */ +KJUR.jws.JWS.getEncodedSignatureValueFromJWS = function(sJWS) { + if (sJWS.match(/^[^.]+\.[^.]+\.([^.]+)$/) == null) { + throw "JWS signature is not a form of 'Head.Payload.SigValue'."; + } + return RegExp.$1; +}; + +/** + * IntDate class for time representation for JSON Web Token(JWT) + * @class KJUR.jws.IntDate class + * @name KJUR.jws.IntDate + * @since jws 3.0.1 + * @description + * Utility class for IntDate which is integer representation of UNIX origin time + * used in JSON Web Token(JWT). + */ +KJUR.jws.IntDate = function() { +}; + +/** + * @name get + * @memberOf KJUR.jws.IntDate + * @function + * @static + * @param {String} s string of time representation + * @return {Integer} UNIX origin time in seconds for argument 's' + * @since jws 3.0.1 + * @throws "unsupported format: s" when malformed format + * @description + * This method will accept following representation of time. + *
    + *
  • now - current time
  • + *
  • now + 1hour - after 1 hour from now
  • + *
  • now + 1day - after 1 day from now
  • + *
  • now + 1month - after 30 days from now
  • + *
  • now + 1year - after 365 days from now
  • + *
  • YYYYmmDDHHMMSSZ - UTC time (ex. 20130828235959Z)
  • + *
  • number - UNIX origin time (seconds from 1970-01-01 00:00:00) (ex. 1377714748)
  • + *
+ */ +KJUR.jws.IntDate.get = function(s) { + if (s == "now") { + return KJUR.jws.IntDate.getNow(); + } else if (s == "now + 1hour") { + return KJUR.jws.IntDate.getNow() + 60 * 60; + } else if (s == "now + 1day") { + return KJUR.jws.IntDate.getNow() + 60 * 60 * 24; + } else if (s == "now + 1month") { + return KJUR.jws.IntDate.getNow() + 60 * 60 * 24 * 30; + } else if (s == "now + 1year") { + return KJUR.jws.IntDate.getNow() + 60 * 60 * 24 * 365; + } else if (s.match(/Z$/)) { + return KJUR.jws.IntDate.getZulu(s); + } else if (s.match(/^[0-9]+$/)) { + return parseInt(s); + } + throw "unsupported format: " + s; +}; + +KJUR.jws.IntDate.getZulu = function(s) { + if (a = s.match(/(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)Z/)) { + var year = parseInt(RegExp.$1); + var month = parseInt(RegExp.$2) - 1; + var day = parseInt(RegExp.$3); + var hour = parseInt(RegExp.$4); + var min = parseInt(RegExp.$5); + var sec = parseInt(RegExp.$6); + var d = new Date(Date.UTC(year, month, day, hour, min, sec)); + return ~~(d / 1000); + } + throw "unsupported format: " + s; +}; + +/* + * @since jws 3.0.1 + */ +KJUR.jws.IntDate.getNow = function() { + var d = ~~(new Date() / 1000); + return d; +}; + +/* + * @since jws 3.0.1 + */ +KJUR.jws.IntDate.intDate2UTCString = function(intDate) { + var d = new Date(intDate * 1000); + return d.toUTCString(); +}; + +/* + * @since jws 3.0.1 + */ +KJUR.jws.IntDate.intDate2Zulu = function(intDate) { + var d = new Date(intDate * 1000); + var year = ("0000" + d.getUTCFullYear()).slice(-4); + var mon = ("00" + (d.getUTCMonth() + 1)).slice(-2); + var day = ("00" + d.getUTCDate()).slice(-2); + var hour = ("00" + d.getUTCHours()).slice(-2); + var min = ("00" + d.getUTCMinutes()).slice(-2); + var sec = ("00" + d.getUTCSeconds()).slice(-2); + return year + mon + day + hour + min + sec + "Z"; +}; + +/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE + * @version 3.0.2 + */ + +(function() { + "use strict"; + function lib$es6$promise$utils$$objectOrFunction(x) { + return typeof x === 'function' || (typeof x === 'object' && x !== null); + } + + function lib$es6$promise$utils$$isFunction(x) { + return typeof x === 'function'; + } + + function lib$es6$promise$utils$$isMaybeThenable(x) { + return typeof x === 'object' && x !== null; + } + + var lib$es6$promise$utils$$_isArray; + if (!Array.isArray) { + lib$es6$promise$utils$$_isArray = function (x) { + return Object.prototype.toString.call(x) === '[object Array]'; + }; + } else { + lib$es6$promise$utils$$_isArray = Array.isArray; + } + + var lib$es6$promise$utils$$isArray = lib$es6$promise$utils$$_isArray; + var lib$es6$promise$asap$$len = 0; + var lib$es6$promise$asap$$toString = {}.toString; + var lib$es6$promise$asap$$vertxNext; + var lib$es6$promise$asap$$customSchedulerFn; + + var lib$es6$promise$asap$$asap = function asap(callback, arg) { + lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len] = callback; + lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len + 1] = arg; + lib$es6$promise$asap$$len += 2; + if (lib$es6$promise$asap$$len === 2) { + // If len is 2, that means that we need to schedule an async flush. + // If additional callbacks are queued before the queue is flushed, they + // will be processed by this flush that we are scheduling. + if (lib$es6$promise$asap$$customSchedulerFn) { + lib$es6$promise$asap$$customSchedulerFn(lib$es6$promise$asap$$flush); + } else { + lib$es6$promise$asap$$scheduleFlush(); + } + } + } + + function lib$es6$promise$asap$$setScheduler(scheduleFn) { + lib$es6$promise$asap$$customSchedulerFn = scheduleFn; + } + + function lib$es6$promise$asap$$setAsap(asapFn) { + lib$es6$promise$asap$$asap = asapFn; + } + + var lib$es6$promise$asap$$browserWindow = (typeof window !== 'undefined') ? window : undefined; + var lib$es6$promise$asap$$browserGlobal = lib$es6$promise$asap$$browserWindow || {}; + var lib$es6$promise$asap$$BrowserMutationObserver = lib$es6$promise$asap$$browserGlobal.MutationObserver || lib$es6$promise$asap$$browserGlobal.WebKitMutationObserver; + var lib$es6$promise$asap$$isNode = typeof process !== 'undefined' && {}.toString.call(process) === '[object process]'; + + // test for web worker but not in IE10 + var lib$es6$promise$asap$$isWorker = typeof Uint8ClampedArray !== 'undefined' && + typeof importScripts !== 'undefined' && + typeof MessageChannel !== 'undefined'; + + // node + function lib$es6$promise$asap$$useNextTick() { + // node version 0.10.x displays a deprecation warning when nextTick is used recursively + // see https://github.com/cujojs/when/issues/410 for details + return function() { + process.nextTick(lib$es6$promise$asap$$flush); + }; + } + + // vertx + function lib$es6$promise$asap$$useVertxTimer() { + return function() { + lib$es6$promise$asap$$vertxNext(lib$es6$promise$asap$$flush); + }; + } + + function lib$es6$promise$asap$$useMutationObserver() { + var iterations = 0; + var observer = new lib$es6$promise$asap$$BrowserMutationObserver(lib$es6$promise$asap$$flush); + var node = document.createTextNode(''); + observer.observe(node, { characterData: true }); + + return function() { + node.data = (iterations = ++iterations % 2); + }; + } + + // web worker + function lib$es6$promise$asap$$useMessageChannel() { + var channel = new MessageChannel(); + channel.port1.onmessage = lib$es6$promise$asap$$flush; + return function () { + channel.port2.postMessage(0); + }; + } + + function lib$es6$promise$asap$$useSetTimeout() { + return function() { + setTimeout(lib$es6$promise$asap$$flush, 1); + }; + } + + var lib$es6$promise$asap$$queue = new Array(1000); + function lib$es6$promise$asap$$flush() { + for (var i = 0; i < lib$es6$promise$asap$$len; i+=2) { + var callback = lib$es6$promise$asap$$queue[i]; + var arg = lib$es6$promise$asap$$queue[i+1]; + + callback(arg); + + lib$es6$promise$asap$$queue[i] = undefined; + lib$es6$promise$asap$$queue[i+1] = undefined; + } + + lib$es6$promise$asap$$len = 0; + } + + function lib$es6$promise$asap$$attemptVertx() { + try { + var r = require; + var vertx = r('vertx'); + lib$es6$promise$asap$$vertxNext = vertx.runOnLoop || vertx.runOnContext; + return lib$es6$promise$asap$$useVertxTimer(); + } catch(e) { + return lib$es6$promise$asap$$useSetTimeout(); + } + } + + var lib$es6$promise$asap$$scheduleFlush; + // Decide what async method to use to triggering processing of queued callbacks: + if (lib$es6$promise$asap$$isNode) { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useNextTick(); + } else if (lib$es6$promise$asap$$BrowserMutationObserver) { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useMutationObserver(); + } else if (lib$es6$promise$asap$$isWorker) { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useMessageChannel(); + } else if (lib$es6$promise$asap$$browserWindow === undefined && typeof require === 'function') { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$attemptVertx(); + } else { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useSetTimeout(); + } + + function lib$es6$promise$$internal$$noop() {} + + var lib$es6$promise$$internal$$PENDING = void 0; + var lib$es6$promise$$internal$$FULFILLED = 1; + var lib$es6$promise$$internal$$REJECTED = 2; + + var lib$es6$promise$$internal$$GET_THEN_ERROR = new lib$es6$promise$$internal$$ErrorObject(); + + function lib$es6$promise$$internal$$selfFulfillment() { + return new TypeError("You cannot resolve a promise with itself"); + } + + function lib$es6$promise$$internal$$cannotReturnOwn() { + return new TypeError('A promises callback cannot return that same promise.'); + } + + function lib$es6$promise$$internal$$getThen(promise) { + try { + return promise.then; + } catch(error) { + lib$es6$promise$$internal$$GET_THEN_ERROR.error = error; + return lib$es6$promise$$internal$$GET_THEN_ERROR; + } + } + + function lib$es6$promise$$internal$$tryThen(then, value, fulfillmentHandler, rejectionHandler) { + try { + then.call(value, fulfillmentHandler, rejectionHandler); + } catch(e) { + return e; + } + } + + function lib$es6$promise$$internal$$handleForeignThenable(promise, thenable, then) { + lib$es6$promise$asap$$asap(function(promise) { + var sealed = false; + var error = lib$es6$promise$$internal$$tryThen(then, thenable, function(value) { + if (sealed) { return; } + sealed = true; + if (thenable !== value) { + lib$es6$promise$$internal$$resolve(promise, value); + } else { + lib$es6$promise$$internal$$fulfill(promise, value); + } + }, function(reason) { + if (sealed) { return; } + sealed = true; + + lib$es6$promise$$internal$$reject(promise, reason); + }, 'Settle: ' + (promise._label || ' unknown promise')); + + if (!sealed && error) { + sealed = true; + lib$es6$promise$$internal$$reject(promise, error); + } + }, promise); + } + + function lib$es6$promise$$internal$$handleOwnThenable(promise, thenable) { + if (thenable._state === lib$es6$promise$$internal$$FULFILLED) { + lib$es6$promise$$internal$$fulfill(promise, thenable._result); + } else if (thenable._state === lib$es6$promise$$internal$$REJECTED) { + lib$es6$promise$$internal$$reject(promise, thenable._result); + } else { + lib$es6$promise$$internal$$subscribe(thenable, undefined, function(value) { + lib$es6$promise$$internal$$resolve(promise, value); + }, function(reason) { + lib$es6$promise$$internal$$reject(promise, reason); + }); + } + } + + function lib$es6$promise$$internal$$handleMaybeThenable(promise, maybeThenable) { + if (maybeThenable.constructor === promise.constructor) { + lib$es6$promise$$internal$$handleOwnThenable(promise, maybeThenable); + } else { + var then = lib$es6$promise$$internal$$getThen(maybeThenable); + + if (then === lib$es6$promise$$internal$$GET_THEN_ERROR) { + lib$es6$promise$$internal$$reject(promise, lib$es6$promise$$internal$$GET_THEN_ERROR.error); + } else if (then === undefined) { + lib$es6$promise$$internal$$fulfill(promise, maybeThenable); + } else if (lib$es6$promise$utils$$isFunction(then)) { + lib$es6$promise$$internal$$handleForeignThenable(promise, maybeThenable, then); + } else { + lib$es6$promise$$internal$$fulfill(promise, maybeThenable); + } + } + } + + function lib$es6$promise$$internal$$resolve(promise, value) { + if (promise === value) { + lib$es6$promise$$internal$$reject(promise, lib$es6$promise$$internal$$selfFulfillment()); + } else if (lib$es6$promise$utils$$objectOrFunction(value)) { + lib$es6$promise$$internal$$handleMaybeThenable(promise, value); + } else { + lib$es6$promise$$internal$$fulfill(promise, value); + } + } + + function lib$es6$promise$$internal$$publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._result); + } + + lib$es6$promise$$internal$$publish(promise); + } + + function lib$es6$promise$$internal$$fulfill(promise, value) { + if (promise._state !== lib$es6$promise$$internal$$PENDING) { return; } + + promise._result = value; + promise._state = lib$es6$promise$$internal$$FULFILLED; + + if (promise._subscribers.length !== 0) { + lib$es6$promise$asap$$asap(lib$es6$promise$$internal$$publish, promise); + } + } + + function lib$es6$promise$$internal$$reject(promise, reason) { + if (promise._state !== lib$es6$promise$$internal$$PENDING) { return; } + promise._state = lib$es6$promise$$internal$$REJECTED; + promise._result = reason; + + lib$es6$promise$asap$$asap(lib$es6$promise$$internal$$publishRejection, promise); + } + + function lib$es6$promise$$internal$$subscribe(parent, child, onFulfillment, onRejection) { + var subscribers = parent._subscribers; + var length = subscribers.length; + + parent._onerror = null; + + subscribers[length] = child; + subscribers[length + lib$es6$promise$$internal$$FULFILLED] = onFulfillment; + subscribers[length + lib$es6$promise$$internal$$REJECTED] = onRejection; + + if (length === 0 && parent._state) { + lib$es6$promise$asap$$asap(lib$es6$promise$$internal$$publish, parent); + } + } + + function lib$es6$promise$$internal$$publish(promise) { + var subscribers = promise._subscribers; + var settled = promise._state; + + if (subscribers.length === 0) { return; } + + var child, callback, detail = promise._result; + + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; + + if (child) { + lib$es6$promise$$internal$$invokeCallback(settled, child, callback, detail); + } else { + callback(detail); + } + } + + promise._subscribers.length = 0; + } + + function lib$es6$promise$$internal$$ErrorObject() { + this.error = null; + } + + var lib$es6$promise$$internal$$TRY_CATCH_ERROR = new lib$es6$promise$$internal$$ErrorObject(); + + function lib$es6$promise$$internal$$tryCatch(callback, detail) { + try { + return callback(detail); + } catch(e) { + lib$es6$promise$$internal$$TRY_CATCH_ERROR.error = e; + return lib$es6$promise$$internal$$TRY_CATCH_ERROR; + } + } + + function lib$es6$promise$$internal$$invokeCallback(settled, promise, callback, detail) { + var hasCallback = lib$es6$promise$utils$$isFunction(callback), + value, error, succeeded, failed; + + if (hasCallback) { + value = lib$es6$promise$$internal$$tryCatch(callback, detail); + + if (value === lib$es6$promise$$internal$$TRY_CATCH_ERROR) { + failed = true; + error = value.error; + value = null; + } else { + succeeded = true; + } + + if (promise === value) { + lib$es6$promise$$internal$$reject(promise, lib$es6$promise$$internal$$cannotReturnOwn()); + return; + } + + } else { + value = detail; + succeeded = true; + } + + if (promise._state !== lib$es6$promise$$internal$$PENDING) { + // noop + } else if (hasCallback && succeeded) { + lib$es6$promise$$internal$$resolve(promise, value); + } else if (failed) { + lib$es6$promise$$internal$$reject(promise, error); + } else if (settled === lib$es6$promise$$internal$$FULFILLED) { + lib$es6$promise$$internal$$fulfill(promise, value); + } else if (settled === lib$es6$promise$$internal$$REJECTED) { + lib$es6$promise$$internal$$reject(promise, value); + } + } + + function lib$es6$promise$$internal$$initializePromise(promise, resolver) { + try { + resolver(function resolvePromise(value){ + lib$es6$promise$$internal$$resolve(promise, value); + }, function rejectPromise(reason) { + lib$es6$promise$$internal$$reject(promise, reason); + }); + } catch(e) { + lib$es6$promise$$internal$$reject(promise, e); + } + } + + function lib$es6$promise$enumerator$$Enumerator(Constructor, input) { + var enumerator = this; + + enumerator._instanceConstructor = Constructor; + enumerator.promise = new Constructor(lib$es6$promise$$internal$$noop); + + if (enumerator._validateInput(input)) { + enumerator._input = input; + enumerator.length = input.length; + enumerator._remaining = input.length; + + enumerator._init(); + + if (enumerator.length === 0) { + lib$es6$promise$$internal$$fulfill(enumerator.promise, enumerator._result); + } else { + enumerator.length = enumerator.length || 0; + enumerator._enumerate(); + if (enumerator._remaining === 0) { + lib$es6$promise$$internal$$fulfill(enumerator.promise, enumerator._result); + } + } + } else { + lib$es6$promise$$internal$$reject(enumerator.promise, enumerator._validationError()); + } + } + + lib$es6$promise$enumerator$$Enumerator.prototype._validateInput = function(input) { + return lib$es6$promise$utils$$isArray(input); + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._validationError = function() { + return new Error('Array Methods must be provided an Array'); + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._init = function() { + this._result = new Array(this.length); + }; + + var lib$es6$promise$enumerator$$default = lib$es6$promise$enumerator$$Enumerator; + + lib$es6$promise$enumerator$$Enumerator.prototype._enumerate = function() { + var enumerator = this; + + var length = enumerator.length; + var promise = enumerator.promise; + var input = enumerator._input; + + for (var i = 0; promise._state === lib$es6$promise$$internal$$PENDING && i < length; i++) { + enumerator._eachEntry(input[i], i); + } + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._eachEntry = function(entry, i) { + var enumerator = this; + var c = enumerator._instanceConstructor; + + if (lib$es6$promise$utils$$isMaybeThenable(entry)) { + if (entry.constructor === c && entry._state !== lib$es6$promise$$internal$$PENDING) { + entry._onerror = null; + enumerator._settledAt(entry._state, i, entry._result); + } else { + enumerator._willSettleAt(c.resolve(entry), i); + } + } else { + enumerator._remaining--; + enumerator._result[i] = entry; + } + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._settledAt = function(state, i, value) { + var enumerator = this; + var promise = enumerator.promise; + + if (promise._state === lib$es6$promise$$internal$$PENDING) { + enumerator._remaining--; + + if (state === lib$es6$promise$$internal$$REJECTED) { + lib$es6$promise$$internal$$reject(promise, value); + } else { + enumerator._result[i] = value; + } + } + + if (enumerator._remaining === 0) { + lib$es6$promise$$internal$$fulfill(promise, enumerator._result); + } + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._willSettleAt = function(promise, i) { + var enumerator = this; + + lib$es6$promise$$internal$$subscribe(promise, undefined, function(value) { + enumerator._settledAt(lib$es6$promise$$internal$$FULFILLED, i, value); + }, function(reason) { + enumerator._settledAt(lib$es6$promise$$internal$$REJECTED, i, reason); + }); + }; + function lib$es6$promise$promise$all$$all(entries) { + return new lib$es6$promise$enumerator$$default(this, entries).promise; + } + var lib$es6$promise$promise$all$$default = lib$es6$promise$promise$all$$all; + function lib$es6$promise$promise$race$$race(entries) { + /*jshint validthis:true */ + var Constructor = this; + + var promise = new Constructor(lib$es6$promise$$internal$$noop); + + if (!lib$es6$promise$utils$$isArray(entries)) { + lib$es6$promise$$internal$$reject(promise, new TypeError('You must pass an array to race.')); + return promise; + } + + var length = entries.length; + + function onFulfillment(value) { + lib$es6$promise$$internal$$resolve(promise, value); + } + + function onRejection(reason) { + lib$es6$promise$$internal$$reject(promise, reason); + } + + for (var i = 0; promise._state === lib$es6$promise$$internal$$PENDING && i < length; i++) { + lib$es6$promise$$internal$$subscribe(Constructor.resolve(entries[i]), undefined, onFulfillment, onRejection); + } + + return promise; + } + var lib$es6$promise$promise$race$$default = lib$es6$promise$promise$race$$race; + function lib$es6$promise$promise$resolve$$resolve(object) { + /*jshint validthis:true */ + var Constructor = this; + + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } + + var promise = new Constructor(lib$es6$promise$$internal$$noop); + lib$es6$promise$$internal$$resolve(promise, object); + return promise; + } + var lib$es6$promise$promise$resolve$$default = lib$es6$promise$promise$resolve$$resolve; + function lib$es6$promise$promise$reject$$reject(reason) { + /*jshint validthis:true */ + var Constructor = this; + var promise = new Constructor(lib$es6$promise$$internal$$noop); + lib$es6$promise$$internal$$reject(promise, reason); + return promise; + } + var lib$es6$promise$promise$reject$$default = lib$es6$promise$promise$reject$$reject; + + var lib$es6$promise$promise$$counter = 0; + + function lib$es6$promise$promise$$needsResolver() { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); + } + + function lib$es6$promise$promise$$needsNew() { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); + } + + var lib$es6$promise$promise$$default = lib$es6$promise$promise$$Promise; + /** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise's eventual value or the reason + why the promise cannot be fulfilled. + + Terminology + ----------- + + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. + + A promise can be in one of three states: pending, fulfilled, or rejected. + + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. + + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. + + + Basic Usage: + ------------ + + ```js + var promise = new Promise(function(resolve, reject) { + // on success + resolve(value); + + // on failure + reject(reason); + }); + + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Advanced Usage: + --------------- + + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. + + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + var xhr = new XMLHttpRequest(); + + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); + + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']')); + } + } + }; + }); + } + + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Unlike callbacks, promises are great composable primitives. + + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON + + return values; + }); + ``` + + @class Promise + @param {function} resolver + Useful for tooling. + @constructor + */ + function lib$es6$promise$promise$$Promise(resolver) { + this._id = lib$es6$promise$promise$$counter++; + this._state = undefined; + this._result = undefined; + this._subscribers = []; + + if (lib$es6$promise$$internal$$noop !== resolver) { + if (!lib$es6$promise$utils$$isFunction(resolver)) { + lib$es6$promise$promise$$needsResolver(); + } + + if (!(this instanceof lib$es6$promise$promise$$Promise)) { + lib$es6$promise$promise$$needsNew(); + } + + lib$es6$promise$$internal$$initializePromise(this, resolver); + } + } + + lib$es6$promise$promise$$Promise.all = lib$es6$promise$promise$all$$default; + lib$es6$promise$promise$$Promise.race = lib$es6$promise$promise$race$$default; + lib$es6$promise$promise$$Promise.resolve = lib$es6$promise$promise$resolve$$default; + lib$es6$promise$promise$$Promise.reject = lib$es6$promise$promise$reject$$default; + lib$es6$promise$promise$$Promise._setScheduler = lib$es6$promise$asap$$setScheduler; + lib$es6$promise$promise$$Promise._setAsap = lib$es6$promise$asap$$setAsap; + lib$es6$promise$promise$$Promise._asap = lib$es6$promise$asap$$asap; + + lib$es6$promise$promise$$Promise.prototype = { + constructor: lib$es6$promise$promise$$Promise, + + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` + + Chaining + -------- + + The return value of `then` is itself a promise. This second, 'downstream' + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return 'default name'; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `'default name'` + }); + + findUser().then(function (user) { + throw new Error('Found user, but still unhappy'); + }, function (reason) { + throw new Error('`findUser` rejected and we're unhappy'); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'. + // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'. + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + + ```js + findUser().then(function (user) { + throw new PedagogicalException('Upstream error'); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` + + Assimilation + ------------ + + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` + + If the assimliated promise rejects, then the downstream promise will also reject. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` + + Simple Example + -------------- + + Synchronous Example + + ```javascript + var result; + + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` + + Promise Example; + + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` + + Advanced Example + -------------- + + Synchronous Example + + ```javascript + var author, books; + + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + + function foundBooks(books) { + + } + + function failure(reason) { + + } + + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` + + Promise Example; + + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` + + @method then + @param {Function} onFulfilled + @param {Function} onRejected + Useful for tooling. + @return {Promise} + */ + then: function(onFulfillment, onRejection) { + var parent = this; + var state = parent._state; + + if (state === lib$es6$promise$$internal$$FULFILLED && !onFulfillment || state === lib$es6$promise$$internal$$REJECTED && !onRejection) { + return this; + } + + var child = new this.constructor(lib$es6$promise$$internal$$noop); + var result = parent._result; + + if (state) { + var callback = arguments[state - 1]; + lib$es6$promise$asap$$asap(function(){ + lib$es6$promise$$internal$$invokeCallback(state, child, callback, result); + }); + } else { + lib$es6$promise$$internal$$subscribe(parent, child, onFulfillment, onRejection); + } + + return child; + }, + + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. + + ```js + function findAuthor(){ + throw new Error('couldn't find that author'); + } + + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } + + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` + + @method catch + @param {Function} onRejection + Useful for tooling. + @return {Promise} + */ + 'catch': function(onRejection) { + return this.then(null, onRejection); + } + }; + function lib$es6$promise$polyfill$$polyfill() { + var local; + + if (typeof global !== 'undefined') { + local = global; + } else if (typeof self !== 'undefined') { + local = self; + } else { + try { + local = Function('return this')(); + } catch (e) { + throw new Error('polyfill failed because global object is unavailable in this environment'); + } + } + + var P = local.Promise; + + if (P && Object.prototype.toString.call(P.resolve()) === '[object Promise]' && !P.cast) { + return; + } + + local.Promise = lib$es6$promise$promise$$default; + } + var lib$es6$promise$polyfill$$default = lib$es6$promise$polyfill$$polyfill; + + var lib$es6$promise$umd$$ES6Promise = { + 'Promise': lib$es6$promise$promise$$default, + 'polyfill': lib$es6$promise$polyfill$$default + }; + + /* global define:true module:true window: true */ + if (typeof define === 'function' && define['amd']) { + define(function() { return lib$es6$promise$umd$$ES6Promise; }); + } else if (typeof module !== 'undefined' && module['exports']) { + module['exports'] = lib$es6$promise$umd$$ES6Promise; + } else if (typeof this !== 'undefined') { + this['ES6Promise'] = lib$es6$promise$umd$$ES6Promise; + } + + lib$es6$promise$polyfill$$default(); +}).call(this); + + +/** + * @constructor + */ +function DefaultHttpRequest() { + + /** + * @name _promiseFactory + * @type DefaultPromiseFactory + */ + + /** + * @param {XMLHttpRequest} xhr + * @param {object.} headers + */ + function setHeaders(xhr, headers) { + var keys = Object.keys(headers); + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var value = headers[key]; + + xhr.setRequestHeader(key, value); + } + } + + /** + * @param {string} url + * @param {{ headers: object. }} [config] + * @returns {Promise} + */ + this.getJSON = function (url, config) { + return _promiseFactory.create(function (resolve, reject) { + + try { + var xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.responseType = "json"; + + if (config) { + if (config.headers) { + setHeaders(xhr, config.headers); + } + } + + xhr.onload = function () { + try { + if (xhr.status === 200) { + var response = ""; + // To support IE9 get the response from xhr.responseText not xhr.response + if (window.XDomainRequest) { + response = xhr.responseText; + } else { + response = xhr.response; + } + if (typeof response === "string") { + response = JSON.parse(response); + } + resolve(response); + } + else { + reject(Error(xhr.statusText + "(" + xhr.status + ")")); + } + } + catch (err) { + reject(err); + } + }; + + xhr.onerror = function () { + reject(Error("Network error")); + }; + + xhr.send(); + } + catch (err) { + return reject(err); + } + }); + }; +} + +_httpRequest = new DefaultHttpRequest(); + +/** + * @constructor + * @param {Promise} promise + */ +function DefaultPromise(promise) { + + /** + * @param {function(*):*} successCallback + * @param {function(*):*} errorCallback + * @returns {DefaultPromise} + */ + this.then = function (successCallback, errorCallback) { + var childPromise = promise.then(successCallback, errorCallback); + + return new DefaultPromise(childPromise); + }; + + /** + * + * @param {function(*):*} errorCallback + * @returns {DefaultPromise} + */ + this.catch = function (errorCallback) { + var childPromise = promise.catch(errorCallback); + + return new DefaultPromise(childPromise); + }; +} + +/** + * @constructor + */ +function DefaultPromiseFactory() { + + this.resolve = function (value) { + return new DefaultPromise(Promise.resolve(value)); + }; + + this.reject = function (reason) { + return new DefaultPromise(Promise.reject(reason)); + }; + + /** + * @param {function(resolve:function, reject:function)} callback + * @returns {DefaultPromise} + */ + this.create = function (callback) { + return new DefaultPromise(new Promise(callback)); + }; +} + +_promiseFactory = new DefaultPromiseFactory(); +/* + * Copyright 2015 Dominick Baier, Brock Allen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function log() { + //var param = [].join.call(arguments); + //console.log(param); +} + +function copy(obj, target) { + target = target || {}; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + target[key] = obj[key]; + } + } + return target; +} + +function rand() { + return ((Date.now() + Math.random()) * Math.random()).toString().replace(".", ""); +} + +function resolve(param) { + return _promiseFactory.resolve(param); +} + +function error(message) { + return _promiseFactory.reject(Error(message)); +} + +function parseOidcResult(queryString) { + log("parseOidcResult"); + + queryString = queryString || location.hash; + + var idx = queryString.lastIndexOf("#"); + if (idx >= 0) { + queryString = queryString.substr(idx + 1); + } + + var params = {}, + regex = /([^&=]+)=([^&]*)/g, + m; + + var counter = 0; + while (m = regex.exec(queryString)) { + params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); + if (counter++ > 50) { + return { + error: "Response exceeded expected number of parameters" + }; + } + } + + for (var prop in params) { + return params; + } +} + +function getJson(url, token) { + log("getJson", url); + + var config = {}; + + if (token) { + config.headers = {"Authorization": "Bearer " + token}; + } + + return _httpRequest.getJSON(url, config); +} + +function OidcClient(settings) { + this._settings = settings || {}; + + if (!this._settings.request_state_key) { + this._settings.request_state_key = "OidcClient.request_state"; + } + + if (!this._settings.request_state_store) { + this._settings.request_state_store = window.localStorage; + } + + if (typeof this._settings.load_user_profile === 'undefined') { + this._settings.load_user_profile = true; + } + + if (typeof this._settings.filter_protocol_claims === 'undefined') { + this._settings.filter_protocol_claims = true; + } + + if (this._settings.authority && this._settings.authority.indexOf('.well-known/openid-configuration') < 0) { + if (this._settings.authority[this._settings.authority.length - 1] !== '/') { + this._settings.authority += '/'; + } + this._settings.authority += '.well-known/openid-configuration'; + } + + if (!this._settings.response_type) { + this._settings.response_type = "id_token token"; + } + + Object.defineProperty(this, "isOidc", { + get: function () { + if (this._settings.response_type) { + var result = this._settings.response_type.split(/\s+/g).filter(function (item) { + return item === "id_token"; + }); + return !!(result[0]); + } + return false; + } + }); + + Object.defineProperty(this, "isOAuth", { + get: function () { + if (this._settings.response_type) { + var result = this._settings.response_type.split(/\s+/g).filter(function (item) { + return item === "token"; + }); + return !!(result[0]); + } + return false; + } + }); +} + +OidcClient.parseOidcResult = parseOidcResult; + +OidcClient.prototype.loadMetadataAsync = function () { + log("OidcClient.loadMetadataAsync"); + + var settings = this._settings; + + if (settings.metadata) { + return resolve(settings.metadata); + } + + if (!settings.authority) { + return error("No authority configured"); + } + + return getJson(settings.authority) + .then(function (metadata) { + settings.metadata = metadata; + return metadata; + }, function (err) { + return error("Failed to load metadata (" + err && err.message + ")"); + }); +}; + +OidcClient.prototype.loadX509SigningKeyAsync = function () { + log("OidcClient.loadX509SigningKeyAsync"); + + var settings = this._settings; + + function getKeyAsync(jwks) { + if (!jwks.keys || !jwks.keys.length) { + return error("Signing keys empty"); + } + + var key = jwks.keys[0]; + if (key.kty !== "RSA") { + return error("Signing key not RSA"); + } + + if (!key.x5c || !key.x5c.length) { + return error("RSA keys empty"); + } + + return resolve(key.x5c[0]); + } + + if (settings.jwks) { + return getKeyAsync(settings.jwks); + } + + return this.loadMetadataAsync().then(function (metadata) { + if (!metadata.jwks_uri) { + return error("Metadata does not contain jwks_uri"); + } + + return getJson(metadata.jwks_uri).then(function (jwks) { + settings.jwks = jwks; + return getKeyAsync(jwks); + }, function (err) { + return error("Failed to load signing keys (" + err && err.message + ")"); + }); + }); +}; + +OidcClient.prototype.loadUserProfile = function (access_token) { + log("OidcClient.loadUserProfile"); + + return this.loadMetadataAsync().then(function (metadata) { + + if (!metadata.userinfo_endpoint) { + return error("Metadata does not contain userinfo_endpoint"); + } + + return getJson(metadata.userinfo_endpoint, access_token); + }); +} + +OidcClient.prototype.loadAuthorizationEndpoint = function () { + log("OidcClient.loadAuthorizationEndpoint"); + + if (this._settings.authorization_endpoint) { + return resolve(this._settings.authorization_endpoint); + } + + if (!this._settings.authority) { + return error("No authorization_endpoint configured"); + } + + return this.loadMetadataAsync().then(function (metadata) { + if (!metadata.authorization_endpoint) { + return error("Metadata does not contain authorization_endpoint"); + } + + return metadata.authorization_endpoint; + }); +}; + +OidcClient.prototype.createTokenRequestAsync = function () { + log("OidcClient.createTokenRequestAsync"); + + var client = this; + var settings = client._settings; + + return client.loadAuthorizationEndpoint().then(function (authorization_endpoint) { + + var state = rand(); + var url = authorization_endpoint + "?state=" + encodeURIComponent(state); + + if (client.isOidc) { + var nonce = rand(); + url += "&nonce=" + encodeURIComponent(nonce); + } + + var required = ["client_id", "redirect_uri", "response_type", "scope"]; + required.forEach(function (key) { + var value = settings[key]; + if (value) { + url += "&" + key + "=" + encodeURIComponent(value); + } + }); + + var optional = ["prompt", "display", "max_age", "ui_locales", "id_token_hint", "login_hint", "acr_values"]; + optional.forEach(function (key) { + var value = settings[key]; + if (value) { + url += "&" + key + "=" + encodeURIComponent(value); + } + }); + + var request_state = { + oidc: client.isOidc, + oauth: client.isOAuth, + state: state + }; + + if (nonce) { + request_state["nonce"] = nonce; + } + + settings.request_state_store.setItem(settings.request_state_key, JSON.stringify(request_state)); + + return { + request_state: request_state, + url: url + }; + }); +} + +OidcClient.prototype.createLogoutRequestAsync = function (id_token_hint) { + log("OidcClient.createLogoutRequestAsync"); + + var settings = this._settings; + return this.loadMetadataAsync().then(function (metadata) { + if (!metadata.end_session_endpoint) { + return error("No end_session_endpoint in metadata"); + } + + var url = metadata.end_session_endpoint; + if (id_token_hint && settings.post_logout_redirect_uri) { + url += "?post_logout_redirect_uri=" + encodeURIComponent(settings.post_logout_redirect_uri); + url += "&id_token_hint=" + encodeURIComponent(id_token_hint); + } + return url; + }); +} + +OidcClient.prototype.validateIdTokenAsync = function (id_token, nonce, access_token) { + log("OidcClient.validateIdTokenAsync"); + + var client = this; + var settings = client._settings; + + return client.loadX509SigningKeyAsync().then(function (cert) { + + var jws = new KJUR.jws.JWS(); + if (jws.verifyJWSByPemX509Cert(id_token, cert)) { + var id_token_contents = JSON.parse(jws.parsedJWS.payloadS); + + if (nonce !== id_token_contents.nonce) { + return error("Invalid nonce"); + } + + return client.loadMetadataAsync().then(function (metadata) { + + if (id_token_contents.iss !== metadata.issuer) { + return error("Invalid issuer"); + } + + if (id_token_contents.aud !== settings.client_id) { + return error("Invalid audience"); + } + + var now = parseInt(Date.now() / 1000); + + // accept tokens issues up to 5 mins ago + var diff = now - id_token_contents.iat; + if (diff > (5 * 60)) { + return error("Token issued too long ago"); + } + + if (id_token_contents.exp < now) { + return error("Token expired"); + } + + if (access_token && settings.load_user_profile) { + // if we have an access token, then call user info endpoint + return client.loadUserProfile(access_token, id_token_contents).then(function (profile) { + return copy(profile, id_token_contents); + }); + } + else { + // no access token, so we have all our claims + return id_token_contents; + } + + }); + } + else { + return error("JWT failed to validate"); + } + + }); + +}; + +OidcClient.prototype.validateAccessTokenAsync = function (id_token_contents, access_token) { + log("OidcClient.validateAccessTokenAsync"); + + if (!id_token_contents.at_hash) { + return error("No at_hash in id_token"); + } + + var hash = KJUR.crypto.Util.sha256(access_token); + var left = hash.substr(0, hash.length / 2); + var left_b64u = hextob64u(left); + + if (left_b64u !== id_token_contents.at_hash) { + return error("at_hash failed to validate"); + } + + return resolve(); +}; + +OidcClient.prototype.validateIdTokenAndAccessTokenAsync = function (id_token, nonce, access_token) { + log("OidcClient.validateIdTokenAndAccessTokenAsync"); + + var client = this; + + return client.validateIdTokenAsync(id_token, nonce, access_token).then(function (id_token_contents) { + + return client.validateAccessTokenAsync(id_token_contents, access_token).then(function () { + + return id_token_contents; + + }); + + }); +} + +OidcClient.prototype.processResponseAsync = function (queryString) { + log("OidcClient.processResponseAsync"); + + var client = this; + var settings = client._settings; + + var request_state = settings.request_state_store.getItem(settings.request_state_key); + settings.request_state_store.removeItem(settings.request_state_key); + + if (!request_state) { + return error("No request state loaded"); + } + + request_state = JSON.parse(request_state); + if (!request_state) { + return error("No request state loaded"); + } + + if (!request_state.state) { + return error("No state loaded"); + } + + var result = parseOidcResult(queryString); + if (!result) { + return error("No OIDC response"); + } + + if (result.error) { + return error(result.error); + } + + if (result.state !== request_state.state) { + return error("Invalid state"); + } + + if (request_state.oidc) { + if (!result.id_token) { + return error("No identity token"); + } + + if (!request_state.nonce) { + return error("No nonce loaded"); + } + } + + if (request_state.oauth) { + if (!result.access_token) { + return error("No access token"); + } + + if (!result.token_type || result.token_type.toLowerCase() !== "bearer") { + return error("Invalid token type"); + } + + if (!result.expires_in) { + return error("No token expiration"); + } + } + + var promise = resolve(); + if (request_state.oidc && request_state.oauth) { + promise = client.validateIdTokenAndAccessTokenAsync(result.id_token, request_state.nonce, result.access_token); + } + else if (request_state.oidc) { + promise = client.validateIdTokenAsync(result.id_token, request_state.nonce); + } + + return promise.then(function (profile) { + if (profile && settings.filter_protocol_claims) { + var remove = ["nonce", "at_hash", "iat", "nbf", "exp", "aud", "iss"]; + remove.forEach(function (key) { + delete profile[key]; + }); + } + + return { + profile: profile, + id_token: result.id_token, + access_token: result.access_token, + expires_in: result.expires_in, + scope: result.scope, + session_state : result.session_state + }; + }); +} + + // exports + OidcClient._promiseFactory = _promiseFactory; + OidcClient._httpRequest = _httpRequest; + window.OidcClient = OidcClient; +})(); +(function () { + +/* +* Copyright 2014-2016 Dominick Baier, Brock Allen +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +// globals +var _promiseFactory = OidcClient._promiseFactory; +var _httpRequest = OidcClient._httpRequest; + +function copy(obj, target) { + target = target || {}; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + target[key] = obj[key]; + } + } + return target; +} + +function Token(other) { + if (other) { + this.profile = other.profile; + this.id_token = other.id_token; + this.access_token = other.access_token; + if (other.access_token) { + this.expires_at = parseInt(other.expires_at); + } + else if (other.id_token) { + this.expires_at = other.profile.exp; + } + else { + throw Error("Either access_token or id_token required."); + } + this.scope = other.scope; + this.session_state = other.session_state; + } + else { + this.expires_at = 0; + } + + Object.defineProperty(this, "scopes", { + get: function () { + return (this.scope || "").split(" "); + } + }); + + Object.defineProperty(this, "expired", { + get: function () { + var now = parseInt(Date.now() / 1000); + return this.expires_at < now; + } + }); + + Object.defineProperty(this, "expires_in", { + get: function () { + var now = parseInt(Date.now() / 1000); + return this.expires_at - now; + } + }); +} + +Token.fromResponse = function (response) { + if (response.access_token) { + var now = parseInt(Date.now() / 1000); + response.expires_at = now + parseInt(response.expires_in); + } + return new Token(response); +} + +Token.fromJSON = function (json) { + if (json) { + try { + var obj = JSON.parse(json); + return new Token(obj); + } + catch (e) { + } + } + return new Token(null); +} + +Token.prototype.toJSON = function () { + return JSON.stringify({ + profile: this.profile, + id_token: this.id_token, + access_token: this.access_token, + expires_at: this.expires_at, + scope: this.scopes.join(" "), + session_state: this.session_state + }); +} + +function FrameLoader(url, config) { + this.url = url; + config = config || {}; + config.cancelDelay = config.cancelDelay || 5000; + this.config = config; +} + +FrameLoader.prototype.loadAsync = function (url) { + var self = this; + url = url || this.url; + + if (!url) { + return _promiseFactory.reject(Error("No url provided")); + } + + return _promiseFactory.create(function (resolve, reject) { + var frame = window.document.createElement("iframe"); + frame.style.display = "none"; + + function cleanup() { + window.removeEventListener("message", message, false); + if (handle) { + window.clearTimeout(handle); + } + handle = null; + window.document.body.removeChild(frame); + } + + function cancel(e) { + cleanup(); + reject(); + } + + function message(e) { + if (handle && e.origin === location.protocol + "//" + location.host && e.source == frame.contentWindow) { + cleanup(); + resolve(e.data); + } + } + + var handle = window.setTimeout(cancel, self.config.cancelDelay); + window.addEventListener("message", message, false); + window.document.body.appendChild(frame); + frame.src = url; + }); +} + +function loadToken(mgr) { + mgr._token = null; + if (mgr._settings.persist) { + var tokenJson = mgr._settings.store.getItem(mgr._settings.persistKey); + if (tokenJson) { + var token = Token.fromJSON(tokenJson); + if (!token.expired) { + mgr._token = token; + } + } + } +} + +function configureTokenExpiring(mgr) { + + function callback() { + handle = null; + mgr._callTokenExpiring(); + } + + var handle = null; + + function cancel() { + if (handle) { + window.clearTimeout(handle); + handle = null; + } + } + + function setup(duration) { + handle = window.setTimeout(callback, duration * 1000); + } + + function configure() { + cancel(); + + if (!mgr.expired) { + var duration = mgr.expires_in; + if (duration > 60) { + setup(duration - 60); + } + else { + callback(); + } + } + } + + configure(); + + mgr.addOnTokenObtained(configure); + mgr.addOnTokenRemoved(cancel); +} + +function configureAutoRenewToken(mgr) { + + if (mgr._settings.silent_redirect_uri && mgr._settings.silent_renew) { + + mgr.addOnTokenExpiring(function () { + mgr.renewTokenSilentAsync().catch(function (e) { + mgr._callSilentTokenRenewFailed(); + console.error(e && e.message || "Unknown error"); + }); + }); + + } +} + +function configureTokenExpired(mgr) { + + function callback() { + handle = null; + + if (mgr._token) { + mgr.saveToken(null); + } + + mgr._callTokenExpired(); + } + + var handle = null; + + function cancel() { + if (handle) { + window.clearTimeout(handle); + handle = null; + } + } + + function setup(duration) { + handle = window.setTimeout(callback, duration * 1000); + } + + function configure() { + cancel(); + if (mgr.expires_in > 0) { + // register 1 second beyond expiration so we don't get into edge conditions for expiration + setup(mgr.expires_in + 1); + } + } + + configure(); + + mgr.addOnTokenObtained(configure); + mgr.addOnTokenRemoved(cancel); +} + +function TokenManager(settings) { + this._settings = settings || {}; + + if (typeof this._settings.persist === 'undefined') { + this._settings.persist = true; + } + this._settings.store = this._settings.store || window.localStorage; + this._settings.persistKey = this._settings.persistKey || "TokenManager.token"; + + this.oidcClient = new OidcClient(this._settings); + + this._callbacks = { + tokenRemovedCallbacks: [], + tokenExpiringCallbacks: [], + tokenExpiredCallbacks: [], + tokenObtainedCallbacks: [], + silentTokenRenewFailedCallbacks: [] + }; + + Object.defineProperty(this, "profile", { + get: function () { + if (this._token) { + return this._token.profile; + } + } + }); + Object.defineProperty(this, "id_token", { + get: function () { + if (this._token) { + return this._token.id_token; + } + } + }); + Object.defineProperty(this, "access_token", { + get: function () { + if (this._token && !this._token.expired) { + return this._token.access_token; + } + } + }); + Object.defineProperty(this, "expired", { + get: function () { + if (this._token) { + return this._token.expired; + } + return true; + } + }); + Object.defineProperty(this, "expires_in", { + get: function () { + if (this._token) { + return this._token.expires_in; + } + return 0; + } + }); + Object.defineProperty(this, "expires_at", { + get: function () { + if (this._token) { + return this._token.expires_at; + } + return 0; + } + }); + Object.defineProperty(this, "scope", { + get: function () { + return this._token && this._token.scope; + } + }); + Object.defineProperty(this, "scopes", { + get: function () { + if (this._token) { + return [].concat(this._token.scopes); + } + return []; + } + }); + Object.defineProperty(this, "session_state", { + get: function () { + if (this._token) { + return this._token.session_state; + } + } + }); + + var mgr = this; + loadToken(mgr); + if (mgr._settings.store instanceof window.localStorage.constructor) { + window.addEventListener("storage", function (e) { + if (e.key === mgr._settings.persistKey) { + loadToken(mgr); + + if (mgr._token) { + mgr._callTokenObtained(); + } + else { + mgr._callTokenRemoved(); + } + } + }); + } + configureTokenExpired(mgr); + configureAutoRenewToken(mgr); + + // delay this so consuming apps can register for callbacks first + window.setTimeout(function () { + configureTokenExpiring(mgr); + }, 0); +} + +/** + * @param {{ create:function(successCallback:function(), errorCallback:function()):Promise, resolve:function(value:*):Promise, reject:function():Promise}} promiseFactory + */ +TokenManager.setPromiseFactory = function (promiseFactory) { + _promiseFactory = promiseFactory; +}; + +/** + * @param {{getJSON:function(url:string, config:{ headers: object. })}} httpRequest + */ +TokenManager.setHttpRequest = function (httpRequest) { + if ((typeof httpRequest !== 'object') || (typeof httpRequest.getJSON !== 'function')) { + throw Error('The provided value is not a valid http request.'); + } + + _httpRequest = httpRequest; +}; + +TokenManager.prototype._callTokenRemoved = function () { + this._callbacks.tokenRemovedCallbacks.forEach(function (cb) { + cb(); + }); +} + +TokenManager.prototype._callTokenExpiring = function () { + this._callbacks.tokenExpiringCallbacks.forEach(function (cb) { + cb(); + }); +} + +TokenManager.prototype._callTokenExpired = function () { + this._callbacks.tokenExpiredCallbacks.forEach(function (cb) { + cb(); + }); +} + +TokenManager.prototype._callTokenObtained = function () { + this._callbacks.tokenObtainedCallbacks.forEach(function (cb) { + cb(); + }); +} + +TokenManager.prototype._callSilentTokenRenewFailed = function () { + this._callbacks.silentTokenRenewFailedCallbacks.forEach(function (cb) { + cb(); + }); +} + +TokenManager.prototype.saveToken = function (token) { + if (token && !(token instanceof Token)) { + token = Token.fromResponse(token); + } + + this._token = token; + + if (this._settings.persist && !this.expired) { + this._settings.store.setItem(this._settings.persistKey, token.toJSON()); + } + else { + this._settings.store.removeItem(this._settings.persistKey); + } + + if (token) { + this._callTokenObtained(); + } + else { + this._callTokenRemoved(); + } +} + +TokenManager.prototype.addOnTokenRemoved = function (cb) { + this._callbacks.tokenRemovedCallbacks.push(cb); +} + +TokenManager.prototype.addOnTokenObtained = function (cb) { + this._callbacks.tokenObtainedCallbacks.push(cb); +} + +TokenManager.prototype.addOnTokenExpiring = function (cb) { + this._callbacks.tokenExpiringCallbacks.push(cb); +} + +TokenManager.prototype.addOnTokenExpired = function (cb) { + this._callbacks.tokenExpiredCallbacks.push(cb); +} + +TokenManager.prototype.addOnSilentTokenRenewFailed = function (cb) { + this._callbacks.silentTokenRenewFailedCallbacks.push(cb); +} + +TokenManager.prototype.removeToken = function () { + this.saveToken(null); +} + +TokenManager.prototype.redirectForToken = function () { + var oidc = this.oidcClient; + return oidc.createTokenRequestAsync().then(function (request) { + window.location = request.url; + }, function (err) { + console.error("TokenManager.redirectForToken error: " + (err && err.message || "Unknown error")); + return _promiseFactory.reject(err); + }); +} + +TokenManager.prototype.redirectForLogout = function () { + var mgr = this; + return mgr.oidcClient.createLogoutRequestAsync(mgr.id_token).then(function (url) { + mgr.removeToken(); + window.location = url; + }, function (err) { + console.error("TokenManager.redirectForLogout error: " + (err && err.message || "Unknown error")); + return _promiseFactory.reject(err); + }); +} + +TokenManager.prototype.processTokenCallbackAsync = function (queryString) { + var mgr = this; + return mgr.oidcClient.processResponseAsync(queryString).then(function (token) { + mgr.saveToken(token); + }); +} + +TokenManager.prototype.renewTokenSilentAsync = function () { + var mgr = this; + + if (!mgr._settings.silent_redirect_uri) { + return _promiseFactory.reject(Error("silent_redirect_uri not configured")); + } + + var settings = copy(mgr._settings); + settings.redirect_uri = settings.silent_redirect_uri; + if (!settings.prompt) { + settings.prompt = "none"; + } + + var oidc = new OidcClient(settings); + return oidc.createTokenRequestAsync().then(function (request) { + var frame = new FrameLoader(request.url, { cancelDelay: mgr._settings.silent_renew_timeout }); + return frame.loadAsync().then(function (hash) { + return oidc.processResponseAsync(hash).then(function (token) { + mgr.saveToken(token); + }); + }); + }); +} + +TokenManager.prototype.processTokenCallbackSilent = function (hash) { + if (window.parent && window !== window.parent) { + var hash = hash || window.location.hash; + if (hash) { + window.parent.postMessage(hash, location.protocol + "//" + location.host); + } + } +} + +TokenManager.prototype.openPopupForTokenAsync = function (popupSettings) { + popupSettings = popupSettings || {}; + popupSettings.features = popupSettings.features || "location=no,toolbar=no"; + popupSettings.target = popupSettings.target || "_blank"; + + var callback_prefix = "tokenmgr_callback_"; + + // this is a shared callback + if (!window.openPopupForTokenAsyncCallback) { + window.openPopupForTokenAsyncCallback = function (hash) { + var result = OidcClient.parseOidcResult(hash); + if (result && result.state && window[callback_prefix + result.state]) { + window[callback_prefix + result.state](hash); + } + } + } + + var mgr = this; + var settings = copy(mgr._settings); + settings.redirect_uri = settings.popup_redirect_uri || settings.redirect_uri; + + if (mgr._pendingPopup) { + return _promiseFactory.create(function (resolve, reject) { + reject(Error("Already a pending popup token request.")); + }); + } + + var popup = window.open(settings.redirect_uri, popupSettings.target, popupSettings.features); + if (!popup) { + return _promiseFactory.create(function (resolve, reject) { + reject(Error("Error opening popup.")); + }); + } + + mgr._pendingPopup = true; + + function cleanup(name) { + if (handle) { + window.clearInterval(handle); + } + popup.close(); + delete mgr._pendingPopup; + if (name) { + delete window[name]; + } + } + + var reject_popup; + function checkClosed() { + if (!popup.window) { + cleanup(); + reject_popup(Error("Popup closed")); + } + } + var handle = window.setInterval(checkClosed, 1000); + + return _promiseFactory.create(function (resolve, reject) { + reject_popup = reject; + + var oidc = new OidcClient(settings); + oidc.createTokenRequestAsync().then(function (request) { + + var callback_name = callback_prefix + request.request_state.state; + window[callback_name] = function (hash) { + cleanup(callback_name); + + oidc.processResponseAsync(hash).then(function (token) { + mgr.saveToken(token); + resolve(); + }, function (err) { + reject(err); + }); + }; + + // give the popup 5 seconds to ready itself, otherwise fail + var seconds_to_wait = 5; + var interval = 500; + var total_times = (seconds_to_wait*1000) / interval; + var count = 0; + function redirectPopup() { + if (popup.setUrl) { + popup.setUrl(request.url); + } + else if (count < total_times) { + count++; + window.setTimeout(redirectPopup, interval); + } + else { + cleanup(callback_name); + reject(Error("Timeout error on popup")); + } + } + redirectPopup(); + }, function (err) { + cleanup(); + reject(err); + }); + }); +} + +TokenManager.prototype.processTokenPopup = function (hash) { + hash = hash || window.location.hash; + + window.setUrl = function (url) { + window.location = url; + } + + if (hash) { + window.opener.openPopupForTokenAsyncCallback(hash); + } +} + + + // exports + window.OidcTokenManager = TokenManager; +})(); diff --git a/src/Services/Basket/Basket.API/Auth/Client/popup.html b/src/Services/Basket/Basket.API/Auth/Client/popup.html new file mode 100644 index 0000000000..364f8d7dd1 --- /dev/null +++ b/src/Services/Basket/Basket.API/Auth/Client/popup.html @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/Auth/Server/AuthorizationHeaderParameterOperationFilter.cs b/src/Services/Basket/Basket.API/Auth/Server/AuthorizationHeaderParameterOperationFilter.cs new file mode 100644 index 0000000000..2acda7be88 --- /dev/null +++ b/src/Services/Basket/Basket.API/Auth/Server/AuthorizationHeaderParameterOperationFilter.cs @@ -0,0 +1,25 @@ +namespace Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server; + +public class AuthorizationHeaderParameterOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var filterPipeline = context.ApiDescription.ActionDescriptor.FilterDescriptors; + var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter); + var allowAnonymous = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter); + + if (isAuthorized && !allowAnonymous) + { + operation.Parameters ??= new List(); + + + operation.Parameters.Add(new OpenApiParameter + { + Name = "Authorization", + In = ParameterLocation.Header, + Description = "access token", + Required = true + }); + } + } +} diff --git a/src/Services/Basket/Basket.API/Basket.API.csproj b/src/Services/Basket/Basket.API/Basket.API.csproj index 6e58267e68..1cf1b7dfcd 100644 --- a/src/Services/Basket/Basket.API/Basket.API.csproj +++ b/src/Services/Basket/Basket.API/Basket.API.csproj @@ -1,26 +1,61 @@  net7.0 - enable + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; ..\..\..\..\docker-compose.dcproj - 2964ec8e-0d48-4541-b305-94cab537f867 + false + true - - - + + PreserveNewest + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + - + + + diff --git a/src/Services/Basket/Basket.API/BasketSettings.cs b/src/Services/Basket/Basket.API/BasketSettings.cs new file mode 100644 index 0000000000..9db8831016 --- /dev/null +++ b/src/Services/Basket/Basket.API/BasketSettings.cs @@ -0,0 +1,7 @@ +namespace Microsoft.eShopOnContainers.Services.Basket.API; + +public class BasketSettings +{ + public string ConnectionString { get; set; } +} + diff --git a/src/Services/Basket/Basket.API/Controllers/BasketController.cs b/src/Services/Basket/Basket.API/Controllers/BasketController.cs index adb1f1f13c..5468bbc15b 100644 --- a/src/Services/Basket/Basket.API/Controllers/BasketController.cs +++ b/src/Services/Basket/Basket.API/Controllers/BasketController.cs @@ -23,6 +23,7 @@ public BasketController( } [HttpGet("{id}")] + [ProducesResponseType(typeof(CustomerBasket), (int)HttpStatusCode.OK)] public async Task> GetBasketByIdAsync(string id) { var basket = await _repository.GetBasketAsync(id); @@ -31,6 +32,7 @@ public async Task> GetBasketByIdAsync(string id) } [HttpPost] + [ProducesResponseType(typeof(CustomerBasket), (int)HttpStatusCode.OK)] public async Task> UpdateBasketAsync([FromBody] CustomerBasket value) { return Ok(await _repository.UpdateBasketAsync(value)); @@ -38,8 +40,8 @@ public async Task> UpdateBasketAsync([FromBody] Cus [Route("checkout")] [HttpPost] - [ProducesResponseType(StatusCodes.Status202Accepted)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType((int)HttpStatusCode.Accepted)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] public async Task CheckoutAsync([FromBody] BasketCheckout basketCheckout, [FromHeader(Name = "x-requestid")] string requestId) { var userId = _identityService.GetUserIdentity(); @@ -54,7 +56,7 @@ public async Task CheckoutAsync([FromBody] BasketCheckout basketCh return BadRequest(); } - var userName = User.FindFirst(x => x.Type == ClaimTypes.Name).Value; + var userName = this.HttpContext.User.FindFirst(x => x.Type == ClaimTypes.Name).Value; var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, userName, basketCheckout.City, basketCheckout.Street, basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName, @@ -69,7 +71,7 @@ public async Task CheckoutAsync([FromBody] BasketCheckout basketCh } catch (Exception ex) { - _logger.LogError(ex, "Error Publishing integration event: {IntegrationEventId}", eventMessage.Id); + _logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName}", eventMessage.Id, Program.AppName); throw; } @@ -79,7 +81,7 @@ public async Task CheckoutAsync([FromBody] BasketCheckout basketCh // DELETE api/values/5 [HttpDelete("{id}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), (int)HttpStatusCode.OK)] public async Task DeleteBasketByIdAsync(string id) { await _repository.DeleteBasketAsync(id); diff --git a/src/Services/Basket/Basket.API/Controllers/HomeController.cs b/src/Services/Basket/Basket.API/Controllers/HomeController.cs new file mode 100644 index 0000000000..8b2b7c2e74 --- /dev/null +++ b/src/Services/Basket/Basket.API/Controllers/HomeController.cs @@ -0,0 +1,11 @@ +namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers; + +public class HomeController : Controller +{ + // GET: // + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } +} + diff --git a/src/Services/Basket/Basket.API/CustomExtensionMethods.cs b/src/Services/Basket/Basket.API/CustomExtensionMethods.cs new file mode 100644 index 0000000000..8fcbf0c28a --- /dev/null +++ b/src/Services/Basket/Basket.API/CustomExtensionMethods.cs @@ -0,0 +1,37 @@ +namespace Microsoft.eShopOnContainers.Services.Basket.API; + +public static class CustomExtensionMethods +{ + public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); + + hcBuilder + .AddRedis( + configuration["ConnectionString"], + name: "redis-check", + tags: new string[] { "redis" }); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder + .AddAzureServiceBusTopic( + configuration["EventBusConnection"], + topicName: "eshop_event_bus", + name: "basket-servicebus-check", + tags: new string[] { "servicebus" }); + } + else + { + hcBuilder + .AddRabbitMQ( + $"amqp://{configuration["EventBusConnection"]}", + name: "basket-rabbitmqbus-check", + tags: new string[] { "rabbitmqbus" }); + } + + return services; + } +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/Dockerfile b/src/Services/Basket/Basket.API/Dockerfile index 71bfc15478..078257cc09 100644 --- a/src/Services/Basket/Basket.API/Dockerfile +++ b/src/Services/Basket/Basket.API/Dockerfile @@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" @@ -32,7 +33,6 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" -COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" @@ -42,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" COPY "docker-compose.dcproj" "docker-compose.dcproj" -COPY "Directory.Packages.props" "Directory.Packages.props" COPY "NuGet.config" "NuGet.config" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" diff --git a/src/Services/Basket/Basket.API/Extensions/Extensions.cs b/src/Services/Basket/Basket.API/Extensions/Extensions.cs deleted file mode 100644 index 7fce10b504..0000000000 --- a/src/Services/Basket/Basket.API/Extensions/Extensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -public static class Extensions -{ - public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) - { - services.AddHealthChecks() - .AddRedis(_ => configuration.GetRequiredConnectionString("redis"), "redis", tags: new[] { "ready", "liveness" }); - - return services; - } - - public static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration) - { - return services.AddSingleton(sp => - { - var redisConfig = ConfigurationOptions.Parse(configuration.GetRequiredConnectionString("redis"), true); - - return ConnectionMultiplexer.Connect(redisConfig); - }); - } -} diff --git a/src/Services/Basket/Basket.API/GlobalUsings.cs b/src/Services/Basket/Basket.API/GlobalUsings.cs index 73171c9927..b2e13ab172 100644 --- a/src/Services/Basket/Basket.API/GlobalUsings.cs +++ b/src/Services/Basket/Basket.API/GlobalUsings.cs @@ -1,19 +1,62 @@ -global using System.ComponentModel.DataAnnotations; -global using System.Security.Claims; -global using System.Text.Json; -global using Basket.API.IntegrationEvents.EventHandling; -global using Basket.API.IntegrationEvents.Events; -global using Basket.API.Model; -global using Basket.API.Repositories; -global using Grpc.Core; -global using GrpcBasket; -global using Microsoft.AspNetCore.Authorization; -global using Microsoft.AspNetCore.Mvc; -global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; -global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling; -global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; -global using Microsoft.eShopOnContainers.Services.Basket.API.Model; -global using Microsoft.eShopOnContainers.Services.Basket.API.Services; -global using Services.Common; -global using StackExchange.Redis; +global using Autofac.Extensions.DependencyInjection; +global using Autofac; +global using Azure.Core; +global using Azure.Identity; +global using Basket.API.Infrastructure.ActionResults; +global using Basket.API.Infrastructure.Exceptions; +global using Basket.API.Infrastructure.Filters; +global using Basket.API.Infrastructure.Middlewares; +global using Basket.API.IntegrationEvents.EventHandling; +global using Basket.API.IntegrationEvents.Events; +global using Basket.API.Model; +global using Grpc.Core; +global using GrpcBasket; +global using HealthChecks.UI.Client; +global using Microsoft.AspNetCore.Authentication.JwtBearer; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Diagnostics.HealthChecks; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http.Features; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc.Authorization; +global using Microsoft.AspNetCore.Mvc.Filters; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.AspNetCore.Server.Kestrel.Core; +global using Microsoft.AspNetCore; +global using Azure.Messaging.ServiceBus; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; +global using Microsoft.eShopOnContainers.Services.Basket.API.Controllers; +global using Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories; +global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling; +global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; +global using Microsoft.eShopOnContainers.Services.Basket.API.Model; +global using Microsoft.eShopOnContainers.Services.Basket.API.Services; +global using Microsoft.eShopOnContainers.Services.Basket.API; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Diagnostics.HealthChecks; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using Microsoft.OpenApi.Models; +global using RabbitMQ.Client; +global using Serilog.Context; +global using Serilog; +global using StackExchange.Redis; +global using Swashbuckle.AspNetCore.SwaggerGen; +global using System.Collections.Generic; +global using System.ComponentModel.DataAnnotations; +global using System.IdentityModel.Tokens.Jwt; +global using System.IO; +global using System.Linq; +global using System.Net; +global using System.Security.Claims; +global using System.Text.Json; +global using System.Threading.Tasks; +global using System; +global using Microsoft.IdentityModel.Tokens; \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs b/src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs new file mode 100644 index 0000000000..5f95e586ee --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs @@ -0,0 +1,11 @@ +namespace Basket.API.Infrastructure.ActionResults; + +public class InternalServerErrorObjectResult : ObjectResult +{ + public InternalServerErrorObjectResult(object error) + : base(error) + { + StatusCode = StatusCodes.Status500InternalServerError; + } +} + diff --git a/src/Services/Basket/Basket.API/Infrastructure/Exceptions/BasketDomainException.cs b/src/Services/Basket/Basket.API/Infrastructure/Exceptions/BasketDomainException.cs new file mode 100644 index 0000000000..0502b7924d --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Exceptions/BasketDomainException.cs @@ -0,0 +1,16 @@ +namespace Basket.API.Infrastructure.Exceptions; + +public class BasketDomainException : Exception +{ + public BasketDomainException() + { } + + public BasketDomainException(string message) + : base(message) + { } + + public BasketDomainException(string message, Exception innerException) + : base(message, innerException) + { } +} + diff --git a/src/Services/Basket/Basket.API/Infrastructure/Exceptions/FailingMiddlewareAppBuilderExtensions.cs b/src/Services/Basket/Basket.API/Infrastructure/Exceptions/FailingMiddlewareAppBuilderExtensions.cs new file mode 100644 index 0000000000..66f55dddd8 --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Exceptions/FailingMiddlewareAppBuilderExtensions.cs @@ -0,0 +1,18 @@ +namespace Basket.API.Infrastructure.Middlewares; + +public static class FailingMiddlewareAppBuilderExtensions +{ + public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder) + { + return UseFailingMiddleware(builder, null); + } + + public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder, Action action) + { + var options = new FailingOptions(); + action?.Invoke(options); + builder.UseMiddleware(options); + return builder; + } +} + diff --git a/src/Services/Basket/Basket.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Basket/Basket.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs new file mode 100644 index 0000000000..00b0b5195d --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -0,0 +1,47 @@ +namespace Basket.API.Infrastructure.Filters; + +public partial class HttpGlobalExceptionFilter : IExceptionFilter +{ + private readonly IWebHostEnvironment env; + private readonly ILogger logger; + + public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger logger) + { + this.env = env; + this.logger = logger; + } + + public void OnException(ExceptionContext context) + { + logger.LogError(new EventId(context.Exception.HResult), + context.Exception, + context.Exception.Message); + + if (context.Exception.GetType() == typeof(BasketDomainException)) + { + var json = new JsonErrorResponse + { + Messages = new[] { context.Exception.Message } + }; + + context.Result = new BadRequestObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + else + { + var json = new JsonErrorResponse + { + Messages = new[] { "An error occurred. Try it again." } + }; + + if (env.IsDevelopment()) + { + json.DeveloperMessage = context.Exception; + } + + context.Result = new InternalServerErrorObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + context.ExceptionHandled = true; + } +} diff --git a/src/Services/Basket/Basket.API/Infrastructure/Filters/JsonErrorResponse.cs b/src/Services/Basket/Basket.API/Infrastructure/Filters/JsonErrorResponse.cs new file mode 100644 index 0000000000..88bc02eda5 --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Filters/JsonErrorResponse.cs @@ -0,0 +1,9 @@ +namespace Basket.API.Infrastructure.Filters; + +public class JsonErrorResponse +{ + public string[] Messages { get; set; } + + public object DeveloperMessage { get; set; } +} + diff --git a/src/Services/Basket/Basket.API/Infrastructure/Filters/ValidateModelStateFilter.cs b/src/Services/Basket/Basket.API/Infrastructure/Filters/ValidateModelStateFilter.cs new file mode 100644 index 0000000000..5c97b85dcc --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Filters/ValidateModelStateFilter.cs @@ -0,0 +1,26 @@ +namespace Basket.API.Infrastructure.Filters; + +public class ValidateModelStateFilter : ActionFilterAttribute +{ + public override void OnActionExecuting(ActionExecutingContext context) + { + if (context.ModelState.IsValid) + { + return; + } + + var validationErrors = context.ModelState + .Keys + .SelectMany(k => context.ModelState[k].Errors) + .Select(e => e.ErrorMessage) + .ToArray(); + + var json = new JsonErrorResponse + { + Messages = validationErrors + }; + + context.Result = new BadRequestObjectResult(json); + } +} + diff --git a/src/Services/Basket/Basket.API/Infrastructure/Middlewares/AuthorizeCheckOperationFilter.cs b/src/Services/Basket/Basket.API/Infrastructure/Middlewares/AuthorizeCheckOperationFilter.cs new file mode 100644 index 0000000000..9608ec0a76 --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Middlewares/AuthorizeCheckOperationFilter.cs @@ -0,0 +1,29 @@ +namespace Basket.API.Infrastructure.Filters; + +public class AuthorizeCheckOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // Check for authorize attribute + var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType().Any() || + context.MethodInfo.GetCustomAttributes(true).OfType().Any(); + + if (!hasAuthorize) return; + + operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); + + var oAuthScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } + }; + + operation.Security = new List + { + new() + { + [ oAuthScheme ] = new [] { "basketapi" } + } + }; + } +} diff --git a/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingMiddleware.cs b/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingMiddleware.cs new file mode 100644 index 0000000000..60fbdb6558 --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingMiddleware.cs @@ -0,0 +1,90 @@ +namespace Basket.API.Infrastructure.Middlewares; + +using Microsoft.Extensions.Logging; + +public class FailingMiddleware +{ + private readonly RequestDelegate _next; + private bool _mustFail; + private readonly FailingOptions _options; + private readonly ILogger _logger; + + public FailingMiddleware(RequestDelegate next, ILogger logger, FailingOptions options) + { + _next = next; + _options = options; + _mustFail = false; + _logger = logger; + } + + public async Task Invoke(HttpContext context) + { + var path = context.Request.Path; + if (path.Equals(_options.ConfigPath, StringComparison.OrdinalIgnoreCase)) + { + await ProcessConfigRequest(context); + return; + } + + if (MustFail(context)) + { + _logger.LogInformation("Response for path {Path} will fail.", path); + context.Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError; + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Failed due to FailingMiddleware enabled."); + } + else + { + await _next.Invoke(context); + } + } + + private async Task ProcessConfigRequest(HttpContext context) + { + var enable = context.Request.Query.Keys.Any(k => k == "enable"); + var disable = context.Request.Query.Keys.Any(k => k == "disable"); + + if (enable && disable) + { + throw new ArgumentException("Must use enable or disable querystring values, but not both"); + } + + if (disable) + { + _mustFail = false; + await SendOkResponse(context, "FailingMiddleware disabled. Further requests will be processed."); + return; + } + if (enable) + { + _mustFail = true; + await SendOkResponse(context, "FailingMiddleware enabled. Further requests will return HTTP 500"); + return; + } + + // If reach here, that means that no valid parameter has been passed. Just output status + await SendOkResponse(context, string.Format("FailingMiddleware is {0}", _mustFail ? "enabled" : "disabled")); + return; + } + + private async Task SendOkResponse(HttpContext context, string message) + { + context.Response.StatusCode = (int)System.Net.HttpStatusCode.OK; + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync(message); + } + + private bool MustFail(HttpContext context) + { + var rpath = context.Request.Path.Value; + + if (_options.NotFilteredPaths.Any(p => p.Equals(rpath, StringComparison.InvariantCultureIgnoreCase))) + { + return false; + } + + return _mustFail && + (_options.EndpointPaths.Any(x => x == rpath) + || _options.EndpointPaths.Count == 0); + } +} diff --git a/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingOptions.cs b/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingOptions.cs new file mode 100644 index 0000000000..7818938d26 --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingOptions.cs @@ -0,0 +1,10 @@ +namespace Basket.API.Infrastructure.Middlewares; + +public class FailingOptions +{ + public string ConfigPath = "/Failing"; + public List EndpointPaths { get; set; } = new List(); + + public List NotFilteredPaths { get; set; } = new List(); +} + diff --git a/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingStartupFilter.cs b/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingStartupFilter.cs new file mode 100644 index 0000000000..74da62b5df --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingStartupFilter.cs @@ -0,0 +1,20 @@ +namespace Basket.API.Infrastructure.Middlewares; + +public class FailingStartupFilter : IStartupFilter +{ + private readonly Action _options; + public FailingStartupFilter(Action optionsAction) + { + _options = optionsAction; + } + + public Action Configure(Action next) + { + return app => + { + app.UseFailingMiddleware(_options); + next(app); + }; + } +} + diff --git a/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingWebHostBuilderExtensions.cs b/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingWebHostBuilderExtensions.cs new file mode 100644 index 0000000000..8a8ba95234 --- /dev/null +++ b/src/Services/Basket/Basket.API/Infrastructure/Middlewares/FailingWebHostBuilderExtensions.cs @@ -0,0 +1,14 @@ +namespace Basket.API.Infrastructure.Middlewares; + +public static class WebHostBuildertExtensions +{ + public static IWebHostBuilder UseFailing(this IWebHostBuilder builder, Action options) + { + builder.ConfigureServices(services => + { + services.AddSingleton(new FailingStartupFilter(options)); + }); + return builder; + } +} + diff --git a/src/Services/Basket/Basket.API/Repositories/RedisBasketRepository.cs b/src/Services/Basket/Basket.API/Infrastructure/Repositories/RedisBasketRepository.cs similarity index 75% rename from src/Services/Basket/Basket.API/Repositories/RedisBasketRepository.cs rename to src/Services/Basket/Basket.API/Infrastructure/Repositories/RedisBasketRepository.cs index c8a6e3ee33..7db463f20f 100644 --- a/src/Services/Basket/Basket.API/Repositories/RedisBasketRepository.cs +++ b/src/Services/Basket/Basket.API/Infrastructure/Repositories/RedisBasketRepository.cs @@ -1,4 +1,4 @@ -namespace Basket.API.Repositories; +namespace Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories; public class RedisBasketRepository : IBasketRepository { @@ -6,9 +6,9 @@ public class RedisBasketRepository : IBasketRepository private readonly ConnectionMultiplexer _redis; private readonly IDatabase _database; - public RedisBasketRepository(ILogger logger, ConnectionMultiplexer redis) + public RedisBasketRepository(ILoggerFactory loggerFactory, ConnectionMultiplexer redis) { - _logger = logger; + _logger = loggerFactory.CreateLogger(); _redis = redis; _database = redis.GetDatabase(); } @@ -35,12 +35,15 @@ public async Task GetBasketAsync(string customerId) return null; } - return JsonSerializer.Deserialize(data, JsonDefaults.CaseInsensitiveOptions); + return JsonSerializer.Deserialize(data, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); } public async Task UpdateBasketAsync(CustomerBasket basket) { - var created = await _database.StringSetAsync(basket.BuyerId, JsonSerializer.Serialize(basket, JsonDefaults.CaseInsensitiveOptions)); + var created = await _database.StringSetAsync(basket.BuyerId, JsonSerializer.Serialize(basket)); if (!created) { @@ -48,7 +51,7 @@ public async Task UpdateBasketAsync(CustomerBasket basket) return null; } - _logger.LogInformation("Basket item persisted successfully."); + _logger.LogInformation("Basket item persisted succesfully."); return await GetBasketAsync(basket.BuyerId); } diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs index 22b1a830bd..2c93f82fd8 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs @@ -15,9 +15,9 @@ public OrderStartedIntegrationEventHandler( public async Task Handle(OrderStartedIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); await _repository.DeleteBasketAsync(@event.UserId.ToString()); } diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs index b84d16591e..b389b73d70 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs @@ -15,9 +15,9 @@ public ProductPriceChangedIntegrationEventHandler( public async Task Handle(ProductPriceChangedIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); var userIds = _repository.GetUsers(); @@ -36,7 +36,7 @@ private async Task UpdatePriceInBasketItems(int productId, decimal newPrice, dec if (itemsToUpdate != null) { - _logger.LogInformation("ProductPriceChangedIntegrationEventHandler - Updating items in basket for user: {BuyerId} ({@Items})", basket.BuyerId, itemsToUpdate); + _logger.LogInformation("----- ProductPriceChangedIntegrationEventHandler - Updating items in basket for user: {BuyerId} ({@Items})", basket.BuyerId, itemsToUpdate); foreach (var item in itemsToUpdate) { diff --git a/src/Services/Basket/Basket.API/Program.cs b/src/Services/Basket/Basket.API/Program.cs index 28f3b1b1ea..e7208476d6 100644 --- a/src/Services/Basket/Basket.API/Program.cs +++ b/src/Services/Basket/Basket.API/Program.cs @@ -1,30 +1,101 @@ -var builder = WebApplication.CreateBuilder(args); +var configuration = GetConfiguration(); -builder.AddServiceDefaults(); +Log.Logger = CreateSerilogLogger(configuration); -builder.Services.AddGrpc(); -builder.Services.AddControllers(); -builder.Services.AddProblemDetails(); +try +{ + Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); + var host = BuildWebHost(configuration, args); -builder.Services.AddHealthChecks(builder.Configuration); -builder.Services.AddRedis(builder.Configuration); + Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); + host.Run(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); + return 0; +} +catch (Exception ex) +{ + Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} -builder.Services.AddTransient(); -builder.Services.AddTransient(); +IWebHost BuildWebHost(IConfiguration configuration, string[] args) => + WebHost.CreateDefaultBuilder(args) + .CaptureStartupErrors(false) + .ConfigureKestrel(options => + { + var ports = GetDefinedPorts(configuration); + options.Listen(IPAddress.Any, ports.httpPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); -var app = builder.Build(); + options.Listen(IPAddress.Any, ports.grpcPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); -app.UseServiceDefaults(); + }) + .ConfigureAppConfiguration(x => x.AddConfiguration(configuration)) + .UseFailing(options => + { + options.ConfigPath = "/Failing"; + options.NotFilteredPaths.AddRange(new[] { "/hc", "/liveness" }); + }) + .UseStartup() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseSerilog() + .Build(); -app.MapGrpcService(); -app.MapControllers(); +Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) +{ + var seqServerUrl = configuration["Serilog:SeqServerUrl"]; + var logstashUrl = configuration["Serilog:LogstashgUrl"]; + return new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithProperty("ApplicationContext", Program.AppName) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) + .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl, null) + .ReadFrom.Configuration(configuration) + .CreateLogger(); +} -var eventBus = app.Services.GetRequiredService(); +IConfiguration GetConfiguration() +{ + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); -eventBus.Subscribe(); -eventBus.Subscribe(); + var config = builder.Build(); -await app.RunAsync(); + if (config.GetValue("UseVault", false)) + { + TokenCredential credential = new ClientSecretCredential( + config["Vault:TenantId"], + config["Vault:ClientId"], + config["Vault:ClientSecret"]); + builder.AddAzureKeyVault(new Uri($"https://{config["Vault:Name"]}.vault.azure.net/"), credential); + } + + return builder.Build(); +} + +(int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config) +{ + var grpcPort = config.GetValue("GRPC_PORT", 5001); + var port = config.GetValue("PORT", 80); + return (port, grpcPort); +} + +public partial class Program +{ + + public static string Namespace = typeof(Startup).Namespace; + public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); +} diff --git a/src/Services/Basket/Basket.API/Properties/launchSettings.json b/src/Services/Basket/Basket.API/Properties/launchSettings.json index d54621f976..60a56b1536 100644 --- a/src/Services/Basket/Basket.API/Properties/launchSettings.json +++ b/src/Services/Basket/Basket.API/Properties/launchSettings.json @@ -1,11 +1,26 @@ { + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:58017/", + "sslPort": 0 + } + }, "profiles": { - "Basket.API": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Microsoft.eShopOnContainers.Services.Basket.API": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "http://localhost:5221", + "launchUrl": "http://localhost:55103/", "environmentVariables": { - "Identity__Url": "http://localhost:5223", "ASPNETCORE_ENVIRONMENT": "Development" } } diff --git a/src/Services/Basket/Basket.API/Properties/serviceDependencies.json b/src/Services/Basket/Basket.API/Properties/serviceDependencies.json deleted file mode 100644 index adb08c8f2e..0000000000 --- a/src/Services/Basket/Basket.API/Properties/serviceDependencies.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "dependencies": { - "secrets1": { - "type": "secrets" - }, - "rabbitmq1": { - "type": "rabbitmq", - "connectionId": "eventbus", - "dynamicId": null - }, - "redis1": { - "type": "redis", - "connectionId": "ConnectionStrings:Redis", - "dynamicId": null - } - } -} \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/Properties/serviceDependencies.local.json b/src/Services/Basket/Basket.API/Properties/serviceDependencies.local.json deleted file mode 100644 index 8fb5df48ac..0000000000 --- a/src/Services/Basket/Basket.API/Properties/serviceDependencies.local.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "dependencies": { - "secrets1": { - "type": "secrets.user" - }, - "rabbitmq1": { - "containerPorts": "5672:5672,15672:15672", - "secretStore": "LocalSecretsFile", - "containerName": "rabbitmq", - "containerImage": "rabbitmq:3-management-alpine", - "type": "rabbitmq.container", - "connectionId": "eventbus", - "dynamicId": null - }, - "redis1": { - "serviceConnectorResourceId": "", - "containerPorts": "6379:6379", - "secretStore": "LocalSecretsFile", - "containerName": "basket-redis", - "containerImage": "redis:alpine", - "type": "redis.container", - "connectionId": "ConnectionStrings:Redis", - "dynamicId": null - } - } -} \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/Startup.cs b/src/Services/Basket/Basket.API/Startup.cs new file mode 100644 index 0000000000..89b8934e9b --- /dev/null +++ b/src/Services/Basket/Basket.API/Startup.cs @@ -0,0 +1,292 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; + +namespace Microsoft.eShopOnContainers.Services.Basket.API; +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public virtual IServiceProvider ConfigureServices(IServiceCollection services) + { + services.AddGrpc(options => + { + options.EnableDetailedErrors = true; + }); + + RegisterAppInsights(services); + + services.AddControllers(options => + { + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + options.Filters.Add(typeof(ValidateModelStateFilter)); + + }) // Added for functional tests + .AddApplicationPart(typeof(BasketController).Assembly) + .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); + + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "eShopOnContainers - Basket HTTP API", + Version = "v1", + Description = "The Basket Service HTTP API" + }); + + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows() + { + Implicit = new OpenApiOAuthFlow() + { + AuthorizationUrl = new Uri($"{Configuration.GetValue("IdentityUrlExternal")}/connect/authorize"), + TokenUrl = new Uri($"{Configuration.GetValue("IdentityUrlExternal")}/connect/token"), + Scopes = new Dictionary() + { + { "basket", "Basket API" } + } + } + } + }); + + options.OperationFilter(); + }); + + ConfigureAuthService(services); + + services.AddCustomHealthCheck(Configuration); + + services.Configure(Configuration); + + //By connecting here we are making sure that our service + //cannot start until redis is ready. This might slow down startup, + //but given that there is a delay on resolving the ip address + //and then creating the connection it seems reasonable to move + //that cost to startup instead of having the first request pay the + //penalty. + services.AddSingleton(sp => + { + var settings = sp.GetRequiredService>().Value; + var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true); + + return ConnectionMultiplexer.Connect(configuration); + }); + + + if (Configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusConnectionString = Configuration["EventBusConnection"]; + + return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); + }); + } + else + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + + var factory = new ConnectionFactory() + { + HostName = Configuration["EventBusConnection"], + DispatchConsumersAsync = true + }; + + if (!string.IsNullOrEmpty(Configuration["EventBusUserName"])) + { + factory.UserName = Configuration["EventBusUserName"]; + } + + if (!string.IsNullOrEmpty(Configuration["EventBusPassword"])) + { + factory.Password = Configuration["EventBusPassword"]; + } + + var retryCount = 5; + if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(Configuration["EventBusRetryCount"]); + } + + return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); + }); + } + + RegisterEventBus(services); + + + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .SetIsOriginAllowed((host) => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + + services.AddOptions(); + + var container = new ContainerBuilder(); + container.Populate(services); + + return new AutofacServiceProvider(container.Build()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + //loggerFactory.AddAzureWebAppDiagnostics(); + //loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); + + var pathBase = Configuration["PATH_BASE"]; + if (!string.IsNullOrEmpty(pathBase)) + { + app.UsePathBase(pathBase); + } + + app.UseSwagger() + .UseSwaggerUI(setup => + { + setup.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Basket.API V1"); + setup.OAuthClientId("basketswaggerui"); + setup.OAuthAppName("Basket Swagger UI"); + }); + + app.UseRouting(); + app.UseCors("CorsPolicy"); + ConfigureAuth(app); + + app.UseStaticFiles(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + endpoints.MapDefaultControllerRoute(); + endpoints.MapControllers(); + endpoints.MapGet("/_proto/", async ctx => + { + ctx.Response.ContentType = "text/plain"; + using var fs = new FileStream(Path.Combine(env.ContentRootPath, "Proto", "basket.proto"), FileMode.Open, FileAccess.Read); + using var sr = new StreamReader(fs); + while (!sr.EndOfStream) + { + var line = await sr.ReadLineAsync(); + if (line != "/* >>" || line != "<< */") + { + await ctx.Response.WriteAsync(line); + } + } + }); + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + + ConfigureEventBus(app); + } + + private void RegisterAppInsights(IServiceCollection services) + { + services.AddApplicationInsightsTelemetry(Configuration); + services.AddApplicationInsightsKubernetesEnricher(); + } + + private void ConfigureAuthService(IServiceCollection services) + { + // prevent from mapping "sub" claim to nameidentifier. + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); + + var identityUrl = Configuration.GetValue("IdentityUrl"); + + services.AddAuthentication("Bearer").AddJwtBearer(options => + { + options.Authority = identityUrl; + options.RequireHttpsMetadata = false; + options.Audience = "basket"; + options.TokenValidationParameters.ValidateAudience = false; + }); + services.AddAuthorization(options => + { + options.AddPolicy("ApiScope", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim("scope", "basket"); + }); + }); + } + + protected virtual void ConfigureAuth(IApplicationBuilder app) + { + app.UseAuthentication(); + app.UseAuthorization(); + } + + private void RegisterEventBus(IServiceCollection services) + { + if (Configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubscriptionsManager = sp.GetRequiredService(); + string subscriptionName = Configuration["SubscriptionClientName"]; + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, + eventBusSubscriptionsManager, iLifetimeScope, subscriptionName); + }); + } + else + { + services.AddSingleton(sp => + { + var subscriptionClientName = Configuration["SubscriptionClientName"]; + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubscriptionsManager = sp.GetRequiredService(); + + var retryCount = 5; + if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(Configuration["EventBusRetryCount"]); + } + + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubscriptionsManager, subscriptionClientName, retryCount); + }); + } + + services.AddSingleton(); + + services.AddTransient(); + services.AddTransient(); + } + + private void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + + eventBus.Subscribe(); + eventBus.Subscribe(); + } +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/TestHttpResponseTrailersFeature.cs b/src/Services/Basket/Basket.API/TestHttpResponseTrailersFeature.cs new file mode 100644 index 0000000000..b1cfef87b5 --- /dev/null +++ b/src/Services/Basket/Basket.API/TestHttpResponseTrailersFeature.cs @@ -0,0 +1,6 @@ +namespace Microsoft.eShopOnContainers.Services.Basket.API; + +internal class TestHttpResponseTrailersFeature : IHttpResponseTrailersFeature +{ + public IHeaderDictionary Trailers { get; set; } +} diff --git a/src/Services/Basket/Basket.API/appsettings.Development.json b/src/Services/Basket/Basket.API/appsettings.Development.json index 4088044ae2..f4a3b9407f 100644 --- a/src/Services/Basket/Basket.API/appsettings.Development.json +++ b/src/Services/Basket/Basket.API/appsettings.Development.json @@ -1,4 +1,16 @@ { + "Serilog": { + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Warning", + "Microsoft.eShopOnContainers": "Debug", + "System": "Warning" + } + } + }, + "IdentityUrlExternal": "http://localhost:5105", + "IdentityUrl": "http://localhost:5105", "ConnectionString": "127.0.0.1", "AzureServiceBusEnabled": false, "EventBusConnection": "localhost" diff --git a/src/Services/Basket/Basket.API/appsettings.json b/src/Services/Basket/Basket.API/appsettings.json index 3a4547555f..295294308b 100644 --- a/src/Services/Basket/Basket.API/appsettings.json +++ b/src/Services/Basket/Basket.API/appsettings.json @@ -1,48 +1,30 @@ { - "Logging": { - "LogLevel": { + "Serilog": { + "SeqServerUrl": null, + "LogstashgUrl": null, + "MinimumLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "Kestrel": { - "Endpoints": { - "Http": { - "Url": "http://localhost:5221" - }, - "gRPC": { - "Url": "http://localhost:6221", - "Protocols": "Http2" + "Override": { + "Microsoft": "Warning", + "Microsoft.eShopOnContainers": "Information", + "System": "Warning" } } }, - "OpenApi": { - "Endpoint": { - "Name": "Basket.API V1" - }, - "Document": { - "Description": "The Basket Service HTTP API", - "Title": "eShopOnContainers - Basket HTTP API", - "Version": "v1" - }, - "Auth": { - "ClientId": "basketswaggerui", - "AppName": "Basket Swagger UI" + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2" } }, - "ConnectionStrings": { - "Redis": "localhost", - "EventBus": "localhost" - }, - "Identity": { - "Audience": "basket", - "Url": "http://localhost:5223", - "Scopes": { - "basket": "Basket API" - } + "SubscriptionClientName": "Basket", + "ApplicationInsights": { + "InstrumentationKey": "" }, - "EventBus": { - "SubscriptionClientName": "Basket", - "RetryCount": 5 + "EventBusRetryCount": 5, + "UseVault": false, + "Vault": { + "Name": "eshop", + "ClientId": "your-client-id", + "ClientSecret": "your-client-secret" } } diff --git a/src/Services/Basket/Basket.API/azds.yaml b/src/Services/Basket/Basket.API/azds.yaml new file mode 100644 index 0000000000..4fbbb7be4d --- /dev/null +++ b/src/Services/Basket/Basket.API/azds.yaml @@ -0,0 +1,56 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: ..\..\..\.. + dockerfile: Dockerfile +install: + chart: ../../../../k8s/helm/basket-api + set: + replicaCount: 1 + image: + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + - $(spacePrefix)eshop$(hostSuffix) + inf: + k8s: + dns: $(spacePrefix)eshop$(hostSuffix) + values: + - values.dev.yaml? + - secrets.dev.yaml? + - inf.yaml + - app.yaml +configurations: + develop: + build: + useGitIgnore: true + dockerfile: Dockerfile.develop + args: + BUILD_CONFIGURATION: ${BUILD_CONFIGURATION:-Debug} + container: + sync: + - '**/Pages/**' + - '**/Views/**' + - '**/wwwroot/**' + - '!**/*.{sln,csproj}' + command: + - dotnet + - run + - --no-restore + - --no-build + - --no-launch-profile + - -c + - ${BUILD_CONFIGURATION:-Debug} + iterate: + processesToKill: + - dotnet + - vsdbg + buildCommands: + - - dotnet + - build + - --no-restore + - -c + - ${BUILD_CONFIGURATION:-Debug} diff --git a/src/Services/Basket/Basket.API/web.config b/src/Services/Basket/Basket.API/web.config new file mode 100644 index 0000000000..a2cf1fe26c --- /dev/null +++ b/src/Services/Basket/Basket.API/web.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Services/Basket/Basket.FunctionalTests/Base/AutoAuthorizeMiddleware.cs b/src/Services/Basket/Basket.FunctionalTests/Base/AutoAuthorizeMiddleware.cs index 551e3069ee..6343dbe688 100644 --- a/src/Services/Basket/Basket.FunctionalTests/Base/AutoAuthorizeMiddleware.cs +++ b/src/Services/Basket/Basket.FunctionalTests/Base/AutoAuthorizeMiddleware.cs @@ -18,7 +18,6 @@ public async Task Invoke(HttpContext httpContext) identity.AddClaim(new Claim("sub", IDENTITY_ID)); identity.AddClaim(new Claim("unique_name", IDENTITY_ID)); identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID)); - identity.AddClaim(new Claim("scope", "basket")); httpContext.User.AddIdentity(identity); diff --git a/src/Services/Basket/Basket.FunctionalTests/Base/BasketScenarioBase.cs b/src/Services/Basket/Basket.FunctionalTests/Base/BasketScenarioBase.cs index 8fe4f2e60f..b50b58bbd9 100644 --- a/src/Services/Basket/Basket.FunctionalTests/Base/BasketScenarioBase.cs +++ b/src/Services/Basket/Basket.FunctionalTests/Base/BasketScenarioBase.cs @@ -1,7 +1,4 @@ -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.Hosting; - -namespace Basket.FunctionalTests.Base; +namespace Basket.FunctionalTests.Base; public class BasketScenarioBase { @@ -9,8 +6,18 @@ public class BasketScenarioBase public TestServer CreateServer() { - var factory = new BasketApplication(); - return factory.Server; + var path = Assembly.GetAssembly(typeof(BasketScenarioBase)) + .Location; + + var hostBuilder = new WebHostBuilder() + .UseContentRoot(Path.GetDirectoryName(path)) + .ConfigureAppConfiguration(cb => + { + cb.AddJsonFile("appsettings.json", optional: false) + .AddEnvironmentVariables(); + }).UseStartup(); + + return new TestServer(hostBuilder); } public static class Get @@ -19,11 +26,6 @@ public static string GetBasket(int id) { return $"{ApiUrlBase}/{id}"; } - - public static string GetBasketByCustomer(string customerId) - { - return $"{ApiUrlBase}/{customerId}"; - } } public static class Post @@ -31,37 +33,4 @@ public static class Post public static string Basket = $"{ApiUrlBase}/"; public static string CheckoutOrder = $"{ApiUrlBase}/checkout"; } - - private class BasketApplication : WebApplicationFactory - { - protected override IHost CreateHost(IHostBuilder builder) - { - builder.ConfigureServices(services => - { - services.AddSingleton(); - }); - - builder.ConfigureAppConfiguration(c => - { - var directory = Path.GetDirectoryName(typeof(BasketScenarioBase).Assembly.Location)!; - - c.AddJsonFile(Path.Combine(directory, "appsettings.Basket.json"), optional: false); - }); - - return base.CreateHost(builder); - } - - private class AuthStartupFilter : IStartupFilter - { - public Action Configure(Action next) - { - return app => - { - app.UseMiddleware(); - - next(app); - }; - } - } - } } diff --git a/src/Services/Basket/Basket.FunctionalTests/Base/BasketTestStartup.cs b/src/Services/Basket/Basket.FunctionalTests/Base/BasketTestStartup.cs new file mode 100644 index 0000000000..b19d825cd9 --- /dev/null +++ b/src/Services/Basket/Basket.FunctionalTests/Base/BasketTestStartup.cs @@ -0,0 +1,31 @@ + + +namespace Basket.FunctionalTests.Base +{ + class BasketTestsStartup : Startup + { + public BasketTestsStartup(IConfiguration env) : base(env) + { + } + + public override IServiceProvider ConfigureServices(IServiceCollection services) + { + // Added to avoid the Authorize data annotation in test environment. + // Property "SuppressCheckForUnhandledSecurityMetadata" in appsettings.json + services.Configure(Configuration); + return base.ConfigureServices(services); + } + + protected override void ConfigureAuth(IApplicationBuilder app) + { + if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) + { + app.UseMiddleware(); + } + else + { + base.ConfigureAuth(app); + } + } + } +} diff --git a/src/Services/Basket/Basket.FunctionalTests/Base/HttpClientExtensions.cs b/src/Services/Basket/Basket.FunctionalTests/Base/HttpClientExtensions.cs new file mode 100644 index 0000000000..45910df141 --- /dev/null +++ b/src/Services/Basket/Basket.FunctionalTests/Base/HttpClientExtensions.cs @@ -0,0 +1,13 @@ +namespace Basket.FunctionalTests.Base; + +static class HttpClientExtensions +{ + public static HttpClient CreateIdempotentClient(this TestServer server) + { + var client = server.CreateClient(); + + client.DefaultRequestHeaders.Add("x-requestid", Guid.NewGuid().ToString()); + + return client; + } +} diff --git a/src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj b/src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj index 9df4803122..002bd16c4d 100644 --- a/src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj +++ b/src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj @@ -2,28 +2,30 @@ net7.0 - enable - false false - + + + + + PreserveNewest - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers - + diff --git a/src/Services/Basket/Basket.FunctionalTests/BasketScenarios.cs b/src/Services/Basket/Basket.FunctionalTests/BasketScenarios.cs index fa9d2036eb..f727b999e0 100644 --- a/src/Services/Basket/Basket.FunctionalTests/BasketScenarios.cs +++ b/src/Services/Basket/Basket.FunctionalTests/BasketScenarios.cs @@ -1,15 +1,16 @@ namespace Basket.FunctionalTests; -public class BasketScenarios : - BasketScenarioBase +public class BasketScenarios + : BasketScenarioBase { [Fact] public async Task Post_basket_and_response_ok_status_code() { using var server = CreateServer(); var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); - var uri = "/api/v1/basket/"; - var response = await server.CreateClient().PostAsync(uri, content); + var response = await server.CreateClient() + .PostAsync(Post.Basket, content); + response.EnsureSuccessStatusCode(); } @@ -19,6 +20,7 @@ public async Task Get_basket_and_response_ok_status_code() using var server = CreateServer(); var response = await server.CreateClient() .GetAsync(Get.GetBasket(1)); + response.EnsureSuccessStatusCode(); } @@ -31,12 +33,9 @@ public async Task Send_Checkout_basket_and_response_ok_status_code() await server.CreateClient() .PostAsync(Post.Basket, contentBasket); - var contentCheckout = new StringContent(BuildCheckout(), UTF8Encoding.UTF8, "application/json") - { - Headers = { { "x-requestid", Guid.NewGuid().ToString() } } - }; + var contentCheckout = new StringContent(BuildCheckout(), UTF8Encoding.UTF8, "application/json"); - var response = await server.CreateClient() + var response = await server.CreateIdempotentClient() .PostAsync(Post.CheckoutOrder, contentCheckout); response.EnsureSuccessStatusCode(); diff --git a/src/Services/Basket/Basket.FunctionalTests/GlobalUsings.cs b/src/Services/Basket/Basket.FunctionalTests/GlobalUsings.cs index 219e4ce57c..d3657c2d3a 100644 --- a/src/Services/Basket/Basket.FunctionalTests/GlobalUsings.cs +++ b/src/Services/Basket/Basket.FunctionalTests/GlobalUsings.cs @@ -1,19 +1,23 @@ -global using System; -global using System.Collections.Generic; -global using System.IO; -global using System.Net.Http; -global using System.Security.Claims; -global using System.Text; -global using System.Text.Json; -global using System.Threading.Tasks; -global using Basket.FunctionalTests.Base; +global using Basket.FunctionalTests.Base; global using Microsoft.AspNetCore.Builder; global using Microsoft.AspNetCore.Hosting; global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Routing; global using Microsoft.AspNetCore.TestHost; +global using Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories; global using Microsoft.eShopOnContainers.Services.Basket.API.Model; +global using Microsoft.eShopOnContainers.Services.Basket.API; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; global using StackExchange.Redis; +global using System.Collections.Generic; +global using System.IO; +global using System.Net.Http; +global using System.Reflection; +global using System.Security.Claims; +global using System.Text.Json; +global using System.Text; +global using System.Threading.Tasks; +global using System; global using Xunit; diff --git a/src/Services/Basket/Basket.FunctionalTests/RedisBasketRepositoryTests.cs b/src/Services/Basket/Basket.FunctionalTests/RedisBasketRepositoryTests.cs index 639f36bb84..0a0cb11fa4 100644 --- a/src/Services/Basket/Basket.FunctionalTests/RedisBasketRepositoryTests.cs +++ b/src/Services/Basket/Basket.FunctionalTests/RedisBasketRepositoryTests.cs @@ -1,4 +1,4 @@ -using Basket.API.Repositories; + namespace Basket.FunctionalTests { @@ -9,8 +9,8 @@ public class RedisBasketRepositoryTests [Fact] public async Task UpdateBasket_return_and_add_basket() { - var server = CreateServer(); - var redis = server.Services.GetRequiredService(); + using var server = CreateServer(); + var redis = server.Host.Services.GetRequiredService(); var redisBasketRepository = BuildBasketRepository(redis); @@ -22,13 +22,16 @@ public async Task UpdateBasket_return_and_add_basket() Assert.NotNull(basket); Assert.Single(basket.Items); + + } [Fact] public async Task Delete_Basket_return_null() { - var server = CreateServer(); - var redis = server.Services.GetRequiredService(); + + using var server = CreateServer(); + var redis = server.Host.Services.GetRequiredService(); var redisBasketRepository = BuildBasketRepository(redis); @@ -49,7 +52,7 @@ public async Task Delete_Basket_return_null() RedisBasketRepository BuildBasketRepository(ConnectionMultiplexer connMux) { var loggerFactory = new LoggerFactory(); - return new RedisBasketRepository(loggerFactory.CreateLogger(), connMux); + return new RedisBasketRepository(loggerFactory, connMux); } List BuildBasketItems() diff --git a/src/Services/Basket/Basket.FunctionalTests/appsettings.Basket.json b/src/Services/Basket/Basket.FunctionalTests/appsettings.Basket.json deleted file mode 100644 index 364a58def9..0000000000 --- a/src/Services/Basket/Basket.FunctionalTests/appsettings.Basket.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "Logging": { - "Console": { - "IncludeScopes": false - }, - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - }, - "Identity": { - "ExternalUrl": "http://localhost:5105", - "Url": "http://localhost:5105" - }, - "ConnectionStrings": { - "Redis": "127.0.0.1" - }, - "EventBus": { - "ConnectionString": "localhost", - "SubscriptionClientName": "Basket" - }, - "isTest": "true", - "SuppressCheckForUnhandledSecurityMetadata": true -} diff --git a/src/Services/Basket/Basket.FunctionalTests/appsettings.json b/src/Services/Basket/Basket.FunctionalTests/appsettings.json new file mode 100644 index 0000000000..8b9ec4d3c7 --- /dev/null +++ b/src/Services/Basket/Basket.FunctionalTests/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "IdentityUrl": "http://localhost:5105", + "IdentityUrlExternal": "http://localhost:5105", + "ConnectionString": "127.0.0.1", + "isTest": "true", + "EventBusConnection": "localhost", + "SubscriptionClientName": "Basket", + "SuppressCheckForUnhandledSecurityMetadata": true +} diff --git a/src/Services/Basket/Basket.UnitTests/Application/BasketWebApiTest.cs b/src/Services/Basket/Basket.UnitTests/Application/BasketWebApiTest.cs index 7c06a6abcc..89f1c2b33b 100644 --- a/src/Services/Basket/Basket.UnitTests/Application/BasketWebApiTest.cs +++ b/src/Services/Basket/Basket.UnitTests/Application/BasketWebApiTest.cs @@ -90,7 +90,7 @@ public async Task Doing_Checkout_Without_Basket_Should_Return_Bad_Request() } [Fact] - public async Task Doing_Checkout_With_Basket_Should_Publish_UserCheckoutAccepted_Integration_Event() + public async Task Doing_Checkout_Wit_Basket_Should_Publish_UserCheckoutAccepted_Integration_Event() { var fakeCustomerId = "1"; var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); diff --git a/src/Services/Basket/Basket.UnitTests/Application/CartControllerTest.cs b/src/Services/Basket/Basket.UnitTests/Application/CartControllerTest.cs index 44d565f392..4231d6a9e2 100644 --- a/src/Services/Basket/Basket.UnitTests/Application/CartControllerTest.cs +++ b/src/Services/Basket/Basket.UnitTests/Application/CartControllerTest.cs @@ -79,7 +79,7 @@ public async Task Add_to_cart_success() //Arrange var fakeCatalogItem = GetFakeCatalogItem(); - _basketServiceMock.Setup(x => x.AddItemToBasket(It.IsAny(), It.IsAny())) + _basketServiceMock.Setup(x => x.AddItemToBasket(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(1)); //Act diff --git a/src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj b/src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj index cf88eb79b5..73a4a773f4 100644 --- a/src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj +++ b/src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj @@ -2,24 +2,22 @@ net7.0 - enable false - false false - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers - + diff --git a/src/Services/Basket/Basket.UnitTests/GlobalUsings.cs b/src/Services/Basket/Basket.UnitTests/GlobalUsings.cs index 5f73c46307..6e5443b743 100644 --- a/src/Services/Basket/Basket.UnitTests/GlobalUsings.cs +++ b/src/Services/Basket/Basket.UnitTests/GlobalUsings.cs @@ -1,18 +1,19 @@ -global using System; -global using System.Collections.Generic; -global using System.Security.Claims; -global using System.Threading.Tasks; -global using Basket.API.IntegrationEvents.Events; +global using Basket.API.IntegrationEvents.Events; global using Basket.API.Model; global using Microsoft.AspNetCore.Http; global using Microsoft.AspNetCore.Mvc; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; global using Microsoft.eShopOnContainers.Services.Basket.API.Controllers; -global using Microsoft.eShopOnContainers.WebMVC.Controllers; -global using Microsoft.eShopOnContainers.WebMVC.Services; -global using Microsoft.eShopOnContainers.WebMVC.ViewModels; +global using Microsoft.eShopOnContainers.Services.Basket.API.Model; global using Microsoft.Extensions.Logging; global using Moq; +global using System; +global using System.Collections.Generic; +global using System.Security.Claims; +global using System.Threading.Tasks; global using Xunit; global using IBasketIdentityService = Microsoft.eShopOnContainers.Services.Basket.API.Services.IIdentityService; +global using Microsoft.eShopOnContainers.WebMVC.Controllers; +global using Microsoft.eShopOnContainers.WebMVC.Services; +global using Microsoft.eShopOnContainers.WebMVC.ViewModels; global using BasketModel = Microsoft.eShopOnContainers.WebMVC.ViewModels.Basket; diff --git a/src/Services/Catalog/Catalog.API/Apis/PicApi.cs b/src/Services/Catalog/Catalog.API/Apis/PicApi.cs deleted file mode 100644 index 70442d5309..0000000000 --- a/src/Services/Catalog/Catalog.API/Apis/PicApi.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Catalog.API.Apis; - -public static class PicApi -{ - public static IEndpointConventionBuilder MapPicApi(this IEndpointRouteBuilder routes) - { - return routes.MapGet("api/v1/catalog/items/{catalogItemId:int}/pic", - async (int catalogItemId, CatalogContext db, IWebHostEnvironment environment) => - { - var item = await db.CatalogItems.FindAsync(catalogItemId); - - if (item is null) - { - return Results.NotFound(); - } - - var path = Path.Combine(environment.ContentRootPath, "Pics", item.PictureFileName); - - string imageFileExtension = Path.GetExtension(item.PictureFileName); - string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension); - - return Results.File(path, mimetype); - }) - .WithTags("Pic") - .Produces(404); - - static string GetImageMimeTypeFromImageFileExtension(string extension) => extension switch - { - ".png" => "image/png", - ".gif" => "image/gif", - ".jpg" or ".jpeg" => "image/jpeg", - ".bmp" => "image/bmp", - ".tiff" => "image/tiff", - ".wmf" => "image/wmf", - ".jp2" => "image/jp2", - ".svg" => "image/svg+xml", - _ => "application/octet-stream", - }; - } -} diff --git a/src/Services/Catalog/Catalog.API/Catalog.API.csproj b/src/Services/Catalog/Catalog.API/Catalog.API.csproj index d047f97bb5..362401a4db 100644 --- a/src/Services/Catalog/Catalog.API/Catalog.API.csproj +++ b/src/Services/Catalog/Catalog.API/Catalog.API.csproj @@ -2,16 +2,23 @@ net7.0 + portable + true Catalog.API Catalog.API aspnet-Catalog.API-20161122013618 - enable ..\..\..\..\docker-compose.dcproj + false + true + - PreserveNewest + Always + + + PreserveNewest PreserveNewest @@ -19,33 +26,71 @@ PreserveNewest - - - - - + + + + + PreserveNewest + - + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + - + + + + PreserveNewest + + + PreserveNewest + + + diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs index 0679534ae7..4dd1143f6a 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs @@ -20,9 +20,9 @@ public CatalogController(CatalogContext context, IOptionsSnapshot), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] public async Task ItemsAsync([FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0, string ids = null) { if (!string.IsNullOrEmpty(ids)) @@ -46,6 +46,15 @@ public async Task ItemsAsync([FromQuery] int pageSize = 10, [From .Take(pageSize) .ToListAsync(); + /* The "awesome" fix for testing Devspaces */ + + /* + foreach (var pr in itemsOnPage) { + pr.Name = "Awesome " + pr.Name; + } + + */ + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); var model = new PaginatedItemsViewModel(pageIndex, pageSize, totalItems, itemsOnPage); @@ -74,8 +83,9 @@ private async Task> GetItemsByIdsAsync(string ids) [HttpGet] [Route("items/{id:int}")] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType(typeof(CatalogItem), (int)HttpStatusCode.OK)] public async Task> ItemByIdAsync(int id) { if (id <= 0) @@ -101,6 +111,7 @@ public async Task> ItemByIdAsync(int id) // GET api/v1/[controller]/items/withname/samplename[?pageSize=3&pageIndex=10] [HttpGet] [Route("items/withname/{name:minlength(1)}")] + [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] public async Task>> ItemsWithNameAsync(string name, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) { var totalItems = await _catalogContext.CatalogItems @@ -121,6 +132,7 @@ public async Task>> ItemsWithN // GET api/v1/[controller]/items/type/1/brand[?pageSize=3&pageIndex=10] [HttpGet] [Route("items/type/{catalogTypeId}/brand/{catalogBrandId:int?}")] + [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] public async Task>> ItemsByTypeIdAndBrandIdAsync(int catalogTypeId, int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) { var root = (IQueryable)_catalogContext.CatalogItems; @@ -148,6 +160,7 @@ public async Task>> ItemsByTyp // GET api/v1/[controller]/items/type/all/brand[?pageSize=3&pageIndex=10] [HttpGet] [Route("items/type/all/brand/{catalogBrandId:int?}")] + [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] public async Task>> ItemsByBrandIdAsync(int? catalogBrandId, [FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0) { var root = (IQueryable)_catalogContext.CatalogItems; @@ -173,6 +186,7 @@ public async Task>> ItemsByBra // GET api/v1/[controller]/CatalogTypes [HttpGet] [Route("catalogtypes")] + [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)] public async Task>> CatalogTypesAsync() { return await _catalogContext.CatalogTypes.ToListAsync(); @@ -181,6 +195,7 @@ public async Task>> CatalogTypesAsync() // GET api/v1/[controller]/CatalogBrands [HttpGet] [Route("catalogbrands")] + [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)] public async Task>> CatalogBrandsAsync() { return await _catalogContext.CatalogBrands.ToListAsync(); @@ -189,8 +204,8 @@ public async Task>> CatalogBrandsAsync() //PUT api/v1/[controller]/items [Route("items")] [HttpPut] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.Created)] public async Task UpdateProductAsync([FromBody] CatalogItem productToUpdate) { var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); @@ -229,7 +244,7 @@ public async Task UpdateProductAsync([FromBody] CatalogItem produc //POST api/v1/[controller]/items [Route("items")] [HttpPost] - [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType((int)HttpStatusCode.Created)] public async Task CreateProductAsync([FromBody] CatalogItem product) { var item = new CatalogItem @@ -252,8 +267,8 @@ public async Task CreateProductAsync([FromBody] CatalogItem produc //DELETE api/v1/[controller]/id [Route("{id}")] [HttpDelete] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task DeleteProductAsync(int id) { var product = _catalogContext.CatalogItems.SingleOrDefault(x => x.Id == id); diff --git a/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs b/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs new file mode 100644 index 0000000000..cd86a2966b --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs @@ -0,0 +1,11 @@ +// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers; + +public class HomeController : Controller +{ + // GET: // + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } +} diff --git a/src/Services/Catalog/Catalog.API/Controllers/PicController.cs b/src/Services/Catalog/Catalog.API/Controllers/PicController.cs new file mode 100644 index 0000000000..dfb6f0d038 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Controllers/PicController.cs @@ -0,0 +1,64 @@ +// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers; + +[ApiController] +public class PicController : ControllerBase +{ + private readonly IWebHostEnvironment _env; + private readonly CatalogContext _catalogContext; + + public PicController(IWebHostEnvironment env, + CatalogContext catalogContext) + { + _env = env; + _catalogContext = catalogContext; + } + + [HttpGet] + [Route("api/v1/catalog/items/{catalogItemId:int}/pic")] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + // GET: // + public async Task GetImageAsync(int catalogItemId) + { + if (catalogItemId <= 0) + { + return BadRequest(); + } + + var item = await _catalogContext.CatalogItems + .SingleOrDefaultAsync(ci => ci.Id == catalogItemId); + + if (item != null) + { + var webRoot = _env.WebRootPath; + var path = Path.Combine(webRoot, item.PictureFileName); + + string imageFileExtension = Path.GetExtension(item.PictureFileName); + string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension); + + var buffer = await System.IO.File.ReadAllBytesAsync(path); + + return File(buffer, mimetype); + } + + return NotFound(); + } + + private string GetImageMimeTypeFromImageFileExtension(string extension) + { + string mimetype = extension switch + { + ".png" => "image/png", + ".gif" => "image/gif", + ".jpg" or ".jpeg" => "image/jpeg", + ".bmp" => "image/bmp", + ".tiff" => "image/tiff", + ".wmf" => "image/wmf", + ".jp2" => "image/jp2", + ".svg" => "image/svg+xml", + _ => "application/octet-stream", + }; + return mimetype; + } +} diff --git a/src/Services/Catalog/Catalog.API/Dockerfile b/src/Services/Catalog/Catalog.API/Dockerfile index 9138482c1d..f7ab4232ca 100644 --- a/src/Services/Catalog/Catalog.API/Dockerfile +++ b/src/Services/Catalog/Catalog.API/Dockerfile @@ -12,6 +12,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" @@ -33,7 +34,6 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" -COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" @@ -43,7 +43,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" COPY "docker-compose.dcproj" "docker-compose.dcproj" -COPY "Directory.Packages.props" "Directory.Packages.props" COPY "NuGet.config" "NuGet.config" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" diff --git a/src/Services/Catalog/Catalog.API/Extensions/Extensions.cs b/src/Services/Catalog/Catalog.API/Extensions/Extensions.cs deleted file mode 100644 index 490c5ede48..0000000000 --- a/src/Services/Catalog/Catalog.API/Extensions/Extensions.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; - -public static class Extensions -{ - public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) - { - var hcBuilder = services.AddHealthChecks(); - - hcBuilder - .AddSqlServer(_ => configuration.GetRequiredConnectionString("CatalogDB"), - name: "CatalogDB-check", - tags: new string[] { "ready" }); - - var accountName = configuration["AzureStorageAccountName"]; - var accountKey = configuration["AzureStorageAccountKey"]; - - if (!string.IsNullOrEmpty(accountName) && !string.IsNullOrEmpty(accountKey)) - { - hcBuilder - .AddAzureBlobStorage( - $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net", - name: "catalog-storage-check", - tags: new string[] { "ready" }); - } - - return services; - } - - public static IServiceCollection AddDbContexts(this IServiceCollection services, IConfiguration configuration) - { - static void ConfigureSqlOptions(SqlServerDbContextOptionsBuilder sqlOptions) - { - sqlOptions.MigrationsAssembly(typeof(Program).Assembly.FullName); - - // Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency - - sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); - }; - - services.AddDbContext(options => - { - var connectionString = configuration.GetRequiredConnectionString("CatalogDB"); - - options.UseSqlServer(connectionString, ConfigureSqlOptions); - }); - - services.AddDbContext(options => - { - var connectionString = configuration.GetRequiredConnectionString("CatalogDB"); - - options.UseSqlServer(connectionString, ConfigureSqlOptions); - }); - - return services; - } - - public static IServiceCollection AddApplicationOptions(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration); - - // TODO: Move to the new problem details middleware - services.Configure(options => - { - options.InvalidModelStateResponseFactory = context => - { - var problemDetails = new ValidationProblemDetails(context.ModelState) - { - Instance = context.HttpContext.Request.Path, - Status = StatusCodes.Status400BadRequest, - Detail = "Please refer to the errors property for additional details." - }; - - return new BadRequestObjectResult(problemDetails) - { - ContentTypes = { "application/problem+json", "application/problem+xml" } - }; - }; - }); - - return services; - } - - public static IServiceCollection AddIntegrationServices(this IServiceCollection services) - { - services.AddTransient>( - sp => (DbConnection c) => new IntegrationEventLogService(c)); - - services.AddTransient(); - - return services; - } -} diff --git a/src/Services/Catalog/Catalog.API/Extensions/LinqSelectExtensions.cs b/src/Services/Catalog/Catalog.API/Extensions/LinqSelectExtensions.cs index 65d72c1d27..85fa9300c3 100644 --- a/src/Services/Catalog/Catalog.API/Extensions/LinqSelectExtensions.cs +++ b/src/Services/Catalog/Catalog.API/Extensions/LinqSelectExtensions.cs @@ -13,7 +13,7 @@ public static IEnumerable> SelectTry(element, default, ex); + returnedValue = new SelectTryResult(element, default(TResult), ex); } yield return returnedValue; } diff --git a/src/Services/Catalog/Catalog.API/Extensions/WebHostExtensions.cs b/src/Services/Catalog/Catalog.API/Extensions/WebHostExtensions.cs new file mode 100644 index 0000000000..588ca7a354 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Extensions/WebHostExtensions.cs @@ -0,0 +1,68 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; + +public static class WebHostExtensions +{ + public static bool IsInKubernetes(this IWebHost host) + { + var cfg = host.Services.GetService(); + var orchestratorType = cfg.GetValue("OrchestratorType"); + return orchestratorType?.ToUpper() == "K8S"; + } + + public static IWebHost MigrateDbContext(this IWebHost host, Action seeder) where TContext : DbContext + { + var underK8s = host.IsInKubernetes(); + + using var scope = host.Services.CreateScope(); + var services = scope.ServiceProvider; + + var logger = services.GetRequiredService>(); + + var context = services.GetService(); + + try + { + logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); + + if (underK8s) + { + InvokeSeeder(seeder, context, services); + } + else + { + var retry = Policy.Handle() + .WaitAndRetry(new TimeSpan[] + { + TimeSpan.FromSeconds(3), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(8), + }); + + //if the sql server container is not created on run docker compose this + //migration can't fail for network related exception. The retry options for DbContext only + //apply to transient exceptions + // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) + retry.Execute(() => InvokeSeeder(seeder, context, services)); + } + + logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); + if (underK8s) + { + throw; // Rethrow under k8s because we rely on k8s to re-run the pod + } + } + + return host; + } + + private static void InvokeSeeder(Action seeder, TContext context, IServiceProvider services) + where TContext : DbContext + { + context.Database.Migrate(); + seeder(context, services); + } +} diff --git a/src/Services/Catalog/Catalog.API/GlobalUsings.cs b/src/Services/Catalog/Catalog.API/GlobalUsings.cs index 3e27424b18..48641cc801 100644 --- a/src/Services/Catalog/Catalog.API/GlobalUsings.cs +++ b/src/Services/Catalog/Catalog.API/GlobalUsings.cs @@ -1,31 +1,63 @@ -global using System.Data.Common; -global using System.Data.SqlClient; -global using System.Globalization; -global using System.IO.Compression; -global using System.Text.RegularExpressions; -global using Catalog.API.Apis; +global using Azure.Core; +global using Azure.Identity; +global using Autofac.Extensions.DependencyInjection; +global using Autofac; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.ActionResults; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Exceptions; +global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents; global using Grpc.Core; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Mvc.Filters; global using Microsoft.AspNetCore.Mvc; -global using Microsoft.EntityFrameworkCore; +global using Microsoft.AspNetCore.Server.Kestrel.Core; +global using Microsoft.AspNetCore; +global using Microsoft.Extensions.Logging; global using Microsoft.EntityFrameworkCore.Design; global using Microsoft.EntityFrameworkCore.Metadata.Builders; +global using Microsoft.EntityFrameworkCore; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; -global using Microsoft.eShopOnContainers.Services.Catalog.API; -global using Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; -global using Microsoft.eShopOnContainers.Services.Catalog.API.Grpc; +global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations; -global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Exceptions; -global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents; -global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; +global using Microsoft.eShopOnContainers.Services.Catalog.API; global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; global using Microsoft.eShopOnContainers.Services.Catalog.API.Model; global using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Grpc; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Options; -global using Polly; global using Polly.Retry; -global using Services.Common; +global using Polly; +global using Serilog.Context; +global using Serilog; +global using System.Collections.Generic; +global using System.Data.Common; +global using System.Data.SqlClient; +global using System.Globalization; +global using System.IO.Compression; +global using System.IO; +global using System.Linq; +global using System.Net; +global using System.Text.RegularExpressions; +global using System.Threading.Tasks; +global using System; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Filters; +global using HealthChecks.UI.Client; +global using Microsoft.AspNetCore.Diagnostics.HealthChecks; +global using Azure.Messaging.ServiceBus; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; +global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; +global using Microsoft.Extensions.Diagnostics.HealthChecks; +global using Microsoft.OpenApi.Models; +global using RabbitMQ.Client; +global using System.Reflection; \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/Grpc/CatalogService.cs b/src/Services/Catalog/Catalog.API/Grpc/CatalogService.cs index 61e56c3148..ebd7854623 100644 --- a/src/Services/Catalog/Catalog.API/Grpc/CatalogService.cs +++ b/src/Services/Catalog/Catalog.API/Grpc/CatalogService.cs @@ -2,13 +2,14 @@ using static CatalogApi.Catalog; namespace Microsoft.eShopOnContainers.Services.Catalog.API.Grpc; +using Microsoft.Extensions.Logging; public class CatalogService : CatalogBase { private readonly CatalogContext _catalogContext; private readonly CatalogSettings _settings; private readonly ILogger _logger; - + public CatalogService(CatalogContext dbContext, IOptions settings, ILogger logger) { _settings = settings.Value; @@ -73,6 +74,15 @@ public override async Task GetItemsByIds(CatalogItemsReq .Take(request.PageSize) .ToListAsync(); + /* The "awesome" fix for testing Devspaces */ + + /* + foreach (var pr in itemsOnPage) { + pr.Name = "Awesome " + pr.Name; + } + + */ + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); var model = this.MapToResponse(itemsOnPage, totalItems, request.PageIndex, request.PageSize); diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs b/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs new file mode 100644 index 0000000000..53af0f0b86 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs @@ -0,0 +1,10 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.ActionResults; + +public class InternalServerErrorObjectResult : ObjectResult +{ + public InternalServerErrorObjectResult(object error) + : base(error) + { + StatusCode = StatusCodes.Status500InternalServerError; + } +} diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs index 935039d91e..cf152f40fa 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs @@ -1,4 +1,6 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; +using Microsoft.eShopOnContainers.Services.Catalog.API.Model; + +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; public class CatalogContext : DbContext { diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs index 472f392c93..4e3a0f6e0c 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs @@ -1,4 +1,6 @@ -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; +using Microsoft.eShopOnContainers.Services.Catalog.API.Model; + +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; public class CatalogContextSeed { @@ -60,14 +62,14 @@ private IEnumerable GetCatalogBrandsFromFile(string contentRootPat } catch (Exception ex) { - logger.LogError(ex, "Error reading CSV headers"); + logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return GetPreconfiguredCatalogBrands(); } return File.ReadAllLines(csvFileCatalogBrands) .Skip(1) // skip header row - .SelectTry(CreateCatalogBrand) - .OnCaughtException(ex => { logger.LogError(ex, "Error creating brand while seeding database"); return null; }) + .SelectTry(x => CreateCatalogBrand(x)) + .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) .Where(x => x != null); } @@ -75,9 +77,9 @@ private CatalogBrand CreateCatalogBrand(string brand) { brand = brand.Trim('"').Trim(); - if (string.IsNullOrEmpty(brand)) + if (String.IsNullOrEmpty(brand)) { - throw new Exception("Catalog Brand Name is empty"); + throw new Exception("catalog Brand Name is empty"); } return new CatalogBrand @@ -115,14 +117,14 @@ private IEnumerable GetCatalogTypesFromFile(string contentRootPath, } catch (Exception ex) { - logger.LogError(ex, "Error reading CSV headers"); + logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return GetPreconfiguredCatalogTypes(); } return File.ReadAllLines(csvFileCatalogTypes) .Skip(1) // skip header row .SelectTry(x => CreateCatalogType(x)) - .OnCaughtException(ex => { logger.LogError(ex, "Error creating catalog type while seeding database"); return null; }) + .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) .Where(x => x != null); } @@ -130,7 +132,7 @@ private CatalogType CreateCatalogType(string type) { type = type.Trim('"').Trim(); - if (string.IsNullOrEmpty(type)) + if (String.IsNullOrEmpty(type)) { throw new Exception("catalog Type Name is empty"); } @@ -170,7 +172,7 @@ private IEnumerable GetCatalogItemsFromFile(string contentRootPath, } catch (Exception ex) { - logger.LogError(ex, "Error reading CSV headers"); + logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return GetPreconfiguredItems(); } @@ -181,11 +183,11 @@ private IEnumerable GetCatalogItemsFromFile(string contentRootPath, .Skip(1) // skip header row .Select(row => Regex.Split(row, ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)")) .SelectTry(column => CreateCatalogItem(column, csvheaders, catalogTypeIdLookup, catalogBrandIdLookup)) - .OnCaughtException(ex => { logger.LogError(ex, "Error creating catalog item while seeding database"); return null; }) + .OnCaughtException(ex => { logger.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) .Where(x => x != null); } - private CatalogItem CreateCatalogItem(string[] column, string[] headers, Dictionary catalogTypeIdLookup, Dictionary catalogBrandIdLookup) + private CatalogItem CreateCatalogItem(string[] column, string[] headers, Dictionary catalogTypeIdLookup, Dictionary catalogBrandIdLookup) { if (column.Count() != headers.Count()) { @@ -205,7 +207,7 @@ private CatalogItem CreateCatalogItem(string[] column, string[] headers, Diction } string priceString = column[Array.IndexOf(headers, "price")].Trim('"').Trim(); - if (!decimal.TryParse(priceString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out decimal price)) + if (!Decimal.TryParse(priceString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out Decimal price)) { throw new Exception($"price={priceString}is not a valid decimal number"); } @@ -224,7 +226,7 @@ private CatalogItem CreateCatalogItem(string[] column, string[] headers, Diction if (availableStockIndex != -1) { string availableStockString = column[availableStockIndex].Trim('"').Trim(); - if (!string.IsNullOrEmpty(availableStockString)) + if (!String.IsNullOrEmpty(availableStockString)) { if (int.TryParse(availableStockString, out int availableStock)) { @@ -241,7 +243,7 @@ private CatalogItem CreateCatalogItem(string[] column, string[] headers, Diction if (restockThresholdIndex != -1) { string restockThresholdString = column[restockThresholdIndex].Trim('"').Trim(); - if (!string.IsNullOrEmpty(restockThresholdString)) + if (!String.IsNullOrEmpty(restockThresholdString)) { if (int.TryParse(restockThresholdString, out int restockThreshold)) { @@ -258,7 +260,7 @@ private CatalogItem CreateCatalogItem(string[] column, string[] headers, Diction if (maxStockThresholdIndex != -1) { string maxStockThresholdString = column[maxStockThresholdIndex].Trim('"').Trim(); - if (!string.IsNullOrEmpty(maxStockThresholdString)) + if (!String.IsNullOrEmpty(maxStockThresholdString)) { if (int.TryParse(maxStockThresholdString, out int maxStockThreshold)) { @@ -275,7 +277,7 @@ private CatalogItem CreateCatalogItem(string[] column, string[] headers, Diction if (onReorderIndex != -1) { string onReorderString = column[onReorderIndex].Trim('"').Trim(); - if (!string.IsNullOrEmpty(onReorderString)) + if (!String.IsNullOrEmpty(onReorderString)) { if (bool.TryParse(onReorderString, out bool onReorder)) { @@ -316,7 +318,7 @@ private string[] GetHeaders(string csvfile, string[] requiredHeaders, string[] o if (csvheaders.Count() < requiredHeaders.Count()) { - throw new Exception($"requiredHeader count '{requiredHeaders.Count()}' is bigger then csv header count '{csvheaders.Count()}' "); + throw new Exception($"requiredHeader count '{ requiredHeaders.Count()}' is bigger then csv header count '{csvheaders.Count()}' "); } if (optionalHeaders != null) @@ -361,7 +363,7 @@ private AsyncRetryPolicy CreatePolicy(ILogger logger, string sleepDurationProvider: retry => TimeSpan.FromSeconds(5), onRetry: (exception, timeSpan, retry, ctx) => { - logger.LogWarning(exception, "[{prefix}] Error seeding database (attempt {retry} of {retries})", prefix, retry, retries); + logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", prefix, exception.GetType().Name, exception.Message, retry, retries); } ); } diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170314083211_AddEventTable.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170314083211_AddEventTable.cs index fb500fc6e0..953797a759 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170314083211_AddEventTable.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170314083211_AddEventTable.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using System; namespace Catalog.API.Infrastructure.Migrations { diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170316012921_RefactoringToIntegrationEventLog.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170316012921_RefactoringToIntegrationEventLog.cs index a86450fc8c..a6c69efa3c 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170316012921_RefactoringToIntegrationEventLog.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170316012921_RefactoringToIntegrationEventLog.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using System; namespace Catalog.API.Infrastructure.Migrations { diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170322124244_RemoveIntegrationEventLogs.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170322124244_RemoveIntegrationEventLogs.cs index aa8b0d7656..392580d00a 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170322124244_RemoveIntegrationEventLogs.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/20170322124244_RemoveIntegrationEventLogs.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using System; namespace Catalog.API.Infrastructure.Migrations { diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/CatalogContextModelSnapshot.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/CatalogContextModelSnapshot.cs index bc5fecc6ed..455e87ec9c 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/CatalogContextModelSnapshot.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogMigrations/CatalogContextModelSnapshot.cs @@ -1,5 +1,7 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; namespace Catalog.API.Infrastructure.Migrations { diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs new file mode 100644 index 0000000000..66ee35e7b7 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -0,0 +1,58 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Filters; + +public class HttpGlobalExceptionFilter : IExceptionFilter +{ + private readonly IWebHostEnvironment env; + private readonly ILogger logger; + + public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger logger) + { + this.env = env; + this.logger = logger; + } + + public void OnException(ExceptionContext context) + { + logger.LogError(new EventId(context.Exception.HResult), + context.Exception, + context.Exception.Message); + + if (context.Exception.GetType() == typeof(CatalogDomainException)) + { + var problemDetails = new ValidationProblemDetails() + { + Instance = context.HttpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." + }; + + problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() }); + + context.Result = new BadRequestObjectResult(problemDetails); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + else + { + var json = new JsonErrorResponse + { + Messages = new[] { "An error ocurred." } + }; + + if (env.IsDevelopment()) + { + json.DeveloperMessage = context.Exception; + } + + context.Result = new InternalServerErrorObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + context.ExceptionHandled = true; + } + + private class JsonErrorResponse + { + public string[] Messages { get; set; } + + public object DeveloperMessage { get; set; } + } +} diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/IntegrationEventMigrations/20170322145434_IntegrationEventInitial.cs b/src/Services/Catalog/Catalog.API/Infrastructure/IntegrationEventMigrations/20170322145434_IntegrationEventInitial.cs index 7b8e183950..5f7ea0c73d 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/IntegrationEventMigrations/20170322145434_IntegrationEventInitial.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/IntegrationEventMigrations/20170322145434_IntegrationEventInitial.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using System; namespace Catalog.API.Migrations { diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/IntegrationEventMigrations/IntegrationEventLogContextDesignTimeFactory.cs b/src/Services/Catalog/Catalog.API/Infrastructure/IntegrationEventMigrations/IntegrationEventLogContextDesignTimeFactory.cs index 3195d97144..3841e3a205 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/IntegrationEventMigrations/IntegrationEventLogContextDesignTimeFactory.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/IntegrationEventMigrations/IntegrationEventLogContextDesignTimeFactory.cs @@ -1,4 +1,8 @@ -namespace Catalog.API.Infrastructure.IntegrationEventMigrations +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; + +namespace Catalog.API.Infrastructure.IntegrationEventMigrations { public class IntegrationEventLogContextDesignTimeFactory : IDesignTimeDbContextFactory { diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs index 44c7b8c75e..282bea5b38 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs @@ -26,7 +26,7 @@ public async Task PublishThroughEventBusAsync(IntegrationEvent evt) { try { - _logger.LogInformation("Publishing integration event: {IntegrationEventId_published} - ({@IntegrationEvent})", evt.Id, evt); + _logger.LogInformation("----- Publishing integration event: {IntegrationEventId_published} from {AppName} - ({@IntegrationEvent})", evt.Id, Program.AppName, evt); await _eventLogService.MarkEventAsInProgressAsync(evt.Id); _eventBus.Publish(evt); @@ -34,14 +34,14 @@ public async Task PublishThroughEventBusAsync(IntegrationEvent evt) } catch (Exception ex) { - _logger.LogError(ex, "Error Publishing integration event: {IntegrationEventId} - ({@IntegrationEvent})", evt.Id, evt); + _logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", evt.Id, Program.AppName, evt); await _eventLogService.MarkEventAsFailedAsync(evt.Id); } } public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt) { - _logger.LogInformation("CatalogIntegrationEventService - Saving changes and integrationEvent: {IntegrationEventId}", evt.Id); + _logger.LogInformation("----- CatalogIntegrationEventService - Saving changes and integrationEvent: {IntegrationEventId}", evt.Id); //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction(): //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs index 453e975e69..35c523c39f 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs @@ -1,5 +1,5 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; - + public class OrderStatusChangedToAwaitingValidationIntegrationEventHandler : IIntegrationEventHandler { @@ -14,14 +14,14 @@ public OrderStatusChangedToAwaitingValidationIntegrationEventHandler( { _catalogContext = catalogContext; _catalogIntegrationEventService = catalogIntegrationEventService; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); } public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); var confirmedOrderStockItems = new List(); diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs index 7d3519b90e..8882e78a6e 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs @@ -11,14 +11,14 @@ public OrderStatusChangedToPaidIntegrationEventHandler( ILogger logger) { _catalogContext = catalogContext; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); } public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); //we're not blocking stock/inventory foreach (var orderStockItem in @event.OrderStockItems) diff --git a/src/Services/Catalog/Catalog.API/Program.cs b/src/Services/Catalog/Catalog.API/Program.cs index 5906ce5ac0..0e51b9a257 100644 --- a/src/Services/Catalog/Catalog.API/Program.cs +++ b/src/Services/Catalog/Catalog.API/Program.cs @@ -1,43 +1,106 @@ -var builder = WebApplication.CreateBuilder(args); +var configuration = GetConfiguration(); -builder.AddServiceDefaults(); +Log.Logger = CreateSerilogLogger(configuration); -builder.Services.AddGrpc(); -builder.Services.AddControllers(); +try +{ + Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); + var host = CreateHostBuilder(configuration, args); -// Application specific services -builder.Services.AddHealthChecks(builder.Configuration); -builder.Services.AddDbContexts(builder.Configuration); -builder.Services.AddApplicationOptions(builder.Configuration); -builder.Services.AddIntegrationServices(); + Log.Information("Applying migrations ({ApplicationContext})...", Program.AppName); + host.MigrateDbContext((context, services) => + { + var env = services.GetService(); + var settings = services.GetService>(); + var logger = services.GetService>(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); + new CatalogContextSeed().SeedAsync(context, env, settings, logger).Wait(); + }) + .MigrateDbContext((_, __) => { }); -var app = builder.Build(); + Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); + host.Run(); -app.UseServiceDefaults(); + return 0; +} +catch (Exception ex) +{ + Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} -app.MapPicApi(); -app.MapControllers(); -app.MapGrpcService(); +IWebHost CreateHostBuilder(IConfiguration configuration, string[] args) => + WebHost.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(x => x.AddConfiguration(configuration)) + .CaptureStartupErrors(false) + .ConfigureKestrel(options => + { + var ports = GetDefinedPorts(configuration); + options.Listen(IPAddress.Any, ports.httpPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + options.Listen(IPAddress.Any, ports.grpcPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); -var eventBus = app.Services.GetRequiredService(); + }) + .UseStartup() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseWebRoot("Pics") + .UseSerilog() + .Build(); -eventBus.Subscribe(); -eventBus.Subscribe(); +Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) +{ + var seqServerUrl = configuration["Serilog:SeqServerUrl"]; + var logstashUrl = configuration["Serilog:LogstashgUrl"]; + return new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithProperty("ApplicationContext", Program.AppName) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) + .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl,null) + .ReadFrom.Configuration(configuration) + .CreateLogger(); +} -// REVIEW: This is done for development ease but shouldn't be here in production -using (var scope = app.Services.CreateScope()) +(int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config) { - var context = scope.ServiceProvider.GetRequiredService(); - var settings = app.Services.GetService>(); - var logger = app.Services.GetService>(); - await context.Database.MigrateAsync(); - - await new CatalogContextSeed().SeedAsync(context, app.Environment, settings, logger); - var integrationEventLogContext = scope.ServiceProvider.GetRequiredService(); - await integrationEventLogContext.Database.MigrateAsync(); + var grpcPort = config.GetValue("GRPC_PORT", 81); + var port = config.GetValue("PORT", 80); + return (port, grpcPort); } -await app.RunAsync(); +IConfiguration GetConfiguration() +{ + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + + var config = builder.Build(); + + if (config.GetValue("UseVault", false)) + { + TokenCredential credential = new ClientSecretCredential( + config["Vault:TenantId"], + config["Vault:ClientId"], + config["Vault:ClientSecret"]); + //builder.AddAzureKeyVault(new Uri($"https://{config["Vault:Name"]}.vault.azure.net/"), credential); + } + + return builder.Build(); +} + +public partial class Program +{ + public static string Namespace = typeof(Startup).Namespace; + public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/Properties/launchSettings.json b/src/Services/Catalog/Catalog.API/Properties/launchSettings.json index 1b56eefed0..a296302694 100644 --- a/src/Services/Catalog/Catalog.API/Properties/launchSettings.json +++ b/src/Services/Catalog/Catalog.API/Properties/launchSettings.json @@ -1,12 +1,39 @@ { + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:57424/", + "sslPort": 0 + } + }, "profiles": { - "Catalog.API": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "/swagger", + "environmentVariables": { + "ConnectionString": "server=localhost,5433;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word", + "Serilog:LogstashgUrl": "http://locahost:8080", + "ASPNETCORE_ENVIRONMENT": "Development", + "EventBusConnection": "localhost", + "Serilog:SeqServerUrl": "http://locahost:5340" + } + }, + "Microsoft.eShopOnContainers.Services.Catalog.API": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "http://localhost:5222/", + "launchUrl": "http://localhost:55101/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "Azure Dev Spaces": { + "commandName": "AzureDevSpaces", + "launchBrowser": true, + "resourceGroup": "edu-devspaces3", + "aksName": "edu-devspaces3", + "subscriptionId": "e3035ac1-c06c-4daf-8939-57b3c5f1f759" } } } \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/Properties/serviceDependencies.json b/src/Services/Catalog/Catalog.API/Properties/serviceDependencies.json deleted file mode 100644 index 62b84ead24..0000000000 --- a/src/Services/Catalog/Catalog.API/Properties/serviceDependencies.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "dependencies": { - "rabbitmq1": { - "type": "rabbitmq", - "connectionId": "ConnectionStrings:EventBus", - "dynamicId": null - }, - "mssql1": { - "type": "mssql", - "connectionId": "ConnectionStrings:CatalogDB", - "dynamicId": null - } - } -} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/Properties/serviceDependencies.local.json b/src/Services/Catalog/Catalog.API/Properties/serviceDependencies.local.json deleted file mode 100644 index 9dcbd89ce6..0000000000 --- a/src/Services/Catalog/Catalog.API/Properties/serviceDependencies.local.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "dependencies": { - "rabbitmq1": { - "containerPorts": "5672:5672,15672:15672", - "secretStore": "LocalSecretsFile", - "containerName": "rabbitmq", - "containerImage": "rabbitmq:3-management-alpine", - "type": "rabbitmq.container", - "connectionId": "ConnectionStrings:EventBus", - "dynamicId": null - }, - "mssql1": { - "serviceConnectorResourceId": "", - "containerPorts": "1434:1433", - "secretStore": "LocalSecretsFile", - "containerName": "catalog-mssql", - "containerImage": "mcr.microsoft.com/mssql/server:2019-latest", - "type": "mssql.container", - "connectionId": "ConnectionStrings:CatalogDB", - "dynamicId": null - } - } -} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs new file mode 100644 index 0000000000..f7b46cb6fe --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -0,0 +1,333 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + services.AddAppInsight(Configuration) + .AddGrpc().Services + .AddCustomMVC(Configuration) + .AddCustomDbContext(Configuration) + .AddCustomOptions(Configuration) + .AddIntegrationServices(Configuration) + .AddEventBus(Configuration) + .AddSwagger(Configuration) + .AddCustomHealthCheck(Configuration); + + var container = new ContainerBuilder(); + container.Populate(services); + + return new AutofacServiceProvider(container.Build()); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + //Configure logs + + //loggerFactory.AddAzureWebAppDiagnostics(); + //loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); + + var pathBase = Configuration["PATH_BASE"]; + + if (!string.IsNullOrEmpty(pathBase)) + { + loggerFactory.CreateLogger().LogDebug("Using PATH BASE '{pathBase}'", pathBase); + app.UsePathBase(pathBase); + } + + app.UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Catalog.API V1"); + }); + + app.UseRouting(); + app.UseCors("CorsPolicy"); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + endpoints.MapControllers(); + endpoints.MapGet("/_proto/", async ctx => + { + ctx.Response.ContentType = "text/plain"; + using var fs = new FileStream(Path.Combine(env.ContentRootPath, "Proto", "catalog.proto"), FileMode.Open, FileAccess.Read); + using var sr = new StreamReader(fs); + while (!sr.EndOfStream) + { + var line = await sr.ReadLineAsync(); + if (line != "/* >>" || line != "<< */") + { + await ctx.Response.WriteAsync(line); + } + } + }); + endpoints.MapGrpcService(); + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + + ConfigureEventBus(app); + } + + protected virtual void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + eventBus.Subscribe(); + eventBus.Subscribe(); + } +} + +public static class CustomExtensionMethods +{ + public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) + { + services.AddApplicationInsightsTelemetry(configuration); + services.AddApplicationInsightsKubernetesEnricher(); + + return services; + } + + public static IServiceCollection AddCustomMVC(this IServiceCollection services, IConfiguration configuration) + { + services.AddControllers(options => + { + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + }) + .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); + + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .SetIsOriginAllowed((host) => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + + return services; + } + + public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) + { + var accountName = configuration.GetValue("AzureStorageAccountName"); + var accountKey = configuration.GetValue("AzureStorageAccountKey"); + + var hcBuilder = services.AddHealthChecks(); + + hcBuilder + .AddCheck("self", () => HealthCheckResult.Healthy()) + .AddSqlServer( + configuration["ConnectionString"], + name: "CatalogDB-check", + tags: new string[] { "catalogdb" }); + + if (!string.IsNullOrEmpty(accountName) && !string.IsNullOrEmpty(accountKey)) + { + hcBuilder + .AddAzureBlobStorage( + $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net", + name: "catalog-storage-check", + tags: new string[] { "catalogstorage" }); + } + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder + .AddAzureServiceBusTopic( + configuration["EventBusConnection"], + topicName: "eshop_event_bus", + name: "catalog-servicebus-check", + tags: new string[] { "servicebus" }); + } + else + { + hcBuilder + .AddRabbitMQ( + $"amqp://{configuration["EventBusConnection"]}", + name: "catalog-rabbitmqbus-check", + tags: new string[] { "rabbitmqbus" }); + } + + return services; + } + + public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) + { + services.AddEntityFrameworkSqlServer() + .AddDbContext(options => + { + options.UseSqlServer(configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }); + + services.AddDbContext(options => + { + options.UseSqlServer(configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }); + + return services; + } + + public static IServiceCollection AddCustomOptions(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration); + services.Configure(options => + { + options.InvalidModelStateResponseFactory = context => + { + var problemDetails = new ValidationProblemDetails(context.ModelState) + { + Instance = context.HttpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." + }; + + return new BadRequestObjectResult(problemDetails) + { + ContentTypes = { "application/problem+json", "application/problem+xml" } + }; + }; + }); + + return services; + } + + public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration) + { + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "eShopOnContainers - Catalog HTTP API", + Version = "v1", + Description = "The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample" + }); + }); + + return services; + + } + + public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddTransient>( + sp => (DbConnection c) => new IntegrationEventLogService(c)); + + services.AddTransient(); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var settings = sp.GetRequiredService>().Value; + var serviceBusConnection = settings.EventBusConnection; + + return new DefaultServiceBusPersisterConnection(serviceBusConnection); + }); + } + else + { + services.AddSingleton(sp => + { + var settings = sp.GetRequiredService>().Value; + var logger = sp.GetRequiredService>(); + + var factory = new ConnectionFactory() + { + HostName = configuration["EventBusConnection"], + DispatchConsumersAsync = true + }; + + if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) + { + factory.UserName = configuration["EventBusUserName"]; + } + + if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) + { + factory.Password = configuration["EventBusPassword"]; + } + + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); + }); + } + + return services; + } + + public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) + { + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + string subscriptionName = configuration["SubscriptionClientName"]; + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, + eventBusSubcriptionsManager, iLifetimeScope, subscriptionName); + }); + + } + else + { + services.AddSingleton(sp => + { + var subscriptionClientName = configuration["SubscriptionClientName"]; + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); + }); + } + + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + + return services; + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/appsettings.Development.json b/src/Services/Catalog/Catalog.API/appsettings.Development.json index 6e2811bf07..1d5574f634 100644 --- a/src/Services/Catalog/Catalog.API/appsettings.Development.json +++ b/src/Services/Catalog/Catalog.API/appsettings.Development.json @@ -1,6 +1,15 @@ { - "ConnectionStrings": { - "CatalogDB": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word;Encrypt=false" + "ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word", + "PicBaseUrl": "http://localhost:5101/api/v1/catalog/items/[0]/pic/", + "Serilog": { + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Warning", + "Microsoft.eShopOnContainers": "Debug", + "System": "Warning" + } + } }, - "PicBaseUrl": "http://localhost:5222/api/v1/catalog/items/[0]/pic/" + "EventBusConnection": "localhost" } \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/appsettings.json b/src/Services/Catalog/Catalog.API/appsettings.json index 3ddd8ff25c..f8342fe8d9 100644 --- a/src/Services/Catalog/Catalog.API/appsettings.json +++ b/src/Services/Catalog/Catalog.API/appsettings.json @@ -1,43 +1,30 @@ { - "Logging": { - "LogLevel": { + "UseCustomizationData": false, + "Serilog": { + "SeqServerUrl": null, + "LogstashgUrl": null, + "MinimumLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "Kestrel": { - "Endpoints": { - "Http": { - "Url": "http://localhost:5222" - }, - "gRPC": { - "Url": "http://localhost:6222", - "Protocols": "Http2" + "Override": { + "Microsoft": "Warning", + "Microsoft.eShopOnContainers": "Information", + "System": "Warning" } } }, - "OpenApi": { - "Endpoint": { - "Name": "Catalog.API V1" - }, - "Document": { - "Description": "The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample", - "Title": "eShopOnContainers - Catalog HTTP API", - "Version": "v1" - } - }, - "ConnectionStrings": { - "EventBus": "localhost" - }, - "EventBus": { - "SubscriptionClientName": "Catalog", - "RetryCount": 5 - }, + "AzureServiceBusEnabled": false, + "AzureStorageEnabled": false, + "SubscriptionClientName": "Catalog", "ApplicationInsights": { "InstrumentationKey": "" }, - "UseCustomizationData": false, - "AzureServiceBusEnabled": false, - "AzureStorageEnabled": false -} + "EventBusRetryCount": 5, + "UseVault": false, + "Vault": { + "Name": "eshop", + "ClientId": "your-client-id", + "ClientSecret": "your-client-secret" + } +} + diff --git a/src/Services/Catalog/Catalog.API/azds.yaml b/src/Services/Catalog/Catalog.API/azds.yaml new file mode 100644 index 0000000000..9f98a37935 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/azds.yaml @@ -0,0 +1,54 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: ..\..\..\.. + dockerfile: Dockerfile +install: + chart: ../../../../k8s/helm/catalog-api + set: + image: + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + - $(spacePrefix)eshop$(hostSuffix) + inf: + k8s: + dns: $(spacePrefix)eshop$(hostSuffix) + values: + - values.dev.yaml? + - secrets.dev.yaml? + - inf.yaml + - app.yaml +configurations: + develop: + build: + useGitIgnore: true + dockerfile: Dockerfile.develop + container: + syncTarget: /src + sync: + - '**/Pages/**' + - '**/Views/**' + - '**/wwwroot/**' + - '!**/*.{sln,csproj}' + command: + - dotnet + - run + - --no-restore + - --no-build + - --no-launch-profile + - -c + - ${Configuration:-Debug} + iterate: + processesToKill: + - dotnet + - vsdbg + buildCommands: + - - dotnet + - build + - --no-restore + - -c + - ${Configuration:-Debug} diff --git a/src/Services/Catalog/Catalog.API/web.config b/src/Services/Catalog/Catalog.API/web.config new file mode 100644 index 0000000000..498dea85d1 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/web.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj b/src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj index ac5bd1a76d..9597df839b 100644 --- a/src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj +++ b/src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj @@ -2,13 +2,12 @@ net7.0 - enable - false + false - + @@ -16,7 +15,7 @@ - + Always @@ -34,14 +33,14 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers - + diff --git a/src/Services/Catalog/Catalog.FunctionalTests/CatalogScenarioBase.cs b/src/Services/Catalog/Catalog.FunctionalTests/CatalogScenarioBase.cs index d49cad5f76..793d170ae7 100644 --- a/src/Services/Catalog/Catalog.FunctionalTests/CatalogScenarioBase.cs +++ b/src/Services/Catalog/Catalog.FunctionalTests/CatalogScenarioBase.cs @@ -1,28 +1,38 @@ -using Microsoft.AspNetCore.Mvc.Testing; - namespace Catalog.FunctionalTests; public class CatalogScenariosBase { - private class CatalogApplication : WebApplicationFactory + public TestServer CreateServer() { - protected override IHost CreateHost(IHostBuilder builder) - { - builder.ConfigureAppConfiguration(c => + var path = Assembly.GetAssembly(typeof(CatalogScenariosBase)) + .Location; + + var hostBuilder = new WebHostBuilder() + .UseContentRoot(Path.GetDirectoryName(path)) + .ConfigureAppConfiguration(cb => { - var directory = Path.GetDirectoryName(typeof(CatalogScenariosBase).Assembly.Location)!; + cb.AddJsonFile("appsettings.json", optional: false) + .AddEnvironmentVariables(); + }) + .UseStartup(); - c.AddJsonFile(Path.Combine(directory, "appsettings.Catalog.json"), optional: false); - }); - return base.CreateHost(builder); - } - } + var testServer = new TestServer(hostBuilder); - public TestServer CreateServer() - { - var factory = new CatalogApplication(); - return factory.Server; + testServer.Host + .MigrateDbContext((context, services) => + { + var env = services.GetService(); + var settings = services.GetService>(); + var logger = services.GetService>(); + + new CatalogContextSeed() + .SeedAsync(context, env, settings, logger) + .Wait(); + }) + .MigrateDbContext((_, __) => { }); + + return testServer; } public static class Get @@ -65,9 +75,4 @@ private static string Paginated(int pageIndex, int pageCount) return $"?pageIndex={pageIndex}&pageSize={pageCount}"; } } - - public static class Put - { - public static string UpdateCatalogProduct = "api/v1/catalog/items"; - } } diff --git a/src/Services/Catalog/Catalog.FunctionalTests/GlobalUsings.cs b/src/Services/Catalog/Catalog.FunctionalTests/GlobalUsings.cs index 58dbf6ade1..2ceffc5354 100644 --- a/src/Services/Catalog/Catalog.FunctionalTests/GlobalUsings.cs +++ b/src/Services/Catalog/Catalog.FunctionalTests/GlobalUsings.cs @@ -1,8 +1,16 @@ -global using System.IO; -global using System.Net; -global using System.Threading.Tasks; -global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Hosting; global using Microsoft.AspNetCore.TestHost; +global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; +global using Microsoft.eShopOnContainers.Services.Catalog.API; global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using System.IO; +global using System.Net; +global using System.Reflection; +global using System.Threading.Tasks; global using Xunit; diff --git a/src/Services/Catalog/Catalog.FunctionalTests/appsettings.Catalog.json b/src/Services/Catalog/Catalog.FunctionalTests/appsettings.Catalog.json deleted file mode 100644 index 6cd2cb1927..0000000000 --- a/src/Services/Catalog/Catalog.FunctionalTests/appsettings.Catalog.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ExternalCatalogBaseUrl": "http://localhost:5101", - "isTest": "true", - "PicBaseUrl": "http://localhost:5101/api/v1/catalog/items/[0]/pic/", - - "ConnectionStrings": { - "CatalogDB": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word;Encrypt=False;TrustServerCertificate=true" - }, - - "EventBus": { - "SubscriptionClientName": "Catalog" - } -} diff --git a/src/Services/Catalog/Catalog.FunctionalTests/appsettings.json b/src/Services/Catalog/Catalog.FunctionalTests/appsettings.json new file mode 100644 index 0000000000..0cd61e36b2 --- /dev/null +++ b/src/Services/Catalog/Catalog.FunctionalTests/appsettings.json @@ -0,0 +1,9 @@ +{ + "ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word", + "ExternalCatalogBaseUrl": "http://localhost:5101", + "IdentityUrl": "http://localhost:5105", + "isTest": "true", + "EventBusConnection": "localhost", + "PicBaseUrl": "http://localhost:5101/api/v1/catalog/items/[0]/pic/", + "SubscriptionClientName": "Catalog" +} diff --git a/src/Services/Catalog/Catalog.UnitTests/Application/CatalogControllerTest.cs b/src/Services/Catalog/Catalog.UnitTests/Application/CatalogControllerTest.cs index 1ea092cbb2..53efafbfde 100644 --- a/src/Services/Catalog/Catalog.UnitTests/Application/CatalogControllerTest.cs +++ b/src/Services/Catalog/Catalog.UnitTests/Application/CatalogControllerTest.cs @@ -1,13 +1,16 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.eShopOnContainers.Services.Catalog.API; using Microsoft.eShopOnContainers.Services.Catalog.API.Controllers; using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; -using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents; using Microsoft.eShopOnContainers.Services.Catalog.API.Model; using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; using Microsoft.Extensions.Options; using Moq; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using Xunit; namespace UnitTest.Catalog.Application; diff --git a/src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj b/src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj index 9b49e8fb86..b2d5f0b77a 100644 --- a/src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj +++ b/src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj @@ -2,22 +2,20 @@ net7.0 - enable false - false false - - - - - + + + + + all runtime; build; native; contentfiles; analyzers - + diff --git a/src/Services/Identity/Identity.API/AppSettings.cs b/src/Services/Identity/Identity.API/AppSettings.cs new file mode 100644 index 0000000000..1f45763fe8 --- /dev/null +++ b/src/Services/Identity/Identity.API/AppSettings.cs @@ -0,0 +1,9 @@ +namespace Microsoft.eShopOnContainers.Services.Identity.API +{ + public class AppSettings + { + public string MvcClient { get; set; } + + public bool UseCustomizationData { get; set; } + } +} diff --git a/src/Services/Identity/Identity.API/Configuration/Config.cs b/src/Services/Identity/Identity.API/Configuration/Config.cs index 066cbdf2a3..a8f3b1a402 100644 --- a/src/Services/Identity/Identity.API/Configuration/Config.cs +++ b/src/Services/Identity/Identity.API/Configuration/Config.cs @@ -1,4 +1,6 @@ -namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration +using Duende.IdentityServer.Models; + +namespace Microsoft.eShopOnContainers.Services.Identity.API.Configuration { public class Config { diff --git a/src/Services/Identity/Identity.API/Data/Migrations/20210914100206_InitialMigration.cs b/src/Services/Identity/Identity.API/Data/Migrations/20210914100206_InitialMigration.cs index c6a89ab738..e0933a52e8 100644 --- a/src/Services/Identity/Identity.API/Data/Migrations/20210914100206_InitialMigration.cs +++ b/src/Services/Identity/Identity.API/Data/Migrations/20210914100206_InitialMigration.cs @@ -1,4 +1,7 @@ -namespace Microsoft.eShopOnContainers.Services.Identity.API.Data.Migrations +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Microsoft.eShopOnContainers.Services.Identity.API.Data.Migrations { public partial class InitialMigration : Migration { diff --git a/src/Services/Identity/Identity.API/Devspaces/DevspacesRedirectUriValidator.cs b/src/Services/Identity/Identity.API/Devspaces/DevspacesRedirectUriValidator.cs new file mode 100644 index 0000000000..e366a7a653 --- /dev/null +++ b/src/Services/Identity/Identity.API/Devspaces/DevspacesRedirectUriValidator.cs @@ -0,0 +1,27 @@ +namespace Microsoft.eShopOnContainers.Services.Identity.API.Devspaces +{ + using Microsoft.Extensions.Logging; + + public class DevspacesRedirectUriValidator : IRedirectUriValidator + { + private readonly ILogger _logger; + public DevspacesRedirectUriValidator(ILogger logger) + { + _logger = logger; + } + + public Task IsPostLogoutRedirectUriValidAsync(string requestedUri, Duende.IdentityServer.Models.Client client) + { + + _logger.LogInformation("Client {ClientName} used post logout uri {RequestedUri}.", client.ClientName, requestedUri); + return Task.FromResult(true); + } + + public Task IsRedirectUriValidAsync(string requestedUri, Duende.IdentityServer.Models.Client client) + { + _logger.LogInformation("Client {ClientName} used post logout uri {RequestedUri}.", client.ClientName, requestedUri); + return Task.FromResult(true); + } + + } +} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/Devspaces/IdentityDevspacesBuilderExtensions.cs b/src/Services/Identity/Identity.API/Devspaces/IdentityDevspacesBuilderExtensions.cs new file mode 100644 index 0000000000..201c85ab2b --- /dev/null +++ b/src/Services/Identity/Identity.API/Devspaces/IdentityDevspacesBuilderExtensions.cs @@ -0,0 +1,14 @@ +namespace Microsoft.eShopOnContainers.Services.Identity.API.Devspaces +{ + static class IdentityDevspacesBuilderExtensions + { + public static IIdentityServerBuilder AddDevspacesIfNeeded(this IIdentityServerBuilder builder, bool useDevspaces) + { + if (useDevspaces) + { + builder.AddRedirectUriValidator(); + } + return builder; + } + } +} diff --git a/src/Services/Identity/Identity.API/Dockerfile b/src/Services/Identity/Identity.API/Dockerfile index 574eb277a1..aca2e1e81f 100644 --- a/src/Services/Identity/Identity.API/Dockerfile +++ b/src/Services/Identity/Identity.API/Dockerfile @@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" @@ -32,7 +33,6 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" -COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" @@ -42,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" COPY "docker-compose.dcproj" "docker-compose.dcproj" -COPY "Directory.Packages.props" "Directory.Packages.props" COPY "NuGet.config" "NuGet.config" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" diff --git a/src/Services/Identity/Identity.API/GlobalUsings.cs b/src/Services/Identity/Identity.API/GlobalUsings.cs index 439583c53b..c21ec0a02b 100644 --- a/src/Services/Identity/Identity.API/GlobalUsings.cs +++ b/src/Services/Identity/Identity.API/GlobalUsings.cs @@ -1,11 +1,10 @@ -global using System; -global using System.Collections.Generic; -global using System.ComponentModel.DataAnnotations; -global using System.IdentityModel.Tokens.Jwt; -global using System.Linq; -global using System.Security.Claims; -global using System.Text.RegularExpressions; -global using System.Threading.Tasks; + +global using System.IO.Compression; +global using Autofac.Extensions.DependencyInjection; +global using Azure.Core; +global using Azure.Identity; +global using HealthChecks.UI.Client; +global using IdentityModel; global using Duende.IdentityServer; global using Duende.IdentityServer.Configuration; global using Duende.IdentityServer.Events; @@ -14,33 +13,61 @@ global using Duende.IdentityServer.Services; global using Duende.IdentityServer.Stores; global using Duende.IdentityServer.Validation; -global using IdentityModel; global using Microsoft.AspNetCore.Authentication; global using Microsoft.AspNetCore.Authorization; global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Diagnostics.HealthChecks; global using Microsoft.AspNetCore.Hosting; -global using Microsoft.AspNetCore.Http; -global using Microsoft.AspNetCore.Identity; global using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +global using Microsoft.AspNetCore.Identity; +global using Microsoft.AspNetCore.Mvc.Rendering; global using Microsoft.AspNetCore.Mvc; global using Microsoft.AspNetCore.Mvc.Filters; -global using Microsoft.AspNetCore.Mvc.Rendering; -global using Microsoft.EntityFrameworkCore; +global using Microsoft.AspNetCore; +global using Microsoft.EntityFrameworkCore.Design; global using Microsoft.EntityFrameworkCore.Infrastructure; global using Microsoft.EntityFrameworkCore.Metadata; global using Microsoft.EntityFrameworkCore.Migrations; +global using Microsoft.EntityFrameworkCore; + global using Microsoft.eShopOnContainers.Services.Identity.API; -global using Microsoft.eShopOnContainers.Services.Identity.API.Configuration; global using Microsoft.eShopOnContainers.Services.Identity.API.Data; +global using Microsoft.eShopOnContainers.Services.Identity.API.Configuration; +global using Microsoft.eShopOnContainers.Services.Identity.API.Devspaces; +global using Microsoft.eShopOnContainers.Services.Identity.API.Models.AccountViewModels; global using Microsoft.eShopOnContainers.Services.Identity.API.Models; global using Microsoft.eShopOnContainers.Services.Identity.API.Services; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Diagnostics.HealthChecks; global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; global using Polly; -global using Services.Common; +global using StackExchange.Redis; +global using System.Collections.Generic; +global using System.ComponentModel.DataAnnotations; +global using System.Data.SqlClient; +global using System.IdentityModel.Tokens.Jwt; +global using System.IO; +global using System.Linq; +global using System.Reflection; +global using System.Security.Claims; +global using System.Security.Cryptography.X509Certificates; +global using System.Text.RegularExpressions; +global using System.Threading.Tasks; +global using System; +global using Microsoft.AspNetCore.Http; + + + + + + + + + + diff --git a/src/Services/Identity/Identity.API/IWebHostExtensions.cs b/src/Services/Identity/Identity.API/IWebHostExtensions.cs new file mode 100644 index 0000000000..57c3865597 --- /dev/null +++ b/src/Services/Identity/Identity.API/IWebHostExtensions.cs @@ -0,0 +1,69 @@ +namespace Microsoft.AspNetCore.Hosting +{ + public static class IWebHostExtensions + { + public static bool IsInKubernetes(this IWebHost webHost) + { + var cfg = webHost.Services.GetService(); + var orchestratorType = cfg.GetValue("OrchestratorType"); + return orchestratorType?.ToUpper() == "K8S"; + } + + public static IWebHost MigrateDbContext(this IWebHost webHost, Action seeder) where TContext : DbContext + { + var underK8s = webHost.IsInKubernetes(); + + using var scope = webHost.Services.CreateScope(); + var services = scope.ServiceProvider; + var logger = services.GetRequiredService>(); + var context = services.GetService(); + + try + { + logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); + + if (underK8s) + { + InvokeSeeder(seeder, context, services); + } + else + { + var retries = 10; + var retry = Policy.Handle() + .WaitAndRetry( + retryCount: retries, + sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + onRetry: (exception, timeSpan, retry, ctx) => + { + logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries); + }); + + //if the sql server container is not created on run docker compose this + //migration can't fail for network related exception. The retry options for DbContext only + //apply to transient exceptions + // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) + retry.Execute(() => InvokeSeeder(seeder, context, services)); + } + + logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); + if (underK8s) + { + throw; // Rethrow under k8s because we rely on k8s to re-run the pod + } + } + + return webHost; + } + + private static void InvokeSeeder(Action seeder, TContext context, IServiceProvider services) + where TContext : DbContext + { + context.Database.Migrate(); + seeder(context, services); + } + } +} diff --git a/src/Services/Identity/Identity.API/Identity.API.csproj b/src/Services/Identity/Identity.API/Identity.API.csproj index e669070246..54c6f2a0e8 100644 --- a/src/Services/Identity/Identity.API/Identity.API.csproj +++ b/src/Services/Identity/Identity.API/Identity.API.csproj @@ -3,63 +3,78 @@ net7.0 aspnet-eShopOnContainers.Identity-90487118-103c-4ff0-b9da-e5e26f7ab0c5 - enable ..\..\..\..\docker-compose.dcproj + false + true - - - - - - - - + + + + + + + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - PreserveNewest - true - PreserveNewest - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Services/Identity/Identity.API/Models/ConsentViewModels/ConsentInputModel.cs b/src/Services/Identity/Identity.API/Models/ConsentViewModels/ConsentInputModel.cs index 6b0c22d8f3..38592c36f9 100644 --- a/src/Services/Identity/Identity.API/Models/ConsentViewModels/ConsentInputModel.cs +++ b/src/Services/Identity/Identity.API/Models/ConsentViewModels/ConsentInputModel.cs @@ -1,4 +1,6 @@ -namespace Microsoft.eShopOnContainers.Services.Identity.API.Models.ConsentViewModels +using System.Collections.Generic; + +namespace Microsoft.eShopOnContainers.Services.Identity.API.Models.ConsentViewModels { public class ConsentInputModel { diff --git a/src/Services/Identity/Identity.API/Models/ConsentViewModels/ConsentViewModel.cs b/src/Services/Identity/Identity.API/Models/ConsentViewModels/ConsentViewModel.cs index 855e9a2021..8f3b7594c0 100644 --- a/src/Services/Identity/Identity.API/Models/ConsentViewModels/ConsentViewModel.cs +++ b/src/Services/Identity/Identity.API/Models/ConsentViewModels/ConsentViewModel.cs @@ -1,4 +1,6 @@ -namespace Microsoft.eShopOnContainers.Services.Identity.API.Models.ConsentViewModels +using System.Collections.Generic; + +namespace Microsoft.eShopOnContainers.Services.Identity.API.Models.ConsentViewModels { public class ConsentViewModel : ConsentInputModel { diff --git a/src/Services/Identity/Identity.API/Models/ConsentViewModels/ProcessConsentResult.cs b/src/Services/Identity/Identity.API/Models/ConsentViewModels/ProcessConsentResult.cs index 88ef60e1a7..cecfc68987 100644 --- a/src/Services/Identity/Identity.API/Models/ConsentViewModels/ProcessConsentResult.cs +++ b/src/Services/Identity/Identity.API/Models/ConsentViewModels/ProcessConsentResult.cs @@ -1,4 +1,6 @@ -namespace Microsoft.eShopOnContainers.Services.Identity.API.Models.ConsentViewModels +using Duende.IdentityServer.Models; + +namespace Microsoft.eShopOnContainers.Services.Identity.API.Models.ConsentViewModels { public class ProcessConsentResult { diff --git a/src/Services/Identity/Identity.API/Program.cs b/src/Services/Identity/Identity.API/Program.cs index aa3d03a84d..5a2fd39762 100644 --- a/src/Services/Identity/Identity.API/Program.cs +++ b/src/Services/Identity/Identity.API/Program.cs @@ -1,63 +1,94 @@ -var builder = WebApplication.CreateBuilder(args); +var appName = "Identity.API"; +var builder = WebApplication.CreateBuilder(); -builder.AddServiceDefaults(); - -builder.Services.AddControllersWithViews(); - -builder.Services.AddDbContext(options => - options.UseSqlServer(builder.Configuration.GetConnectionString("IdentityDB"))); - -builder.Services.AddIdentity() - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); - -builder.Services.AddIdentityServer(options => -{ - options.IssuerUri = "null"; - options.Authentication.CookieLifetime = TimeSpan.FromHours(2); - - options.Events.RaiseErrorEvents = true; - options.Events.RaiseInformationEvents = true; - options.Events.RaiseFailureEvents = true; - options.Events.RaiseSuccessEvents = true; -}) -.AddInMemoryIdentityResources(Config.GetResources()) -.AddInMemoryApiScopes(Config.GetApiScopes()) -.AddInMemoryApiResources(Config.GetApis()) -.AddInMemoryClients(Config.GetClients(builder.Configuration)) -.AddAspNetIdentity() -.AddDeveloperSigningCredential(); // Not recommended for production - you need to store your key material somewhere secure - -builder.Services.AddHealthChecks() - .AddSqlServer(_ => - builder.Configuration.GetRequiredConnectionString("IdentityDB"), - name: "IdentityDB-check", - tags: new string[] { "IdentityDB" }); - -builder.Services.AddTransient(); -builder.Services.AddTransient, EFLoginService>(); -builder.Services.AddTransient(); +builder.AddCustomConfiguration(); +builder.AddCustomSerilog(); +builder.AddCustomMvc(); +builder.AddCustomDatabase(); +builder.AddCustomIdentity(); +builder.AddCustomIdentityServer(); +builder.AddCustomAuthentication(); +builder.AddCustomHealthChecks(); +builder.AddCustomApplicationServices(); var app = builder.Build(); +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} -app.UseServiceDefaults(); - +var pathBase = builder.Configuration["PATH_BASE"]; +if (!string.IsNullOrEmpty(pathBase)) +{ + app.UsePathBase(pathBase); +} app.UseStaticFiles(); // This cookie policy fixes login issues with Chrome 80+ using HHTP app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax }); + app.UseRouting(); + app.UseIdentityServer(); + + app.UseAuthorization(); app.MapDefaultControllerRoute(); -// Apply database migration automatically. Note that this approach is not -// recommended for production scenarios. Consider generating SQL scripts from -// migrations instead. -using (var scope = app.Services.CreateScope()) +app.MapHealthChecks("/hc", new HealthCheckOptions() +{ + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse +}); +app.MapHealthChecks("/liveness", new HealthCheckOptions +{ + Predicate = r => r.Name.Contains("self") +}); +try { - await SeedData.EnsureSeedData(scope, app.Configuration, app.Logger); + app.Logger.LogInformation("Seeding database ({ApplicationName})...", appName); + + // Apply database migration automatically. Note that this approach is not + // recommended for production scenarios. Consider generating SQL scripts from + // migrations instead. + using (var scope = app.Services.CreateScope()) + { + await SeedData.EnsureSeedData(scope, app.Configuration, app.Logger); + } + + app.Logger.LogInformation("Starting web host ({ApplicationName})...", appName); + app.Run(); + + return 0; } +catch (Exception ex) +{ + app.Logger.LogCritical(ex, "Host terminated unexpectedly ({ApplicationName})...", appName); + return 1; +} +finally +{ + Serilog.Log.CloseAndFlush(); +} + +IConfiguration GetConfiguration() +{ + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + + var config = builder.Build(); + + if (config.GetValue("UseVault", false)) + { + TokenCredential credential = new ClientSecretCredential( + config["Vault:TenantId"], + config["Vault:ClientId"], + config["Vault:ClientSecret"]); + builder.AddAzureKeyVault(new Uri($"https://{config["Vault:Name"]}.vault.azure.net/"), credential); + } -await app.RunAsync(); + return builder.Build(); +} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/ProgramExtensions.cs b/src/Services/Identity/Identity.API/ProgramExtensions.cs new file mode 100644 index 0000000000..7292955700 --- /dev/null +++ b/src/Services/Identity/Identity.API/ProgramExtensions.cs @@ -0,0 +1,117 @@ +using Serilog; + +namespace Microsoft.eShopOnContainers.Services.Identity.API; + +public static class ProgramExtensions +{ + private const string AppName = "Identity API"; + + public static void AddCustomConfiguration(this WebApplicationBuilder builder) + { + builder.Configuration.AddConfiguration(GetConfiguration()).Build(); + + } + + public static void AddCustomSerilog(this WebApplicationBuilder builder) + { + var seqServerUrl = builder.Configuration["SeqServerUrl"]; + var logstashUrl = builder.Configuration["LogstashgUrl"]; + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithProperty("ApplicationContext", AppName) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) + .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://localhost:8080" : logstashUrl, null) + .ReadFrom.Configuration(builder.Configuration) + .CreateLogger(); + + builder.Host.UseSerilog(); + } + + public static void AddCustomMvc(this WebApplicationBuilder builder) + { + builder.Services.AddControllersWithViews(); + builder.Services.AddControllers(); + builder.Services.AddRazorPages(); + + } + + + public static void AddCustomDatabase(this WebApplicationBuilder builder) => + builder.Services.AddDbContext( + options => options.UseSqlServer(builder.Configuration["ConnectionString"])); + + public static void AddCustomIdentity(this WebApplicationBuilder builder) + { + builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + } + + + public static void AddCustomIdentityServer(this WebApplicationBuilder builder) + { + var identityServerBuilder = builder.Services.AddIdentityServer(options => + { + options.IssuerUri = "null"; + options.Authentication.CookieLifetime = TimeSpan.FromHours(2); + + options.Events.RaiseErrorEvents = true; + options.Events.RaiseInformationEvents = true; + options.Events.RaiseFailureEvents = true; + options.Events.RaiseSuccessEvents = true; + }) + .AddInMemoryIdentityResources(Config.GetResources()) + .AddInMemoryApiScopes(Config.GetApiScopes()) + .AddInMemoryApiResources(Config.GetApis()) + .AddInMemoryClients(Config.GetClients(builder.Configuration)) + .AddAspNetIdentity(); + + // not recommended for production - you need to store your key material somewhere secure + identityServerBuilder.AddDeveloperSigningCredential(); + } + + public static void AddCustomAuthentication(this WebApplicationBuilder builder) + { + builder.Services.AddAuthentication(); + } + + public static void AddCustomHealthChecks(this WebApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy()) + .AddSqlServer(builder.Configuration["ConnectionString"], + name: "IdentityDB-check", + tags: new string[] { "IdentityDB" }); + } + + public static void AddCustomApplicationServices(this WebApplicationBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient, EFLoginService>(); + builder.Services.AddTransient(); + } + + static IConfiguration GetConfiguration() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + + var config = builder.Build(); + + if (config.GetValue("UseVault", false)) + { + TokenCredential credential = new ClientSecretCredential( + config["Vault:TenantId"], + config["Vault:ClientId"], + config["Vault:ClientSecret"]); + builder.AddAzureKeyVault(new Uri($"https://{config["Vault:Name"]}.vault.azure.net/"), credential); + } + + return builder.Build(); + } +} diff --git a/src/Services/Identity/Identity.API/Properties/launchSettings.json b/src/Services/Identity/Identity.API/Properties/launchSettings.json index 485a5f3130..e52e9f99cb 100644 --- a/src/Services/Identity/Identity.API/Properties/launchSettings.json +++ b/src/Services/Identity/Identity.API/Properties/launchSettings.json @@ -1,9 +1,25 @@ { + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:54010/", + "sslPort": 0 + } + }, "profiles": { - "Identity.API": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "http://localhost:55105", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "eShopOnContainers.Identity": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "http://localhost:5223", + "launchUrl": "http://localhost:55105", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Services/Identity/Identity.API/Properties/serviceDependencies.json b/src/Services/Identity/Identity.API/Properties/serviceDependencies.json deleted file mode 100644 index 718987b23e..0000000000 --- a/src/Services/Identity/Identity.API/Properties/serviceDependencies.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "mssql1": { - "type": "mssql", - "connectionId": "ConnectionString", - "dynamicId": null - } - } -} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/Properties/serviceDependencies.local.json b/src/Services/Identity/Identity.API/Properties/serviceDependencies.local.json deleted file mode 100644 index cae5e39316..0000000000 --- a/src/Services/Identity/Identity.API/Properties/serviceDependencies.local.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "dependencies": { - "mssql1": { - "serviceConnectorResourceId": "", - "containerPorts": "1433:1433", - "secretStore": "LocalSecretsFile", - "containerName": "identity-sql", - "containerImage": "mcr.microsoft.com/mssql/server:2019-latest", - "type": "mssql.container", - "connectionId": "ConnectionString", - "dynamicId": null - } - } -} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/Quickstart/Account/AccountController.cs b/src/Services/Identity/Identity.API/Quickstart/Account/AccountController.cs index e0aef7b955..f14c998967 100644 --- a/src/Services/Identity/Identity.API/Quickstart/Account/AccountController.cs +++ b/src/Services/Identity/Identity.API/Quickstart/Account/AccountController.cs @@ -1,6 +1,13 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +using Duende.IdentityServer.Events; +using Duende.IdentityServer.Extensions; +using Duende.IdentityServer.Stores; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + namespace IdentityServerHost.Quickstart.UI { [SecurityHeaders] @@ -127,7 +134,7 @@ public async Task Login(LoginInputModel model, string button) } } - await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId: context?.Client.ClientId)); + await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.Client.ClientId)); ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage); } @@ -139,7 +146,7 @@ public async Task Login(LoginInputModel model, string button) return View(vm); } - + /// /// Show logout page /// diff --git a/src/Services/Identity/Identity.API/Quickstart/Account/LoginViewModel.cs b/src/Services/Identity/Identity.API/Quickstart/Account/LoginViewModel.cs index f539c70e99..753f4773e3 100644 --- a/src/Services/Identity/Identity.API/Quickstart/Account/LoginViewModel.cs +++ b/src/Services/Identity/Identity.API/Quickstart/Account/LoginViewModel.cs @@ -10,7 +10,7 @@ public class LoginViewModel : LoginInputModel public bool EnableLocalLogin { get; set; } = true; public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); - public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !string.IsNullOrWhiteSpace(x.DisplayName)); + public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; diff --git a/src/Services/Identity/Identity.API/Quickstart/Consent/ConsentController.cs b/src/Services/Identity/Identity.API/Quickstart/Consent/ConsentController.cs index 2cdf323ec4..1255df44ac 100644 --- a/src/Services/Identity/Identity.API/Quickstart/Consent/ConsentController.cs +++ b/src/Services/Identity/Identity.API/Quickstart/Consent/ConsentController.cs @@ -217,7 +217,7 @@ private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool chec public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check) { var displayName = apiScope.DisplayName ?? apiScope.Name; - if (!string.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter)) + if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter)) { displayName += ":" + parsedScopeValue.ParsedParameter; } diff --git a/src/Services/Identity/Identity.API/Quickstart/Home/HomeController.cs b/src/Services/Identity/Identity.API/Quickstart/Home/HomeController.cs index 27f4d73cc9..6110df1f81 100644 --- a/src/Services/Identity/Identity.API/Quickstart/Home/HomeController.cs +++ b/src/Services/Identity/Identity.API/Quickstart/Home/HomeController.cs @@ -1,6 +1,10 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + namespace IdentityServerHost.Quickstart.UI { [SecurityHeaders] diff --git a/src/Services/Identity/Identity.API/SeedData.cs b/src/Services/Identity/Identity.API/SeedData.cs index ad2c37bf35..ece8c6d54b 100644 --- a/src/Services/Identity/Identity.API/SeedData.cs +++ b/src/Services/Identity/Identity.API/SeedData.cs @@ -1,8 +1,11 @@ -namespace Microsoft.eShopOnContainers.Services.Identity.API; +using System.Threading.Tasks; +using System; + +namespace Microsoft.eShopOnContainers.Services.Identity.API; public class SeedData { - public static async Task EnsureSeedData(IServiceScope scope, IConfiguration configuration, ILogger logger) + public static async Task EnsureSeedData(IServiceScope scope, IConfiguration configuration, Microsoft.Extensions.Logging.ILogger logger) { var retryPolicy = CreateRetryPolicy(configuration, logger); var context = scope.ServiceProvider.GetRequiredService(); @@ -92,7 +95,7 @@ await retryPolicy.ExecuteAsync(async () => }); } - private static AsyncPolicy CreateRetryPolicy(IConfiguration configuration, ILogger logger) + private static AsyncPolicy CreateRetryPolicy(IConfiguration configuration, Microsoft.Extensions.Logging.ILogger logger) { var retryMigrations = false; bool.TryParse(configuration["RetryMigrations"], out retryMigrations); @@ -104,7 +107,16 @@ private static AsyncPolicy CreateRetryPolicy(IConfiguration configuration, ILogg return Policy.Handle(). WaitAndRetryForeverAsync( sleepDurationProvider: retry => TimeSpan.FromSeconds(5), - onRetry: (exception, retry, timeSpan) => logger.LogWarning(exception, "Error migrating database (retry attempt {retry})", retry)); + onRetry: (exception, retry, timeSpan) => + { + logger.LogWarning( + exception, + "Exception {ExceptionType} with message {Message} detected during database migration (retry attempt {retry})", + exception.GetType().Name, + exception.Message, + retry); + } + ); } return Policy.NoOpAsync(); diff --git a/src/Services/Identity/Identity.API/appsettings.Development.json b/src/Services/Identity/Identity.API/appsettings.Development.json deleted file mode 100644 index 7b119bdf19..0000000000 --- a/src/Services/Identity/Identity.API/appsettings.Development.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ConnectionStrings": { - "IdentityDB": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.IdentityDb;User Id=sa;Password=Pass@word;Encrypt=false" - } -} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/appsettings.json b/src/Services/Identity/Identity.API/appsettings.json index cc633ba9e3..087e17fe73 100644 --- a/src/Services/Identity/Identity.API/appsettings.json +++ b/src/Services/Identity/Identity.API/appsettings.json @@ -1,22 +1,31 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, + "ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.IdentityDb;User Id=sa;Password=Pass@word;Encrypt=False;TrustServerCertificate=true;", "IsClusterEnv": "False", + "MvcClient": "http://localhost:5100", + "SpaClient": "http://localhost:5104", "XamarinCallback": "http://localhost:5105/xamarincallback", "UseCustomizationData": false, + "Serilog": { + "SeqServerUrl": null, + "LogstashgUrl": null, + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.eShopOnContainers": "Information", + "System": "Warning" + } + } + }, "ApplicationInsights": { "InstrumentationKey": "" }, + "UseVault": false, + "Vault": { + "Name": "eshop", + "ClientId": "your-client-id", + "ClientSecret": "your-client-secret" + }, "TokenLifetimeMinutes": 120, - "PermanentTokenLifetimeDays": 365, - "BasketApiClient": "http://localhost:5221", - "OrderingApiClient": "http://localhost:5224", - "WebShoppingAggClient": "http://localhost:5229", - "WebhooksApiClient": "http://localhost:5227", - "MvcClient": "http://localhost:5331", - "SpaClient": "http://localhost:5104" + "PermanentTokenLifetimeDays": 365 } diff --git a/src/Services/Identity/Identity.API/azds.yaml b/src/Services/Identity/Identity.API/azds.yaml new file mode 100644 index 0000000000..0c47aab13d --- /dev/null +++ b/src/Services/Identity/Identity.API/azds.yaml @@ -0,0 +1,56 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: ..\..\..\.. + dockerfile: Dockerfile +install: + chart: ../../../../k8s/helm/identity-api + set: + replicaCount: 1 + image: + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + - $(spacePrefix)eshop$(hostSuffix) + inf: + k8s: + dns: $(spacePrefix)eshop$(hostSuffix) + values: + - values.dev.yaml? + - secrets.dev.yaml? + - inf.yaml + - app.yaml +configurations: + develop: + build: + useGitIgnore: true + dockerfile: Dockerfile.develop + args: + BUILD_CONFIGURATION: ${BUILD_CONFIGURATION:-Debug} + container: + sync: + - '**/Pages/**' + - '**/Views/**' + - '**/wwwroot/**' + - '!**/*.{sln,csproj}' + command: + - dotnet + - run + - --no-restore + - --no-build + - --no-launch-profile + - -c + - ${BUILD_CONFIGURATION:-Debug} + iterate: + processesToKill: + - dotnet + - vsdbg + buildCommands: + - - dotnet + - build + - --no-restore + - -c + - ${BUILD_CONFIGURATION:-Debug} diff --git a/src/Services/Identity/Identity.API/keys/is-signing-key-10C452043E09C5A22FC8D97669868B8F.json b/src/Services/Identity/Identity.API/keys/is-signing-key-10C452043E09C5A22FC8D97669868B8F.json new file mode 100644 index 0000000000..09d78ff83f --- /dev/null +++ b/src/Services/Identity/Identity.API/keys/is-signing-key-10C452043E09C5A22FC8D97669868B8F.json @@ -0,0 +1 @@ +{"Version":1,"Id":"10C452043E09C5A22FC8D97669868B8F","Created":"2022-11-30T12:03:40.1630635Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8JmnWj_BpdBDujWYr1NmSawi1hoBHaFG7R1_Hu0pSObE9SiTNg1Wm83zVA3-oCwbntipelQt_gpZ3KH-NMBYdleYt4Gzhpvf4uchFNFzvdnx1X9jpWQi9WCmUm3cVNfzNG2eIKzrVLAoiaWLuDDG9XT-u2ojWIuKRKH7zEBMGrqCDQiVHZlBuqH_qzpWUQn-qKYEkFRcm05OmMYXJA0AquXimdOl1V_BcagZOpNVzG3C5t7lZTsTrm7FpD5zMdyZxL2VJJjRy5fUZbIQIjbQdTGYJaROhj-Sc4h2MgyrMNYJ-TQZKci_5ZoN-GJfjdaB-3UHDk_uFrVk9WpAYEWw0rw-I05tMu8vVxlE2jAnEGaVWEqS0f_Bz53MkDQb2YEleekTwaLJM1JZF791NTaG7oDXHSetSeo7UngHt3D9Ls2FHnZwV-B5mvzZju_-U7inwsoKXjZeqzQfQMs2IsIgiXJJkS20PObmosEbq7LkT97qRrinGicY0pA2zIcDmLiywYzWpsP6Rjw7epDaDlPu2J9BZ93Ni-slfTkNdn2SvAfE2dsJmV7Pz5tMEE-0mBlSMbcrs8TuVQayYPm2dm-sZLCv-iQu0rrvGcJ-lLPliby69du5oFU6WKpo52X4Du9tcM-5prBLWRVt_gcq16zBY_f8ieNnq6b0C_9chaztCevTQAouiSk7ni3zjJo3RuG1wPUts2tLT4sjY6IK2Zbhsvi-negegn91cFA0MzdOaD2Ca2GWN2L2kRrDWioVz_P4ztVFIU3SrgjW6stOGNreRvOB8txUCexJuUP0rb85pOa8rxPCkk1Iw8BjjRdirhvU3bvqhml6i2_iqNTAxpx89A6LRUuOYvYiwJersfhOF9F-FfOVGNxxfm1AKKnUBDfgLWDAw1r8PBeV6huAqUSIwaN5PcQJcdsfER9FeP9_CJqDgNGaL-psm2NQDaVV2Vx4m3GqWQhKMlk90zee5XNe1wobBS0XW_GQYieWWhH5t0pyzJi0QvpFyMAz1mcGXQs-TiHoU142FlFQojIu3_Ynf2RHOOByWDM9krMKMn6ZTnLWgBXnWtMefXqYNEhlACtP8lIXBxcbI2wFeBgRmaQA2DtEEjlymZ4RTaiQEuVC6B_wtTTVVFx3CL-TM62Xs-FOWTYafzKqGsHKzVWnB3fZqjDh4DNwb_A4mkfxoyb2UI9W4P7BJmbV5tOktYudVAFLt3XL7uNhBYLDSOjL_n8bixTg-sLUyO1H5-IxYQJciZdAro4yGO7r40FTYpZfTyvlr3RqFMDhBEPu_nSx9lPKrgpei0DmqYWw_cHJ3mPR1HDUE_i8Prlyzsyf9F-pYV4HMyupetXeXxMAIULnB52SE7uYfSUZHC25Ffo_tif8zMPo2ZKiMpc42BZ0cBptkPKUCvkR-Cq3pFnqXkKADWh1LLBP09hZtOjXqkFdsIFZq5KfRE6UVIyMJ91L-OSHOnggdjScFJzPlgEteUbydKSlPEBkicIs-uXXe8Tk8KZg2CfUU2WnziP0-VgymbTomKR5xz36kJqMjP2VYp6Xy-t3joQEqPE50ntT0AYUFJb6NZt5stHQz76-WecPnA6ZSA3aJec7NlLrxwM6BMGDyBZpbkgWtWFJdGWz6m9j4RKuyZBnyOXzYZU8FDNpDu6FVEymUkYxEhUnpIl7PZWuiZGADwj9-Bpzg0To8jMJ-jgx8w3RDzb5DoM6-jh0IJ5vecn8lSfRzHcOu48CLY5mTuHFr93YKiQCK06DS8VY3kI1b8irtdvmY-IvUCyCJlpzBYh8vFrS2PdJYsRVWuGJraxMiHYP_zH5r4Q4z27p6vt0Rbm0S4Fm1I9z4Hmaa4igF1mnktuTMizyxKWTsh3DjbsB_6W_lNU62RqJAt_IGr69MIphVY6ZOcbLx2I8p0z4yWpuYJey2BmRQPQeCZ-Tb9HtHESxTBFxupYul8619OJWA7RfLl-xv43X81OZnaxE9wLVFs5uEowKoEOzl_K5nxOw4igCiWYWufaFMMrZSu-0ni9RVSwytOPiEbGV0XOcVnB_lHDz1ymvDI4u1rfOVkehwB_wFBeRJRSEymzoD-bgRox-a1qLJNhOsdm9WUdZF3MHbmmA8NpyqU5qOTTqCvY-FXdJHL_OHmq1nDVHCJvcwLqUfPFMfFAi0MZQEhRndeQWYGdRGOhmvtwbRX-Hryz2HKXber1ixUvY3-XCxjzSyZdn845UzpKBdsgwAwY-LFzlgNQGEKHzsZUl8MSeqx3iO2QwDwC5eV4gNWBC5QBw0NAloBmdnQ84sSWydPViOYW1CinUQAnZsW2Z-0Xw0FP4fbS4HvulNBMIxE_mfJsHny4YZu_463zGR0SrV2hEFSiXuwSx27K6xSyPRB26__t8w37AeFYXtavqr9rB5GXnEJRQkW-i5lDnhtA93uRjfNzVhUpcTK-ROm3h7b4UrUTaK_IMLXxoYEjOFm8-cw1ZGHu3","DataProtected":true} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/keys/is-signing-key-1417AE0BAF26F393609EF30FA355D06C.json b/src/Services/Identity/Identity.API/keys/is-signing-key-1417AE0BAF26F393609EF30FA355D06C.json new file mode 100644 index 0000000000..47ee8034b9 --- /dev/null +++ b/src/Services/Identity/Identity.API/keys/is-signing-key-1417AE0BAF26F393609EF30FA355D06C.json @@ -0,0 +1 @@ +{"Version":1,"Id":"1417AE0BAF26F393609EF30FA355D06C","Created":"2022-11-30T15:12:36.3392917Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8Etya4tS5j5DiLTWt2Gh6S_0SY7Shff7x3C-zhStPviP80hTMRjXNVXCumUQnwoKrJ48OLZhHuU7wgSxqXaAT3hk35KvO_KOhz5pV_sWLPOogHC-hpQTIzSEbXzyfEIkliqVgW3AfpLbuDTZY4Hj8hu-OM0waYEjCQkIv-PrftxX7JU4KCT1rxZBUrDJN_mKCQEGWy0LGLVsLV8dKyLMSvJsIz3HJh4sngzist-XVaPxat2eMPqVOJEWkbUf6VqwJOnssR-ZOS9b63YZPhihUcz6J0T5Y2aWIyZfZwSkuh8DiL9_wzKSRTmplaY1tzeq_-FhbkgwtLX9VDbeesx_HGL_Pxj5SG5aprfZ7DN2FBr70m7olb-bEl8jOm0w7ot0QGwxHft53Nf9piLEyQp4hsReq-PNwCirzFNkQ5aM6Kwvwitkop_Ab8gpzfwiEw-uVyPN_gpkrt099MQrdtljc9gXHKuXr4321pgcbwO6Ua-amRcF_9uYSCVBdIhwmpaQAgMmI-0uI-5Ph7H23ZV2zR1epClkiR2t5RWOtXNgLqoupXyBjDWrse3al-CsdnkofmtY0bqr2ZdNd_EKFxqn7Gf5-dNuPJEboDUdwvjUpWYIzMlq6_3q9KP012SbimkqMgz3-3jDBw_xe43DvZd2CnWFTDQSuD0m7DTf2GVL0DlasQNYQgO1T7xN7hgScsXmzhJNUx7iz2ze5BMqWV3vw6P6ny4sj_QA4KAi0zxl7sPgPo2p-65MWlbZy646ZYlm-jDD9dfORdo9nmURg3ITYNVUREtVg4IcEzPVDBMnV-NEf3T3vHD1V49yVhovA9oAHj4uTye-bbLUIhoLPCZ5GfR8AEHE4IhL0JyjuaWilp1qNEXAhziwZUELenYAf74wEzMvSSobEWL5Hq-ApMdRePBjjeGrIobS6KuE7b3HVsYwJt9pWvmVCYZ-nQCDAOIFcgH5kifkHu6NEi23Jk7a5hmgq1kVJXr5xcli9B-bHu1mO1kl0gXsqJcGpfetcw8bEtwO8F_6kdkmW3RXLa3rvrnkigPJJtn4sm_3ee1dqFYarJlXHPugwuyzfEPXNiT1HCcDhQYlrYxfKSzmlhjthGn14W2ZYtb3YVCXUFReA8V20rs4ec7VTdjuKCPgA8bk3xKevr6aETF9I65hbI0pIFQ9_cJHkl9a3BZL-X5qSH2s3_rvPupVuP6mu-Y1pUEGp4p5XjZc0CqsLm-W6MsUKmBgeCiokmUGfs5P_Riv9ZuvlHyYcwj2qmgf4MDkmhThfisook1nrPomO1lByHMRHx8rwbVQQXHPKMI32WZxfpRQiuAERonMvLBd2P2UOutqe6t7qy_gbeM0WNEcKb08UowSxGUFnEH6Vk1Dx9Vzye2tw7ej-EsT39hudhe_59nd9IYvx1o547N1cI_ASDVn3ibVQU_BWLrSK-HIqGaA52F_hthPGpMjVBO8xtLXHiyxqI-gh9iLq0vpvMeFQjyLSmFjKQifMEVaATAF9h2TJTc0yqkIVN9y77LvkUDoFvi96zGSYDyIf6rtiF_E1GnAwu-6Le16bj9npI-FcNmh-DRIvA6LepbSXs27rVMjQdK6dOPDavu5Y9IM5bg0pndKek0orZzkbaFwxTxmM08AhKlyN6PPiIqcZ7i456FQDhqMTUEeBEShsOJy4573e0GA-J1sHvzVQYiKDlzvgYD-zh79OxnJXlq22BNrWsQ6pqXfXgB5UIvhUTrmcrUtv3uw6GcAyl76ySgfs3Hb9Mpk0aeUdI-xyMuxxJvz1w1NffcuXPsxf3VosZO8vkofjVSO8lGtzarScy-abFGD9UA-S7xGvz0rqRSAEnpxe4AZ5eBfs_VmTy21RYJqxPbrYWcjrRdQjxERhND70wiuLkF1pivUQ-bohP3w0EfV0fJoSoH9HthQ9_aY2ASJnv82BX_lPRdEgYVh1RYWziVGB_6WWkch8BLrUzXezJZcfQHGuEQYzVB17RQIhCqFo7GexmHkFB40punvvS1gUZOQLiqSu8UvJ27F5KNLpkgDy8q0pDWewRGmZ5J09aMlJE6e08u_gPrA9G52SWXvdkX8CRvPn-xOLsBfb1_84QBhB1PuSKeptNLjgMufTdfUkGrL_b3fTo4AgLz7VuKrAMNYjwCTLr4e6towu-JJ08Fr3c8igbWApP6yNiVbE1W1mX66jaRfyw3jiVAP2ytEH_0kLOE3BDFkepIopwGtkQ2ultqFHxr_-oSadSpP3g4dLt1GfQ7fjzYkYAkstXxgYgGGe7Q-mwQM1dR8RnE0jFvQnTKi-Oc_pj0h91HgRK_ASQrqrgOf9JN6McbVlE3VbEKvFfOKP8ukFa6i8n7tk1H0B8bX5iy4m_b3i3MbQmQ16gkZ2ECW29-bA4vWz6-R3LZ3kf2hHAJzv8U6cG71KLVgWSBxIyRJE-cSiBPxtM1hGT1qcJvpAVWvowkcO9Anng3lrfgnGaYh","DataProtected":true} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/keys/is-signing-key-22178AEDB80CD0A41DF6874FE93F794D.json b/src/Services/Identity/Identity.API/keys/is-signing-key-22178AEDB80CD0A41DF6874FE93F794D.json new file mode 100644 index 0000000000..07609260d2 --- /dev/null +++ b/src/Services/Identity/Identity.API/keys/is-signing-key-22178AEDB80CD0A41DF6874FE93F794D.json @@ -0,0 +1 @@ +{"Version":1,"Id":"22178AEDB80CD0A41DF6874FE93F794D","Created":"2022-11-29T07:52:44.7571677Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8E4RSuU_s2BGnQ_I0J1Xz9YiYIkBDFNHHGo4A20gtVeWqoNIQNxtP1p0JacgjeZkGl9NlE2-7qD02Vs9NIZ_IfOHW71XkfBIuIp5JvnwMi-X42Qw1F6UTQKNlYNvk1Fh27a6tM-mQgxysayCdDLlJWoRJYKDPDkUO03rZVkWDD2r6QNcMNtYhjM7oxy2yiw479PIeIldPOk73cPqp_k4oMPTRWwS04dX8fpQNA_3d2PoV1sw8-Hbmspiv3PO-umUGU2OFcN243cP9aaGXDoR-RgEbCn225WuHAWTe3tf39zVDtbAWDnRTNEX_jCNBItSaFOdMerDKD28PXTnWXuoMKyZ1_8ETZk0CDMAMVktqOatYrx0JHisG8vJbHtzxIvQrZL1YqoAN_qraVLgTl99WIZv56rYSb3RbNSzqB7yNn1Y_YAobXVbqJCaqqVG6uEaQxQ4WRI78sit630anIlKP7vccOtJH_bspQlPisNVrpCWiGo0D0IzNsZXLNPkc9kUbJKGc3xgE5H-z9kbk3sOecVH_iQy4b6fJ0IrDGz9fIhu2UI0-_lUE6sQ0g-NAA9kW68vD6PVSytiag7tFVV0olRPhaiBkSRuNGKjqsPmlPtkpzVxuTxSj19YxoC200cFARZq68qjxZ7ugdHQGlqd6bKFjj6NWCfG3Sbub1HpTkPnEUIR6ic8EfUrbu18MlOh-7bnIJkIuz0Nr5zZ9tPgiJCdGPjNo47z4i-kPWiNhS9k1sJqg60dgCT3onC2Uft4F4dnQpIKb28Fli9qkTDOg9gObEr9W19tMJvEPAhWJSl6HSmIJ-TElrW7nNCdS-H7PIlGWIvP6f0CCzrlgtsfMh8TK0B-Vpv5ky9D67L03IA7VfrhHss5mEkoz2s5-yefn2Wo5uMTxy53O4GIS8j3lF0ufUIBjGqR5q6IyEQUyU79OA5A7A3gJx78Cb-OEK3GixhYwjXYOw1YcguMZo_rGsq-EnjPoVBuZfdwyEIZ847gVwDzFsh0FHZIi2vVxXZ3tNGjJI2TXxNx7nyXuBxERgCPI3C5OeK2Uy8e3ooZCqPICKS9xEnICZteEcSYkRFmvPX6O1pZMKWoiyzGe-bhuBiotTB3yGIVlkhMyqNni2hQHoR5b1CX4YaGBMT_fdtW4uhvYLhLnGR69aal2ffIhiFJMfB_ByI8sALzgsn5jW-zOAmtiEOaPs1O9FqPf1x3Wk2YuoyujgHjHi9SSUz1hAJRmsyCmcYsMdGjMd-wnk83dsdMMeNANTKJHlZP5-__XA8emO-sloHrCiHpALX-JB5RQ1BGVD-dPiTlM0MYeTLdt7y5HRKSAbsOCnnF1vzV8y8NkUgpKVLTPRsJaoNYqik4Wpoa2d2ez-gXrG9S-pAByXHyTHiVDVNyqUTDMI0s8Fm_6_QwJTkgGEV6dR6r89Rzq70FFjdKB5RygRLhRdF2Be8blyExHI3HQg8_S9fcZ3zCybjatlNuNCgNZTTelC1UzWSEql3HqeqiMWIi9Kr8GNl59OIGm2lm9KNoyvcFHlQVQ33lw4akd5Z7aYu_HcdpBhMcEr9CuN6yQPhbCDOVhsMkqzXGzpwC2ezFnALAkbj9P44vRU9p0FEKFDNn_pQimVTLQJoU5Taf1n70CxYcPgM9BU3JPL9VUb9rDS3bHA7XQoO6878k6PkLbJ6GcfIJitaRZL5TkAUJUywH-a5xADVo-1_k3ADJYhOTbwxsYU96Kuhw8FOIToBH8CtLnqJLoJ8iwVPjB9LSsHPhmR7Rsi-x7Oi5JeugLctOvBn1sq7x_hHQKPVHA5LvtQ6Y719bOgPKLybQSCebe0ordUIsPAZMq-OvLj1sPS8PvfvZBDkyUA04avHm5kgT6RJF7ARUI1vMNLijm-_dxxrzY2bj6FHT340iFDURFePLdSM-Ctu3X-gLXKqPzQ6MBBQS0pyC3GkuX7J_GhPOZ-yXvRJmvIEoDAVEvjUQc57Ud6nyS_C_ljLmIoHghJiTJxNOOjR5pucb9Vljxm_BGPZwMi6-bTbD454ESbZ1kyvOAG73PjnPoQSaEYyM3XLW3XgJ9R4dnYDk31zHpQebodqwcFsXzxV43yi7zPrJSMzaFnC3-uPQ7qH4xs9puX5RL4q4lSCUKAB82TMtCJ0xHwj4DYqsJQt9jUAz1IZbgbChq8d0EPe6y2yg38LZs50r6DD0f-VgF1fyup_1ORM1Ub6gRyWxIBZLyLFY6uLvzPhVQaBsDitK2ku-9ELv9frr0j-BPNNCBpNRQyJCbZwOcovJ3gn9w1TAJ4cXGxzJWRGVnJY0e_tJ5ZMwRrn4E6LwhKXK9N2lBU7-2RQxEU79sTRz12SV9v8Az51MlUHzjWZp8p2bttpkQKMeoIkIVu31t88Dn7MOZkwR5DQ8b1_tNV_3Nbqyc9OUUFTSrpfEJeU6VRrUilgvBq78lSov4q1tbKxp4OjhxdNv9NxfYCL11kTZVB_h","DataProtected":true} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/keys/is-signing-key-3139DF3FF07C8E3881CFA9743F89A787.json b/src/Services/Identity/Identity.API/keys/is-signing-key-3139DF3FF07C8E3881CFA9743F89A787.json new file mode 100644 index 0000000000..30deeccf47 --- /dev/null +++ b/src/Services/Identity/Identity.API/keys/is-signing-key-3139DF3FF07C8E3881CFA9743F89A787.json @@ -0,0 +1 @@ +{"Version":1,"Id":"3139DF3FF07C8E3881CFA9743F89A787","Created":"2022-12-13T11:30:03.9474661Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8LWRXnsATcxOsVa-h-Gywwx7S1URlnPkv5vJtTle9xG_ytf2YXKlRsyey8qFhmrfLZ9uixxUmHTLywyrhUV6-gdvwyirWsUU8qn3SNe4rlPgNYHESkjYwWHERmwrMQIjRell8Xnot5UU1tfIvdpX9AbruzjEuAtYg668fBiXv338XfZ-2DW2RlYxT2TsPAak5ZsGXuIojYCLC3S4uGRz8AhSoko_DSl2KXQqXsqJWTNqAmiEHkVtHeFzc_o2gJIbegzqi7l4F2jDxM0-gkkRZYk6AZlN25wYb90xYaM82bKbzEnqPhLEIkuM-3QTGnjLZIF9jFp1qftq6dDm0feovXXM3SQCsgQPvZ6rKHR_8f-Z072t8PiRxDTgVQKywQ5bCS3u3Mq89hOZf_o_IBNT4uO_hDBqv4eFhj_AAy1Kk2K_QJZhF94bPLsujkkoFjWlNXvm6-Layr9chiY7ZQGxb5QtJr03Y5CT7w6hO2LsdsYEyJLs4kWBdlILT5FjrB4LCVHJlVDGxPBNuGzZL75rSGDa38FCKsUhoBvu7T5XFTm37FaBYrgY7MASQrlHQjGPwyRrjGe-M2ZwTCrtd_MwAHJnJsLoYs6SLQLpQa4sII2qgjRr2UmfDlXP12E6wJrpWfSL6QV13CoCy4hEko4LAy4tG2Yz0HzsqFzPYL142UeJC5z-d9JNdJr9Ya8TqVc7biOJCdY47jeOF5wJpM_BWTzT584w5HRs_1LVNsmjm9l6w1v5I04uNxbpopfRS0B1J2NPS3bfpS2lGuQlyPsZJwNgkZNwxDwCQMa8cgwREnaKWXOeKNksWmS6-ve2M5DguPaKVYF30AyJ1ECqWLX-CLJScBym-ZwVM-2pdfKw3pucfXXUqsXK1lTZSWi7A8gFtnsUZFUC2lkltBT80xuw7f9ojIfoYZjGSwl1mwM5LN5IuwUyppToU7vct9GueMaBjNP5bTrWS1XwiU_oOJbjIx8gNdN-DWNG8-5VmdgXoaJDcd_ZRLaM2XpWcpB5R2MwodIe3HGdpieTDxdqh9Nd1TzQO3FivfxcW0K21CQC-xYaRsx8Ii3djek6k90C304VFRF_QLe4lTOIaAjxxDSFVKbn8tVOZyINRI3FRAXOOaVBoGR1Wsn7bXVGNGbM5eN6wCPOzE-g5_489Sq2MWgczq33_GHW8rGc840OXiqJVu7fL4LVIuG7hPzaVZD3Q-jCu3xG3rYmr2ophVP3HNhl7QDgtpHLYNAIdzZNXz7ROKJU7ws0uZq34plYnLhXEzNe8ntV1N3qCbv1lMk7RjfffZxRwE0OM0mbIC--SPqKG_Cm0cD2k04vS36N9mr1LCmDNiFQRcOqzqxgk8mJdl0Vna-IYycviWyUBs63y6n7bDU3RAifVTSVPUrrrAIMTT5uk4ZVLbO_tGKvKWaSsi7TMypJXd993tut8SPwCl8iNZS3qPwIUXx8IpStnAmJmqcWLZcldZa1bfOWqXW2s22xLNgexft7Xosp-gPNPsICPwtux_tXN-XbbgopGnWgrsyrXctm95OcfEUOVYna73ZA5cukdUbfftGVBitwe7DkG1Jb3MJLZbo2ykiqW71mLgDqvQ2KD_PHw410v-51jsXkUPNknyeSuHiRXomo2HqUf-y0xvx2S73v58yM39XadMJUMYrQlw9lnWBqCDcYoutAVakgWAXSPYELDX2BbmpZsa099h6HlKJptmSLqp2D3J7fHGGWZpdBr9hxVQ56TkMDUNCEd5W1Bc5ecT7b1R5u6IuM16A5aEGOa_phaGuqc9cUhD0UmRBDO6FE-LbfjCnzhjroAOYEujJloOcAYEAL1zx3wUHd_-0hVkmffPWC_Wu2uV8EyQQwlj8bVgCEz6R6bqxl8TN5993C2joikVDCcFYSi8RingP-ItGC5TVTxx0kWweImuBA9s6eoqUZ7TMLKOAQHTxjx5g8mBkLs94RVWhChIUif513Br0aJGxwjBvhN_NBWDFJSbP-l581YNCrmALfS4IX2jeV81bnOAAde53Yplaski3eoR2Z3daNdAJNVqOBe32gHf1eTwDCXrgqd5wYsXxW_YJ-P2kKN2CpuPE-so6tdoCLqFmXu4-3q3vGGE_nVvp-MnhHwLP50U6-h7_MDh105qlIxduM91AT7I3XPTQGrOeIv_eBrng3vULA_ohah-OzbPxpPmYSNFS8YUlGChhPgmMWHKnaYbirh-Df4rc6sK5OFEyRVlCzeKon8hFsmwIEEk7uAR2AysF1PJWjoObQkM-3vqJphkTBBYsZEmngjvQlV8TnGY6P0GRe5gNOaT0BZ71SIlKQY1iLq5NmzEGM1iIOEJI-JA3VHhXs2mw7WDKpsE_k5kbFDlgdachB778jMTSs8xWjESxyidPz-tjqvWaDg482VjoOVYBQOGyUuFRMKVJ9xZBE03nVLG29cKVCNJ_KylBUy0O4LkHskJRzrbZE_HlcoCafl1z1","DataProtected":true} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/keys/is-signing-key-870FA7120249C21C30ADE458B07918C1.json b/src/Services/Identity/Identity.API/keys/is-signing-key-870FA7120249C21C30ADE458B07918C1.json new file mode 100644 index 0000000000..acb1883a8c --- /dev/null +++ b/src/Services/Identity/Identity.API/keys/is-signing-key-870FA7120249C21C30ADE458B07918C1.json @@ -0,0 +1 @@ +{"Version":1,"Id":"870FA7120249C21C30ADE458B07918C1","Created":"2022-11-30T09:59:58.1299062Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8BCNHSqqa79Fqw5EoSNNrOMZDqq5p81RZsuqUc9iuvF_3nm9RHjc2lAsu-T8AjFPxhbWkSNnQAU7JGWpNh_TU1XK9-Skieb68wO2lUFauCBnoNf7R7JEpuw7zdjkInQZFOF1hBzhL0qKszqhM8-2gz2Q2V5c8Ng0C_2CzHNqCsyhTHBD8NgXfzpxdym7V8HZSS4SkJ3QpKLH-VMXRFssCM7PXEimNWBN2yky4IW5_fRgYgRkkCaXCU43ayGxLyBe_J6DzZ1fefO7ZBne2b8HaVVQaAEQDTlsFUxNqDLcAO-k6q3FoL9ZW2gm82a8ra1mSJTbEeAacNK5oE22urnovVF1GPwTVnnXMi_9eloBqyTM5xtTQPYbVlOkZl5pgRjKOY_Mc62Ix9p9hoH-gCfDHw2Q9X34JT1ymzBdYqtgTmWoY548-ZkX6v3AEpkhmsMdKMyNSdz-SJfasTqKkp4JkqGR-gXW17HOtxHmaOPg0ZRuQ4Dx_YHuQr98UQPMUktYL_kF_76FevNyfy5eyN6eawHNGwhXzn0YdyCjaRV1LXfD-4LqqJBhExUSyCtG0Pq0qjZ0ySAuKi72-0532XNRJ1gh7CxlfRdeLDID4ffs1cgmfODFv1k1JElYihqqGwXMwx-SfgpM9eo4j6jqd0kdAgj3n4ZsJKQMCDOGv84Gfhnw7dxlIL0e-D0sKKc6f7KagNFJCbt0LZ492IWzIPIGp5lXQwhrDPECjdNvBv0jhC6yMzZQZlVQXX6MYue_qzXb45TK23j8HjlgYPCip9eqH1NBA1MWkHykA6ZkdZ29eCCMuO-m8zB_Z_7YdEdzGcViK25T2q8qo_0BMzAzdrEJ3rOYOly7wzsoESn6aKr2U1b4qh-fljOwQrYqoSzuiu5l4QY1qVG4ZR3VMjV2lo3LEbMayA6Sis38LQy_ulUKGdOi4KeyNSlbL44wN48gDM86kNPkTM2kVoJc6RRZ5zptnGyXgAsuknHaF5EX48Ihxt2AFU_pVgKGnrx4XkYkxMEwbInoL19WgaKZyLAXjwtUG3FSOPdWswUqbcDNoetfY21Y9yWZsZ0rUTUa3x9rLzRP-H9NGbK_Yf8j75lpdx2sFUvJyyxZ2KEoq74bS-Zg203Q3jhz_RVJi8M3R0oRQHh2I-wUimdAHQ9JQu-M-VAtVLG6DVrY4vyhdv2JxIPyLrRTJPVhMDEqKqUcbm0VJE8MJOb3C6hdFrrzcM8AAkxGhYHh2vlJu0MAa8y7U0lHm8mcrNqsjPahAFvVMwUPkxaXFSYwJDdnBfODquWHt6reE9GwQw-ZFvigPGmY3SWMZiEBK-2Zkz9NglOKEgnojT6xcAbdaeEDadvY-Qriej8rMBZ3F-2IHYNMM4J2MkbexgloQgS6UWx7Nx1ln-pI-uHCDUmwsb5bl05Jma4_DXMsIjzinc2u-CvpEUXe1KmDddh7NYSrUv5RjaZfxgzjYCW3gSynFRGNfy57PaX7R7VV9pn3IRMKsbdWKlA957M2tDqmgkzC2M8KZAJRFmw_tCVk-swjPjyFmXUgsdLs0oX8OSAh4JX8DWvK42wHerh4gbjX_N7mYWh38e4-jMvyZ5Zdqf-2phj21z9uZla1mCsncjDDPJy7sVCrlJasgPLgtCD5j0lxp3wdAjvx2I4q6CKNcP8-MapHABe2TIgMmEf2VOJv42yrO99q49OwkopbyhMf-0-drAOo7Tw7eKHH7S174LfEKewCMmj2BVaM_I-rTgU28Be8pfbl6jTmj-9WCvuBbBiCtvhpeOApuMbJkc43As4QV031W9DEQpB520DWUdAHjvNkUMo0QDBLm7Zx4xzeO3Ei35Xw8_w9OjZBCZw3kalIbZAL9MlsyfFXczbZXMZnzkccPTwTK2SgS5lq1Ic07YB_uqY52CwYlpjbRP3ygM2mVCLTB921NhXKh9-Md4oJ9ieaDUNyNPch2MLQluKmyIH0sumgBkyn1k17IM4qN3EXRSsUiqa-FBU-PFa1gVIf_dhdnLI_GOPGEYBmZYTrk8J8HK2-wIi5shGDOB-WjBaAWbymdMmPhifRKiDKdOmPVArGptSyrM_5UvHF189Jl0ABxEWcsW6DScgw72GtcXxhDQligdcjanZHNVGEnfwFN604v68G42IC7clOVAno7cgaapHXXYSadEqVzP_Q4d-d-obG0v44yMu3WSdfhmJ8GC-8jS3b16q5_vTfyim5VFmLdG0PX1L9--QYop1Rns-64a5Dz8T8MmyrtgRXBAMfXC84R7N8I7SgRbsSo2pgMzRafCOpYWqfUlNcPjqQXSPj8oEt0iz0DBq9ui1dK0JiiOGOO9uOdswQP2r_cVlF-QvTszcyy1hn6QHCdsfqojSi70jmyhpBa5Vgba7nyP_qbJCtYiZwk7RLWT-jZ47BwZzEK73UXoAYCc8NT5HsIW8KFuManq8Mau7BLNj6WhXJOKmbP0oD88LMj3TeEvZ3","DataProtected":true} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/keys/is-signing-key-B0B34AEB91E2638F61EB9A5543F3940F.json b/src/Services/Identity/Identity.API/keys/is-signing-key-B0B34AEB91E2638F61EB9A5543F3940F.json new file mode 100644 index 0000000000..ddcb78b6da --- /dev/null +++ b/src/Services/Identity/Identity.API/keys/is-signing-key-B0B34AEB91E2638F61EB9A5543F3940F.json @@ -0,0 +1 @@ +{"Version":1,"Id":"B0B34AEB91E2638F61EB9A5543F3940F","Created":"2022-11-30T11:21:15.586844Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8Fzd_R2KZDdMrJulCrd4GFTnFtf9LeKAfSemG0-9e385u01WZOrLNU5PPCNSRvcQKMERaUvLDcAsYIsqdKPJ14QGDgSTlxggwrR50kCv3494z5uCvWOvWqYc3KKzhR-7e-mXViY4m2oYHpGaVCITKXYwCRYxO-P0mSdr6D7W-846cfdst47_rjIxxeMajL1suYSYe_sz_nz8ooKW8ZH_kl5M9cI0waB2XFviIP3f08pz_eyneKqy7tCc3ruSNiUzfUBAF7fBHDcyPrE2h4AKaXG1PT-I6JOFVj9lAgv_1ACSqTVEjJiRWfFIfft3sQ-chSv1yxy7b8GvhIsKKEn-OqXCnYr5pFW7VPagt7pf9IQdivU7I4m4zSyW0bjIWXdCL3sXQAChiDpvRCFqFfsiH4lWmI4X8Aq8Tz1_LkbicHW5-ZxeZpvgPlg6t77xqFIxFtixqdc1XxifVwJR5GwrFcbcI9VpnlBomkBSUFMslW9NxGwSztgnYatkQuiXTzdqFoOr5d4YGPEHe_Oc3h9yDyUxgBdJK30sSx1Dwki94CeQNBS7PisGbu5q60MbsyFQ2S6kBogReQdURLW326NkgOBJ27jHk3ccx24WfFuuDzquE7xetxvnX4AV0oKOudMClsOv4Wr1aMm98p4VimUQkAneFImQszK1-zSQfxeElamoYy-4h1P2V_A5siSmfKpDs1q2ja3C62lBr0YSx822O-QF4XI0id6GOZiCc_9v52B_teTlgMq6L54EjqDM-h6ERkPvoqNFL0fWz3ktOBPCQm56YrSyyaXNKG2TwL4AMLsaQ9us5SXnJ7UpiY_7ls4SVm_h3btoQcokOiO247mYo4-BRq4pCsFHNBsCReiKlJH8G8fwrujWcu_U7NdVWgmUgmcsIh9mfommuOdBdVQdP8CYZM8od1NBIIifIoj51WuZXtb9Wr4QVIddEjAQrYnexJAozF9TeBVfjhDkIIA2-b8yBGqNl5VrF4bo3b3LLm1rbq5vzfKN6pKzs2sDGple6kZn_t0Ym5fmf8iWXLzMnRyasEDuXX0-Vg9WxiWyXzHsUY-SZEfEXs_WfuzBZb9NP1JvrB_w33cUfgl9Sa6RD56O5F1YAR3WxiCiWGnlSnqpaiiyTb4NVr8N0qQl9xOz9KCSaN8H2WKgCML2DdjW5_O8Zea2EPpSf1NGB1G-Nm-9HWAn8xDAei5x8kzTIxka6Jb8GO6Rdffk7wwuYMoB4ZYuiuL1q-rqvaU5GffQWMjGz-yH-WCivVFCosvJgk9340K0bDl97CULs43LF3nQFcmuLQoW-fasNYk5ZAXYA8bjn9eYm-hWbsfA402c3iZKKZVcLAU-jPAr44Z1P9PbT-xwooGTKKnZuR55l24T3p4hDatpazlObRd27kPtSLgbXPkNKbru9N8--JUhoEeFGC8E01gCn9256wlpIO1hk9a-P87t7zJWdYkl9Uo8Poz_Hfm9Uaml55BWSghfXlB99hULUmpnPwyOVJnRhGOrnr6ys_2l6bCW-gZlWS7oevQ_BuJBcCXVuRYQPbVtH9ZZSKF-S-uczbtfzhJfH29tCNsHzAukVHFiIwHqzhLHNu1gX5COGzYdN57p7DXGkJXGJUipczm2ZAfI9g-Kgvy6MaXScuwaobPobL5AH5WG1CclEb8kg_08rnQsDfvNhmsAJE7dN1GjpA46fP2bJ09T4hfNdmW-IZbeQaDMGc_8RpoADdUY81swXUgrL56UxoCFxqifXtskbzxF5Z1ad4qRYNlqU_zMbxjSZTsSizNja4pmsx1O2NAhvN31bSoA8PWpLLz37Lq-craDpXW6-mIySm3NQWxTLx2hsk4i29-dD6AjUX2aDLEng4JLKLWKLcH_ulkujNP0oXdkXcGikLqFwUKG8_lEWV_ROlXUVlmwNf9WaohIX8caCK2CF_tyEkCxEjZIBzw5xazq7_zNbI66rXfqBHxDKqrdM3UJYBNFMk2LLyEGD4TetGu4RzBDt2r4rXatSXQh9a662LYlCXxlElP8xfH7BudBa9KgV0FQOdHQ4sm9chHiT9JPQX6bHwf_imYHmTyMV3cIN7f7bACLji-5hFf7if8ANbhY7zer9-rQWR9Wy-vYUyUaSWCWRXHQC2eXL0XklBNnv3vK1nFOcaeC5R0GGULfMnBluyClu2Z67OCsAzQqi-_aacxQVN91SNmAfUbVCrPgBTbQY_1u64fEMc8ZcL52xwT0qgCSvBPCNDEJ8YDVVd8L8eLX0hg7rV8SgCnAl5qG74BCdkmhCMLB-OiwGZxZ9s94yGdjX9jA4D70uOgn5t9OhpboX3OoRK40Xged6zCBEhUd9MilD9o-BtEPeAHloKqMrvrVDE4l9R739pjhgOfvXqGIytXD-bKlMZAY7dnJ4vu4GjVvhHfzxTV3zu_nAV8j3n41_knCe2RyCm9j74YZejnRC15ADqItTDmTre8_iS6hTOrVO9FP","DataProtected":true} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/keys/is-signing-key-CFE19FEED1112F0740C7CEAAE490834F.json b/src/Services/Identity/Identity.API/keys/is-signing-key-CFE19FEED1112F0740C7CEAAE490834F.json new file mode 100644 index 0000000000..ca414c2b24 --- /dev/null +++ b/src/Services/Identity/Identity.API/keys/is-signing-key-CFE19FEED1112F0740C7CEAAE490834F.json @@ -0,0 +1 @@ +{"Version":1,"Id":"CFE19FEED1112F0740C7CEAAE490834F","Created":"2022-11-29T07:38:58.7490696Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8PyEV1YRvftHv5uH-VZQ5M7m2ypM-2X7o3A9EvjXNQQsFD50DwqX3UR8e4cDEITQrBowxyPPRXo_bTgEnifJjx1kdzA3q0L-FPJqCOjh6AaRqUg_TlnhvbEgG1HYoBZTAwvhi6g3h86RFPU4juwhOWHcD1vzCgW1vfJ9VFXXq3F-FSfbpdykS5bLn5hUdg3zgNWh7zZXOIFxgNkelR7lGMfF6hquTQMbCRhtDg0-N8O8m7M0JaNsB6_H6v-Z2RlYzH0R0B0Ka2fH3uPnudPrd1RIP4BONzMorJpDFZP9TF0DY9sJhkcHEhKDZlRlwI_Z-e0z8WeW183Gg_HSV23wO4B5R8ldXRSioqWyUuQfoYplUanuNeccblBe7hxQw9fhsanHGgbiuBjuypXarZmlVlbFO2zPKl46Vj8YZTxopOFOi4ALcNlUzGgPG5lwPjDfXEyAp7_ipMIW84UVfwqnPZFE1FASpFx1BPif-huoAaXw2GF4HJ2QVqfxkr_Gn8isrzBOT3fqmeZ1n0HNOc8VxCSdScbBSKFMnxNSgPQSexHGSKnXpvXF0OL6R8oqHoi8TNfYFitGE_qED6wCjQ77qMXOUVXB18kW7nJYqph2u1BH1PykRWaWDlnh3H7fKWRhO0ejaTJWl7oSLllYzi99yL5JvudDCjI0QdfbMz7o3SrdrZbIKQcbTPPgl_cfNneP4QA6MSlJFbG3bc3q8IUPhISrZHO_s7SsoX0BiP-AHcA7M_WrkAQYLkTMsBuO3x6N-bWjtTouqzHY7-h5ZYHqvk7Mg-jlgJjFjgKRDOJFiDorbf-5nvxjl3eLV8rjX7ML9KqjD-ZUjMsDt79lpjCi6B2EFqbiGT35I67hC6a7l8gPjwLKB7Oc8h4YggwyS1eLCq3CgmfMqlIAeAqLL7nVwDPReW-pirHix92q2tSkKCuz5UV5jphmDAnRFQmICaIinoSsBtjb1RlonM-yklDO-jdR5r0ZEFb8ZCssSojhfqEi5mcK3xJsyzM_Inzg71zAHCUl5cuBhbWekBeHiqb2p5ar9guCK_h7zmga1ZZTqfWAGzxHrHkHb6XiK_wLlwBjJEoGhgHfi0UzUECddK2DVwEuTsXhNDJ8CgB2oWNhIHgnlNxL_JA6_bY8BnGtcOKnEuaKpR6rXEZit2Jcr-1-Q8YJ29tVNubphYm2ZMrDTGdAVE8ODftey6BIN_HmBELf6a7x-Cd5DPrW46iVqlOuBqmjxjZ4HHTS08HHGSD4_xj2vNaWl8rIbE4BbDq9V_qyP548KLNbGqSIed38DDcbsRMIuac8po95Z3q6NUf2JR0mCOEizdrszZOW9sGVlO-WReaTkpNN8MaKYoUy6k84IxwPPv-Tov0yMdTN-jp7H2HaTOGDvEb72swcW39huoAbXwjZdrpFQjTD9_cnNrVdclDGN_yix_i8-vQDQlQ9Ronpk2NvAdnrmEzQ6w7D7PqAe5c4YJfn_0z45ERm9zBwCMCbw8lP_Yi3nwTbqGtFqAoXTSZ8vR5_UxQLqxsJy6U6gK4Es68-hnD-YR5p3CzBXxSEgV7_qvW2gly5VCB-1yoD8dTVInogMhOzDZD71gmpJURgKXX9WN92yq-YB24E8koOXdbxTvSblIVTxwDqQ7lb81y68HmZVSRLk3zhxbkEkjI1kkB8uzlArbXljN7LnxyXHU81DDeufVK5_myoIMA5NpSwEKxAkmCoF7p642qBwo90k0q0AvJVmoy8xpCRiJyLDR8wYHFsed3T99jvvvV7GyA4SG5Tt-ST5kKFKvOOwT_WrOg-ao5cz6Br8_YL2xW5dB231R4ZxfJlmrJY9WcuTQMZrWnYOOSj_5u2I4sgcnp7nrRF1l2xnieiym5ef23a-rSMBZi_leN6BAma6N1M7tu1N8cEw7wd_6FV6hqrcKFPGNm4HE5M-pmoWvzjBTo25rh_4atG5uEmDgmhmY6qiZc1eso__Fz29uJ4792kkpdGyPAcV8igzPMdKts1KiO9ThrDRH25Knwxa1tOWwezkeId-OuZRwC4LhGuZseww5TxzQtyN8mMWbLDPCmWya56F-i0jag4i-r2Z26VEmhpSywbACetljEsaVDJF2ZdIrIFJqjYLa9zhcKbD9GZPHZt0HKilXDp8DomCQHHWsIapwCiG6WLGOhVc0-0VJ28dBqIowyCQScefePKIIk7Oa0T4UzPXcXiXYNQktHGs9VrF2ux4De-SbxDRdJy1IRSH8_NQ4SFtUelO8IG27B_8Ywbonn9yRsoojmuS-fXPuWSJMHDjPcSYwJqQyxjfPjGCLsnQWRZCNT5Ft6cfcSDRZWoCqKK2qLfqcGs-FR71HM0G6ipAuVI_2IyfalS-fXqTPCncyFsOqp4BRhhxLFu8UxTZ5-HQyKH3fX75mZCD21d9RObyWv8g2T0HtEBLEf2eR8I8zCUcOf98asCoT2CU4AtOhVwqrE2u1qNKupc5Nvw","DataProtected":true} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/libman.json b/src/Services/Identity/Identity.API/libman.json index 3ff9915807..8a86a776ae 100644 --- a/src/Services/Identity/Identity.API/libman.json +++ b/src/Services/Identity/Identity.API/libman.json @@ -3,12 +3,12 @@ "defaultProvider": "cdnjs", "libraries": [ { - "library": "jquery@3.6.3", + "library": "jquery@3.3.1", "destination": "wwwroot/lib/jquery/" }, { "provider": "unpkg", - "library": "bootstrap@5.2.3", + "library": "bootstrap@4.1.3", "files": [ "dist/css/bootstrap.css", "dist/css/bootstrap.css.map", @@ -20,11 +20,11 @@ "destination": "wwwroot/lib/bootstrap/" }, { - "library": "jquery-validation-unobtrusive@4.0.0", + "library": "jquery-validation-unobtrusive@3.2.10", "destination": "wwwroot/lib/jquery-validation-unobtrusive/" }, { - "library": "jquery-validate@1.19.5", + "library": "jquery-validate@1.17.0", "destination": "wwwroot/lib/jquery-validate/", "files": [ "jquery.validate.min.js", diff --git a/src/Services/Identity/Identity.API/values.dev.yaml b/src/Services/Identity/Identity.API/values.dev.yaml new file mode 100644 index 0000000000..1d6ef67d3e --- /dev/null +++ b/src/Services/Identity/Identity.API/values.dev.yaml @@ -0,0 +1 @@ +enableDevspaces: "true" diff --git a/src/Services/Identity/Identity.API/web.config b/src/Services/Identity/Identity.API/web.config new file mode 100644 index 0000000000..a2cf1fe26c --- /dev/null +++ b/src/Services/Identity/Identity.API/web.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/Behaviors/LoggingBehavior.cs b/src/Services/Ordering/Ordering.API/Application/Behaviors/LoggingBehavior.cs index 838697c8ca..9ed7d17723 100644 --- a/src/Services/Ordering/Ordering.API/Application/Behaviors/LoggingBehavior.cs +++ b/src/Services/Ordering/Ordering.API/Application/Behaviors/LoggingBehavior.cs @@ -6,9 +6,9 @@ public class LoggingBehavior : IPipelineBehavior Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { - _logger.LogInformation("Handling command {CommandName} ({@Command})", request.GetGenericTypeName(), request); + _logger.LogInformation("----- Handling command {CommandName} ({@Command})", request.GetGenericTypeName(), request); var response = await next(); - _logger.LogInformation("Command {CommandName} handled - response: {@Response}", request.GetGenericTypeName(), response); + _logger.LogInformation("----- Command {CommandName} handled - response: {@Response}", request.GetGenericTypeName(), response); return response; } diff --git a/src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehavior.cs b/src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehaviour.cs similarity index 68% rename from src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehavior.cs rename to src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehaviour.cs index a4ddcc2038..e9deb1cf76 100644 --- a/src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehavior.cs +++ b/src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehaviour.cs @@ -2,15 +2,15 @@ using Microsoft.Extensions.Logging; -public class TransactionBehavior : IPipelineBehavior where TRequest : IRequest +public class TransactionBehaviour : IPipelineBehavior where TRequest : IRequest { - private readonly ILogger> _logger; + private readonly ILogger> _logger; private readonly OrderingContext _dbContext; private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; - public TransactionBehavior(OrderingContext dbContext, + public TransactionBehaviour(OrderingContext dbContext, IOrderingIntegrationEventService orderingIntegrationEventService, - ILogger> logger) + ILogger> logger) { _dbContext = dbContext ?? throw new ArgumentException(nameof(OrderingContext)); _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentException(nameof(orderingIntegrationEventService)); @@ -36,13 +36,13 @@ await strategy.ExecuteAsync(async () => Guid transactionId; await using var transaction = await _dbContext.BeginTransactionAsync(); - using (_logger.BeginScope(new List> { new("TransactionContext", transaction.TransactionId) })) + using (LogContext.PushProperty("TransactionContext", transaction.TransactionId)) { - _logger.LogInformation("Begin transaction {TransactionId} for {CommandName} ({@Command})", transaction.TransactionId, typeName, request); + _logger.LogInformation("----- Begin transaction {TransactionId} for {CommandName} ({@Command})", transaction.TransactionId, typeName, request); response = await next(); - _logger.LogInformation("Commit transaction {TransactionId} for {CommandName}", transaction.TransactionId, typeName); + _logger.LogInformation("----- Commit transaction {TransactionId} for {CommandName}", transaction.TransactionId, typeName); await _dbContext.CommitTransactionAsync(transaction); @@ -56,7 +56,7 @@ await strategy.ExecuteAsync(async () => } catch (Exception ex) { - _logger.LogError(ex, "Error Handling transaction for {CommandName} ({@Command})", typeName, request); + _logger.LogError(ex, "ERROR Handling transaction for {CommandName} ({@Command})", typeName, request); throw; } diff --git a/src/Services/Ordering/Ordering.API/Application/Behaviors/ValidatorBehavior.cs b/src/Services/Ordering/Ordering.API/Application/Behaviors/ValidatorBehavior.cs index c8099dc574..59c4ea398a 100644 --- a/src/Services/Ordering/Ordering.API/Application/Behaviors/ValidatorBehavior.cs +++ b/src/Services/Ordering/Ordering.API/Application/Behaviors/ValidatorBehavior.cs @@ -15,7 +15,7 @@ public async Task Handle(TRequest request, RequestHandlerDelegate v.Validate(request)) diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CancelOrderCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CancelOrderCommandHandler.cs index 024f5ade46..74535c3f87 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CancelOrderCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CancelOrderCommandHandler.cs @@ -43,6 +43,6 @@ public CancelOrderIdentifiedCommandHandler( protected override bool CreateResultForDuplicateRequest() { - return true; // Ignore duplicate requests for processing order. + return true; // Ignore duplicate requests for processing order. } } diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs index 6ff6bc40fb..fb16306ee7 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs @@ -10,7 +10,6 @@ // https://docs.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/how-to-implement-a-lightweight-class-with-auto-implemented-properties using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Models; -using Microsoft.eShopOnContainers.Services.Ordering.API.Extensions; [DataContract] public class CreateOrderCommand @@ -81,5 +80,21 @@ public CreateOrderCommand(List basketItems, string userId, string us CardSecurityNumber = cardSecurityNumber; CardTypeId = cardTypeId; } + + + public record OrderItemDTO + { + public int ProductId { get; init; } + + public string ProductName { get; init; } + + public decimal UnitPrice { get; init; } + + public decimal Discount { get; init; } + + public int Units { get; init; } + + public string PictureUrl { get; init; } + } } diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs index 76d7520469..e445e5de1c 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs @@ -44,7 +44,7 @@ public async Task Handle(CreateOrderCommand message, CancellationToken can order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units); } - _logger.LogInformation("Creating Order - Order: {@Order}", order); + _logger.LogInformation("----- Creating Order - Order: {@Order}", order); _orderRepository.Add(order); @@ -67,6 +67,6 @@ public CreateOrderIdentifiedCommandHandler( protected override bool CreateResultForDuplicateRequest() { - return true; // Ignore duplicate requests for creating order. + return true; // Ignore duplicate requests for creating order. } } diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderDraftCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderDraftCommand.cs index caa6fb2858..4866212d9b 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderDraftCommand.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderDraftCommand.cs @@ -3,6 +3,7 @@ public class CreateOrderDraftCommand : IRequest { + public string BuyerId { get; private set; } public IEnumerable Items { get; private set; } diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderDraftCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderDraftCommandHandler.cs index aaf982c294..32d965fd00 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderDraftCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderDraftCommandHandler.cs @@ -1,12 +1,23 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; -using Microsoft.eShopOnContainers.Services.Ordering.API.Extensions; +using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; // Regular CommandHandler public class CreateOrderDraftCommandHandler : IRequestHandler { + private readonly IOrderRepository _orderRepository; + private readonly IIdentityService _identityService; + private readonly IMediator _mediator; + + // Using DI to inject infrastructure persistence Repositories + public CreateOrderDraftCommandHandler(IMediator mediator, IIdentityService identityService) + { + _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService)); + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + } + public Task Handle(CreateOrderDraftCommand message, CancellationToken cancellationToken) { @@ -21,6 +32,7 @@ public Task Handle(CreateOrderDraftCommand message, CancellationT } } + public record OrderDraftDTO { public IEnumerable OrderItems { get; init; } @@ -42,19 +54,5 @@ public static OrderDraftDTO FromOrder(Order order) Total = order.GetTotal() }; } -} -public record OrderItemDTO -{ - public int ProductId { get; init; } - - public string ProductName { get; init; } - - public decimal UnitPrice { get; init; } - - public decimal Discount { get; init; } - - public int Units { get; init; } - - public string PictureUrl { get; init; } -} +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommandHandler.cs index 0852bdc103..38c8a356d8 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommandHandler.cs @@ -6,7 +6,7 @@ /// /// Type of the command handler that performs the operation if request is not duplicated /// Return value of the inner command handler -public abstract class IdentifiedCommandHandler : IRequestHandler, R> +public class IdentifiedCommandHandler : IRequestHandler, R> where T : IRequest { private readonly IMediator _mediator; @@ -18,17 +18,19 @@ public IdentifiedCommandHandler( IRequestManager requestManager, ILogger> logger) { - ArgumentNullException.ThrowIfNull(logger); _mediator = mediator; _requestManager = requestManager; - _logger = logger; + _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); } /// /// Creates the result value to return if a previous request was found /// /// - protected abstract R CreateResultForDuplicateRequest(); + protected virtual R CreateResultForDuplicateRequest() + { + return default(R); + } /// /// This method handles the command. It just ensures that no other request exists with the same ID, and if this is the case @@ -77,7 +79,7 @@ public async Task Handle(IdentifiedCommand message, CancellationToken c } _logger.LogInformation( - "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", commandName, idProperty, commandId, @@ -87,7 +89,7 @@ public async Task Handle(IdentifiedCommand message, CancellationToken c var result = await _mediator.Send(command, cancellationToken); _logger.LogInformation( - "Command result: {@Result} - {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Command result: {@Result} - {CommandName} - {IdProperty}: {CommandId} ({@Command})", result, commandName, idProperty, @@ -98,7 +100,7 @@ public async Task Handle(IdentifiedCommand message, CancellationToken c } catch { - return default; + return default(R); } } } diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommandHandler.cs index 7a7fdd6667..c8bd37d759 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommandHandler.cs @@ -43,6 +43,6 @@ public SetAwaitingValidationIdentifiedOrderStatusCommandHandler( protected override bool CreateResultForDuplicateRequest() { - return true; // Ignore duplicate requests for processing order. + return true; // Ignore duplicate requests for processing order. } } diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetPaidOrderStatusCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetPaidOrderStatusCommandHandler.cs index 277d5aa152..09520a65fd 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/SetPaidOrderStatusCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetPaidOrderStatusCommandHandler.cs @@ -46,6 +46,6 @@ public SetPaidIdentifiedOrderStatusCommandHandler( protected override bool CreateResultForDuplicateRequest() { - return true; // Ignore duplicate requests for processing order. + return true; // Ignore duplicate requests for processing order. } } diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommandHandler.cs index 89112a7738..df87db63c7 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommandHandler.cs @@ -34,9 +34,9 @@ public async Task Handle(SetStockConfirmedOrderStatusCommand command, Canc // Use for Idempotency in Command process -public class SetStockConfirmedOrderStatusIdentifiedCommandHandler : IdentifiedCommandHandler +public class SetStockConfirmedOrderStatusIdenfifiedCommandHandler : IdentifiedCommandHandler { - public SetStockConfirmedOrderStatusIdentifiedCommandHandler( + public SetStockConfirmedOrderStatusIdenfifiedCommandHandler( IMediator mediator, IRequestManager requestManager, ILogger> logger) @@ -46,6 +46,6 @@ public SetStockConfirmedOrderStatusIdentifiedCommandHandler( protected override bool CreateResultForDuplicateRequest() { - return true; // Ignore duplicate requests for processing order. + return true; // Ignore duplicate requests for processing order. } } diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommandHandler.cs index 72a2a27fe3..fcef729e60 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommandHandler.cs @@ -35,9 +35,9 @@ public async Task Handle(SetStockRejectedOrderStatusCommand command, Cance // Use for Idempotency in Command process -public class SetStockRejectedOrderStatusIdentifiedCommandHandler : IdentifiedCommandHandler +public class SetStockRejectedOrderStatusIdenfifiedCommandHandler : IdentifiedCommandHandler { - public SetStockRejectedOrderStatusIdentifiedCommandHandler( + public SetStockRejectedOrderStatusIdenfifiedCommandHandler( IMediator mediator, IRequestManager requestManager, ILogger> logger) @@ -47,6 +47,6 @@ public SetStockRejectedOrderStatusIdentifiedCommandHandler( protected override bool CreateResultForDuplicateRequest() { - return true; // Ignore duplicate requests for processing order. + return true; // Ignore duplicate requests for processing order. } } diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/ShipOrderCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/ShipOrderCommandHandler.cs index 8887aaadf1..58b09f53b1 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/ShipOrderCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/ShipOrderCommandHandler.cs @@ -43,6 +43,6 @@ public ShipOrderIdentifiedCommandHandler( protected override bool CreateResultForDuplicateRequest() { - return true; // Ignore duplicate requests for processing order. + return true; // Ignore duplicate requests for processing order. } } diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/BuyerAndPaymentMethodVerified/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/BuyerAndPaymentMethodVerified/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs new file mode 100644 index 0000000000..919a78a45b --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/BuyerAndPaymentMethodVerified/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs @@ -0,0 +1,29 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.BuyerAndPaymentMethodVerified; + +public class UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler + : INotificationHandler +{ + private readonly IOrderRepository _orderRepository; + private readonly ILoggerFactory _logger; + + public UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler( + IOrderRepository orderRepository, ILoggerFactory logger) + { + _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + // Domain Logic comment: + // When the Buyer and Buyer's payment method have been created or verified that they existed, + // then we can update the original Order with the BuyerId and PaymentId (foreign keys) + public async Task Handle(BuyerAndPaymentMethodVerifiedDomainEvent buyerPaymentMethodVerifiedEvent, CancellationToken cancellationToken) + { + var orderToUpdate = await _orderRepository.GetAsync(buyerPaymentMethodVerifiedEvent.OrderId); + orderToUpdate.SetBuyerId(buyerPaymentMethodVerifiedEvent.Buyer.Id); + orderToUpdate.SetPaymentId(buyerPaymentMethodVerifiedEvent.Payment.Id); + + _logger.CreateLogger() + .LogTrace("Order with Id: {OrderId} has been successfully updated with a payment method {PaymentMethod} ({Id})", + buyerPaymentMethodVerifiedEvent.OrderId, nameof(buyerPaymentMethodVerifiedEvent.Payment), buyerPaymentMethodVerifiedEvent.Payment.Id); + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelledDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelled/OrderCancelledDomainEventHandler.cs similarity index 50% rename from src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelledDomainEventHandler.cs rename to src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelled/OrderCancelledDomainEventHandler.cs index dc9944c835..dbd8abceb9 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelledDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCancelled/OrderCancelledDomainEventHandler.cs @@ -1,16 +1,18 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers; +using Microsoft.eShopOnContainers.Services.Ordering.Domain.Events; -public partial class OrderCancelledDomainEventHandler +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderCancelled; + +public class OrderCancelledDomainEventHandler : INotificationHandler { private readonly IOrderRepository _orderRepository; private readonly IBuyerRepository _buyerRepository; - private readonly ILogger _logger; + private readonly ILoggerFactory _logger; private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; public OrderCancelledDomainEventHandler( IOrderRepository orderRepository, - ILogger logger, + ILoggerFactory logger, IBuyerRepository buyerRepository, IOrderingIntegrationEventService orderingIntegrationEventService) { @@ -20,14 +22,16 @@ public OrderCancelledDomainEventHandler( _orderingIntegrationEventService = orderingIntegrationEventService; } - public async Task Handle(OrderCancelledDomainEvent domainEvent, CancellationToken cancellationToken) + public async Task Handle(OrderCancelledDomainEvent orderCancelledDomainEvent, CancellationToken cancellationToken) { - OrderingApiTrace.LogOrderStatusUpdated(_logger, domainEvent.Order.Id, nameof(OrderStatus.Cancelled), OrderStatus.Cancelled.Id); + _logger.CreateLogger() + .LogTrace("Order with Id: {OrderId} has been successfully updated to status {Status} ({Id})", + orderCancelledDomainEvent.Order.Id, nameof(OrderStatus.Cancelled), OrderStatus.Cancelled.Id); - var order = await _orderRepository.GetAsync(domainEvent.Order.Id); + var order = await _orderRepository.GetAsync(orderCancelledDomainEvent.Order.Id); var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString()); - var integrationEvent = new OrderStatusChangedToCancelledIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name); - await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent); + var orderStatusChangedToCancelledIntegrationEvent = new OrderStatusChangedToCancelledIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name); + await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToCancelledIntegrationEvent); } } diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderGracePeriodConfirmed/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs similarity index 53% rename from src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs rename to src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderGracePeriodConfirmed/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs index 4b8553f50f..2aa3867be0 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderGracePeriodConfirmed/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs @@ -1,16 +1,15 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers; - +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderGracePeriodConfirmed; + public class OrderStatusChangedToAwaitingValidationDomainEventHandler : INotificationHandler { private readonly IOrderRepository _orderRepository; - private readonly ILogger _logger; + private readonly ILoggerFactory _logger; private readonly IBuyerRepository _buyerRepository; private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; public OrderStatusChangedToAwaitingValidationDomainEventHandler( - IOrderRepository orderRepository, - ILogger logger, + IOrderRepository orderRepository, ILoggerFactory logger, IBuyerRepository buyerRepository, IOrderingIntegrationEventService orderingIntegrationEventService) { @@ -20,17 +19,21 @@ public OrderStatusChangedToAwaitingValidationDomainEventHandler( _orderingIntegrationEventService = orderingIntegrationEventService; } - public async Task Handle(OrderStatusChangedToAwaitingValidationDomainEvent domainEvent, CancellationToken cancellationToken) + public async Task Handle(OrderStatusChangedToAwaitingValidationDomainEvent orderStatusChangedToAwaitingValidationDomainEvent, CancellationToken cancellationToken) { - OrderingApiTrace.LogOrderStatusUpdated(_logger, domainEvent.OrderId, nameof(OrderStatus.AwaitingValidation), OrderStatus.AwaitingValidation.Id); + _logger.CreateLogger() + .LogTrace("Order with Id: {OrderId} has been successfully updated to status {Status} ({Id})", + orderStatusChangedToAwaitingValidationDomainEvent.OrderId, nameof(OrderStatus.AwaitingValidation), OrderStatus.AwaitingValidation.Id); + + var order = await _orderRepository.GetAsync(orderStatusChangedToAwaitingValidationDomainEvent.OrderId); - var order = await _orderRepository.GetAsync(domainEvent.OrderId); var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString()); - var orderStockList = domainEvent.OrderItems + var orderStockList = orderStatusChangedToAwaitingValidationDomainEvent.OrderItems .Select(orderItem => new OrderStockItem(orderItem.ProductId, orderItem.GetUnits())); - var integrationEvent = new OrderStatusChangedToAwaitingValidationIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name, orderStockList); - await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent); + var orderStatusChangedToAwaitingValidationIntegrationEvent = new OrderStatusChangedToAwaitingValidationIntegrationEvent( + order.Id, order.OrderStatus.Name, buyer.Name, orderStockList); + await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToAwaitingValidationIntegrationEvent); } } diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToPaidDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderPaid/OrderStatusChangedToPaidDomainEventHandler.cs similarity index 55% rename from src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToPaidDomainEventHandler.cs rename to src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderPaid/OrderStatusChangedToPaidDomainEventHandler.cs index 761313a4d5..7f0138d71c 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToPaidDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderPaid/OrderStatusChangedToPaidDomainEventHandler.cs @@ -1,17 +1,19 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers; - -public class OrderStatusChangedToPaidDomainEventHandler : INotificationHandler +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderPaid; + +public class OrderStatusChangedToPaidDomainEventHandler + : INotificationHandler { private readonly IOrderRepository _orderRepository; - private readonly ILogger _logger; + private readonly ILoggerFactory _logger; private readonly IBuyerRepository _buyerRepository; private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + public OrderStatusChangedToPaidDomainEventHandler( - IOrderRepository orderRepository, - ILogger logger, + IOrderRepository orderRepository, ILoggerFactory logger, IBuyerRepository buyerRepository, - IOrderingIntegrationEventService orderingIntegrationEventService) + IOrderingIntegrationEventService orderingIntegrationEventService + ) { _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -19,22 +21,24 @@ public OrderStatusChangedToPaidDomainEventHandler( _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService)); } - public async Task Handle(OrderStatusChangedToPaidDomainEvent domainEvent, CancellationToken cancellationToken) + public async Task Handle(OrderStatusChangedToPaidDomainEvent orderStatusChangedToPaidDomainEvent, CancellationToken cancellationToken) { - OrderingApiTrace.LogOrderStatusUpdated(_logger, domainEvent.OrderId, nameof(OrderStatus.Paid), OrderStatus.Paid.Id); + _logger.CreateLogger() + .LogTrace("Order with Id: {OrderId} has been successfully updated to status {Status} ({Id})", + orderStatusChangedToPaidDomainEvent.OrderId, nameof(OrderStatus.Paid), OrderStatus.Paid.Id); - var order = await _orderRepository.GetAsync(domainEvent.OrderId); + var order = await _orderRepository.GetAsync(orderStatusChangedToPaidDomainEvent.OrderId); var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString()); - var orderStockList = domainEvent.OrderItems + var orderStockList = orderStatusChangedToPaidDomainEvent.OrderItems .Select(orderItem => new OrderStockItem(orderItem.ProductId, orderItem.GetUnits())); - var integrationEvent = new OrderStatusChangedToPaidIntegrationEvent( - domainEvent.OrderId, + var orderStatusChangedToPaidIntegrationEvent = new OrderStatusChangedToPaidIntegrationEvent( + orderStatusChangedToPaidDomainEvent.OrderId, order.OrderStatus.Name, buyer.Name, orderStockList); - await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent); + await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToPaidIntegrationEvent); } } diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderShippedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderShipped/OrderShippedDomainEventHandler.cs similarity index 58% rename from src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderShippedDomainEventHandler.cs rename to src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderShipped/OrderShippedDomainEventHandler.cs index 3bdb035e79..445c7e04f2 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderShippedDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderShipped/OrderShippedDomainEventHandler.cs @@ -1,4 +1,4 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers; +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderShipped; public class OrderShippedDomainEventHandler : INotificationHandler @@ -6,11 +6,11 @@ public class OrderShippedDomainEventHandler private readonly IOrderRepository _orderRepository; private readonly IBuyerRepository _buyerRepository; private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; - private readonly ILogger _logger; + private readonly ILoggerFactory _logger; public OrderShippedDomainEventHandler( IOrderRepository orderRepository, - ILogger logger, + ILoggerFactory logger, IBuyerRepository buyerRepository, IOrderingIntegrationEventService orderingIntegrationEventService) { @@ -20,14 +20,16 @@ public OrderShippedDomainEventHandler( _orderingIntegrationEventService = orderingIntegrationEventService; } - public async Task Handle(OrderShippedDomainEvent domainEvent, CancellationToken cancellationToken) + public async Task Handle(OrderShippedDomainEvent orderShippedDomainEvent, CancellationToken cancellationToken) { - OrderingApiTrace.LogOrderStatusUpdated(_logger, domainEvent.Order.Id, nameof(OrderStatus.Shipped), OrderStatus.Shipped.Id); + _logger.CreateLogger() + .LogTrace("Order with Id: {OrderId} has been successfully updated to status {Status} ({Id})", + orderShippedDomainEvent.Order.Id, nameof(OrderStatus.Shipped), OrderStatus.Shipped.Id); - var order = await _orderRepository.GetAsync(domainEvent.Order.Id); + var order = await _orderRepository.GetAsync(orderShippedDomainEvent.Order.Id); var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString()); - var integrationEvent = new OrderStatusChangedToShippedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name); - await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent); + var orderStatusChangedToShippedIntegrationEvent = new OrderStatusChangedToShippedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name); + await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToShippedIntegrationEvent); } } diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/SendEmailToCustomerWhenOrderStartedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/SendEmailToCustomerWhenOrderStartedDomainEventHandler.cs new file mode 100644 index 0000000000..720b656d6f --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/SendEmailToCustomerWhenOrderStartedDomainEventHandler.cs @@ -0,0 +1,15 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderStartedEvent; + +public class SendEmailToCustomerWhenOrderStartedDomainEventHandler +//: IAsyncNotificationHandler +{ + public SendEmailToCustomerWhenOrderStartedDomainEventHandler() + { + + } + + //public async Task Handle(OrderStartedDomainEvent orderNotification) + //{ + // //TBD - Send email logic + //} +} diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs new file mode 100644 index 0000000000..da50372597 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStartedEvent/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs @@ -0,0 +1,55 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderStartedEvent; + +public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler + : INotificationHandler +{ + private readonly ILoggerFactory _logger; + private readonly IBuyerRepository _buyerRepository; + private readonly IIdentityService _identityService; + private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + + public ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler( + ILoggerFactory logger, + IBuyerRepository buyerRepository, + IIdentityService identityService, + IOrderingIntegrationEventService orderingIntegrationEventService) + { + _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository)); + _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService)); + _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task Handle(OrderStartedDomainEvent orderStartedEvent, CancellationToken cancellationToken) + { + var cardTypeId = (orderStartedEvent.CardTypeId != 0) ? orderStartedEvent.CardTypeId : 1; + var buyer = await _buyerRepository.FindAsync(orderStartedEvent.UserId); + bool buyerOriginallyExisted = (buyer == null) ? false : true; + + if (!buyerOriginallyExisted) + { + buyer = new Buyer(orderStartedEvent.UserId, orderStartedEvent.UserName); + } + + buyer.VerifyOrAddPaymentMethod(cardTypeId, + $"Payment Method on {DateTime.UtcNow}", + orderStartedEvent.CardNumber, + orderStartedEvent.CardSecurityNumber, + orderStartedEvent.CardHolderName, + orderStartedEvent.CardExpiration, + orderStartedEvent.Order.Id); + + var buyerUpdated = buyerOriginallyExisted ? + _buyerRepository.Update(buyer) : + _buyerRepository.Add(buyer); + + await _buyerRepository.UnitOfWork + .SaveEntitiesAsync(cancellationToken); + + var orderStatusChangedToSubmittedIntegrationEvent = new OrderStatusChangedToSubmittedIntegrationEvent(orderStartedEvent.Order.Id, orderStartedEvent.Order.OrderStatus.Name, buyer.Name); + await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToSubmittedIntegrationEvent); + _logger.CreateLogger() + .LogTrace("Buyer {BuyerId} and related payment method were validated or updated for orderId: {OrderId}.", + buyerUpdated.Id, orderStartedEvent.Order.Id); + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToStockConfirmedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs similarity index 58% rename from src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToStockConfirmedDomainEventHandler.cs rename to src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs index 26aae4736d..142b6449dd 100644 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToStockConfirmedDomainEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderStockConfirmed/OrderStatusChangedToStockConfirmedDomainEventHandler.cs @@ -1,17 +1,17 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers; +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderStockConfirmed; public class OrderStatusChangedToStockConfirmedDomainEventHandler : INotificationHandler { private readonly IOrderRepository _orderRepository; private readonly IBuyerRepository _buyerRepository; - private readonly ILogger _logger; + private readonly ILoggerFactory _logger; private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; public OrderStatusChangedToStockConfirmedDomainEventHandler( IOrderRepository orderRepository, IBuyerRepository buyerRepository, - ILogger logger, + ILoggerFactory logger, IOrderingIntegrationEventService orderingIntegrationEventService) { _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); @@ -20,14 +20,16 @@ public OrderStatusChangedToStockConfirmedDomainEventHandler( _orderingIntegrationEventService = orderingIntegrationEventService; } - public async Task Handle(OrderStatusChangedToStockConfirmedDomainEvent domainEvent, CancellationToken cancellationToken) + public async Task Handle(OrderStatusChangedToStockConfirmedDomainEvent orderStatusChangedToStockConfirmedDomainEvent, CancellationToken cancellationToken) { - OrderingApiTrace.LogOrderStatusUpdated(_logger, domainEvent.OrderId, nameof(OrderStatus.StockConfirmed), OrderStatus.StockConfirmed.Id); + _logger.CreateLogger() + .LogTrace("Order with Id: {OrderId} has been successfully updated to status {Status} ({Id})", + orderStatusChangedToStockConfirmedDomainEvent.OrderId, nameof(OrderStatus.StockConfirmed), OrderStatus.StockConfirmed.Id); - var order = await _orderRepository.GetAsync(domainEvent.OrderId); + var order = await _orderRepository.GetAsync(orderStatusChangedToStockConfirmedDomainEvent.OrderId); var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString()); - var integrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name); - await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent); + var orderStatusChangedToStockConfirmedIntegrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name); + await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStatusChangedToStockConfirmedIntegrationEvent); } } diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs deleted file mode 100644 index 5c14ac65c3..0000000000 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers; - -public class UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler : INotificationHandler -{ - private readonly IOrderRepository _orderRepository; - private readonly ILogger _logger; - - public UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler( - IOrderRepository orderRepository, - ILogger logger) - { - _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - // Domain Logic comment: - // When the Buyer and Buyer's payment method have been created or verified that they existed, - // then we can update the original Order with the BuyerId and PaymentId (foreign keys) - public async Task Handle(BuyerAndPaymentMethodVerifiedDomainEvent domainEvent, CancellationToken cancellationToken) - { - var orderToUpdate = await _orderRepository.GetAsync(domainEvent.OrderId); - orderToUpdate.SetBuyerId(domainEvent.Buyer.Id); - orderToUpdate.SetPaymentId(domainEvent.Payment.Id); - OrderingApiTrace.LogOrderPaymentMethodUpdated(_logger, domainEvent.OrderId, nameof(domainEvent.Payment), domainEvent.Payment.Id); - } -} diff --git a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs deleted file mode 100644 index a613e3794c..0000000000 --- a/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers; - -public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler - : INotificationHandler -{ - private readonly ILogger _logger; - private readonly IBuyerRepository _buyerRepository; - private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; - - public ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler( - ILogger logger, - IBuyerRepository buyerRepository, - IOrderingIntegrationEventService orderingIntegrationEventService) - { - _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository)); - _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public async Task Handle(OrderStartedDomainEvent domainEvent, CancellationToken cancellationToken) - { - var cardTypeId = domainEvent.CardTypeId != 0 ? domainEvent.CardTypeId : 1; - var buyer = await _buyerRepository.FindAsync(domainEvent.UserId); - var buyerExisted = buyer is not null; - - if (!buyerExisted) - { - buyer = new Buyer(domainEvent.UserId, domainEvent.UserName); - } - - buyer.VerifyOrAddPaymentMethod(cardTypeId, - $"Payment Method on {DateTime.UtcNow}", - domainEvent.CardNumber, - domainEvent.CardSecurityNumber, - domainEvent.CardHolderName, - domainEvent.CardExpiration, - domainEvent.Order.Id); - - var buyerUpdated = buyerExisted ? - _buyerRepository.Update(buyer) : - _buyerRepository.Add(buyer); - - await _buyerRepository.UnitOfWork - .SaveEntitiesAsync(cancellationToken); - - var integrationEvent = new OrderStatusChangedToSubmittedIntegrationEvent(domainEvent.Order.Id, domainEvent.Order.OrderStatus.Name, buyer.Name); - await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent); - OrderingApiTrace.LogOrderBuyerAndPaymentValidatedOrUpdated(_logger, buyerUpdated.Id, domainEvent.Order.Id); - } -} diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs index 3639df4362..55a8a39b93 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs @@ -10,7 +10,7 @@ public GracePeriodConfirmedIntegrationEventHandler( ILogger logger) { _mediator = mediator; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); } /// @@ -23,14 +23,14 @@ public GracePeriodConfirmedIntegrationEventHandler( /// public async Task Handle(GracePeriodConfirmedIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); var command = new SetAwaitingValidationOrderStatusCommand(@event.OrderId); _logger.LogInformation( - "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", command.GetGenericTypeName(), nameof(command.OrderNumber), command.OrderNumber, diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs index 2596c759bf..4170ac18f6 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs @@ -16,14 +16,14 @@ public OrderPaymentFailedIntegrationEventHandler( public async Task Handle(OrderPaymentFailedIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); var command = new CancelOrderCommand(@event.OrderId); _logger.LogInformation( - "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", command.GetGenericTypeName(), nameof(command.OrderNumber), command.OrderNumber, diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSucceededIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSucceededIntegrationEventHandler.cs index bbe7186b50..4bd6304d08 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSucceededIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSucceededIntegrationEventHandler.cs @@ -1,5 +1,5 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.EventHandling; - + public class OrderPaymentSucceededIntegrationEventHandler : IIntegrationEventHandler { @@ -16,14 +16,14 @@ public OrderPaymentSucceededIntegrationEventHandler( public async Task Handle(OrderPaymentSucceededIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); var command = new SetPaidOrderStatusCommand(@event.OrderId); _logger.LogInformation( - "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", command.GetGenericTypeName(), nameof(command.OrderNumber), command.OrderNumber, diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs index 2b35ef3c49..0c9557a5b1 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs @@ -1,4 +1,4 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.EventHandling; +namespace Ordering.API.Application.IntegrationEvents.EventHandling; public class OrderStockConfirmedIntegrationEventHandler : IIntegrationEventHandler @@ -16,14 +16,14 @@ public OrderStockConfirmedIntegrationEventHandler( public async Task Handle(OrderStockConfirmedIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); var command = new SetStockConfirmedOrderStatusCommand(@event.OrderId); _logger.LogInformation( - "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", command.GetGenericTypeName(), nameof(command.OrderNumber), command.OrderNumber, diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs index a6bafa568f..4fa61b9b59 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs @@ -9,14 +9,14 @@ public OrderStockRejectedIntegrationEventHandler( ILogger logger) { _mediator = mediator; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); } public async Task Handle(OrderStockRejectedIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); var orderStockRejectedItems = @event.OrderStockItems .FindAll(c => !c.HasStock) @@ -26,7 +26,7 @@ public async Task Handle(OrderStockRejectedIntegrationEvent @event) var command = new SetStockRejectedOrderStatusCommand(@event.OrderId, orderStockRejectedItems); _logger.LogInformation( - "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", command.GetGenericTypeName(), nameof(command.OrderNumber), command.OrderNumber, diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs index e2d5584d80..a5a15c06c4 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs @@ -24,15 +24,15 @@ public UserCheckoutAcceptedIntegrationEventHandler( /// public async Task Handle(UserCheckoutAcceptedIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); var result = false; if (@event.RequestId != Guid.Empty) { - using (_logger.BeginScope(new List> { new("IdentifiedCommandId", @event.RequestId) })) + using (LogContext.PushProperty("IdentifiedCommandId", @event.RequestId)) { var createOrderCommand = new CreateOrderCommand(@event.Basket.Items, @event.UserId, @event.UserName, @event.City, @event.Street, @event.State, @event.Country, @event.ZipCode, @@ -42,7 +42,7 @@ public async Task Handle(UserCheckoutAcceptedIntegrationEvent @event) var requestCreateOrder = new IdentifiedCommand(createOrderCommand, @event.RequestId); _logger.LogInformation( - "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", requestCreateOrder.GetGenericTypeName(), nameof(requestCreateOrder.Id), requestCreateOrder.Id, @@ -52,7 +52,7 @@ public async Task Handle(UserCheckoutAcceptedIntegrationEvent @event) if (result) { - _logger.LogInformation("CreateOrderCommand suceeded - RequestId: {RequestId}", @event.RequestId); + _logger.LogInformation("----- CreateOrderCommand suceeded - RequestId: {RequestId}", @event.RequestId); } else { diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/GracePeriodConfirmedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/GracePeriodConfirmedIntegrationEvent.cs index 8dfa6d08fc..3e653f8df5 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/GracePeriodConfirmedIntegrationEvent.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/GracePeriodConfirmedIntegrationEvent.cs @@ -1,4 +1,4 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.Events; +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.Events; public record GracePeriodConfirmedIntegrationEvent : IntegrationEvent { diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs index 5a3e0342a2..1f7ef35e2b 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs @@ -1,5 +1,5 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.Events; - + public record OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent { public int OrderId { get; } diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs index 990825e4f9..25e1acf638 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs @@ -1,4 +1,4 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.Events; +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.Events; public record OrderStockRejectedIntegrationEvent : IntegrationEvent { diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs index 97cc43c7ba..811be0ec46 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/UserCheckoutAcceptedIntegrationEvent.cs @@ -1,15 +1,15 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.Events; public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent -{ +{ public string UserId { get; } - + public string UserName { get; } - + public string City { get; set; } - + public string Street { get; set; } - + public string State { get; set; } public string Country { get; set; } diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs index 29e9a7ab4b..e2545cae5c 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs @@ -27,7 +27,7 @@ public async Task PublishEventsThroughEventBusAsync(Guid transactionId) foreach (var logEvt in pendingLogEvents) { - _logger.LogInformation("Publishing integration event: {IntegrationEventId} - ({@IntegrationEvent})", logEvt.EventId, logEvt.IntegrationEvent); + _logger.LogInformation("----- Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", logEvt.EventId, Program.AppName, logEvt.IntegrationEvent); try { @@ -37,7 +37,7 @@ public async Task PublishEventsThroughEventBusAsync(Guid transactionId) } catch (Exception ex) { - _logger.LogError(ex, "Error publishing integration event: {IntegrationEventId}", logEvt.EventId); + _logger.LogError(ex, "ERROR publishing integration event: {IntegrationEventId} from {AppName}", logEvt.EventId, Program.AppName); await _eventLogService.MarkEventAsFailedAsync(logEvt.EventId); } @@ -46,7 +46,7 @@ public async Task PublishEventsThroughEventBusAsync(Guid transactionId) public async Task AddAndSaveEventAsync(IntegrationEvent evt) { - _logger.LogInformation("Enqueuing integration event {IntegrationEventId} to repository ({@IntegrationEvent})", evt.Id, evt); + _logger.LogInformation("----- Enqueuing integration event {IntegrationEventId} to repository ({@IntegrationEvent})", evt.Id, evt); await _eventLogService.SaveEventAsync(evt, _orderingContext.GetCurrentTransaction()); } diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs index 8f6f2ee922..860c587c40 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs @@ -1,5 +1,5 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries; - + public class OrderQueries : IOrderQueries { diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/CancelOrderCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/CancelOrderCommandValidator.cs index 80fbcfcc4c..7d901b1fe1 100644 --- a/src/Services/Ordering/Ordering.API/Application/Validations/CancelOrderCommandValidator.cs +++ b/src/Services/Ordering/Ordering.API/Application/Validations/CancelOrderCommandValidator.cs @@ -6,6 +6,6 @@ public CancelOrderCommandValidator(ILogger logger) { RuleFor(order => order.OrderNumber).NotEmpty().WithMessage("No orderId found"); - logger.LogTrace("INSTANCE CREATED - {ClassName}", GetType().Name); + logger.LogTrace("----- INSTANCE CREATED - {ClassName}", GetType().Name); } } diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs index 0e7db99ec3..eb61082e77 100644 --- a/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs +++ b/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs @@ -1,4 +1,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Validations; + +using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand; + public class CreateOrderCommandValidator : AbstractValidator { public CreateOrderCommandValidator(ILogger logger) @@ -15,7 +18,7 @@ public CreateOrderCommandValidator(ILogger logger) RuleFor(command => command.CardTypeId).NotEmpty(); RuleFor(command => command.OrderItems).Must(ContainOrderItems).WithMessage("No order items found"); - logger.LogTrace("INSTANCE CREATED - {ClassName}", GetType().Name); + logger.LogTrace("----- INSTANCE CREATED - {ClassName}", GetType().Name); } private bool BeValidExpirationDate(DateTime dateTime) diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/IdentifiedCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/IdentifiedCommandValidator.cs index 4d98fc14c3..bde2d771fa 100644 --- a/src/Services/Ordering/Ordering.API/Application/Validations/IdentifiedCommandValidator.cs +++ b/src/Services/Ordering/Ordering.API/Application/Validations/IdentifiedCommandValidator.cs @@ -6,6 +6,6 @@ public IdentifiedCommandValidator(ILogger logger) { RuleFor(command => command.Id).NotEmpty(); - logger.LogTrace("INSTANCE CREATED - {ClassName}", GetType().Name); + logger.LogTrace("----- INSTANCE CREATED - {ClassName}", GetType().Name); } } diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/ShipOrderCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/ShipOrderCommandValidator.cs index dea55a4e33..0341c6028c 100644 --- a/src/Services/Ordering/Ordering.API/Application/Validations/ShipOrderCommandValidator.cs +++ b/src/Services/Ordering/Ordering.API/Application/Validations/ShipOrderCommandValidator.cs @@ -6,6 +6,6 @@ public ShipOrderCommandValidator(ILogger logger) { RuleFor(order => order.OrderNumber).NotEmpty().WithMessage("No orderId found"); - logger.LogTrace("INSTANCE CREATED - {ClassName}", GetType().Name); + logger.LogTrace("----- INSTANCE CREATED - {ClassName}", GetType().Name); } } diff --git a/src/Services/Ordering/Ordering.API/Controllers/HomeController.cs b/src/Services/Ordering/Ordering.API/Controllers/HomeController.cs new file mode 100644 index 0000000000..601b7ab6d0 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Controllers/HomeController.cs @@ -0,0 +1,10 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers; + +public class HomeController : Controller +{ + // GET: // + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } +} diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs index df7572bb7f..2cfd4063fb 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -1,7 +1,9 @@ -using CardType = Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries.CardType; -using Order = Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries.Order; +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers; -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions; +using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; +using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries; +using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services; [Route("api/v1/[controller]")] [Authorize] @@ -27,8 +29,8 @@ public OrdersController( [Route("cancel")] [HttpPut] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] public async Task CancelOrderAsync([FromBody] CancelOrderCommand command, [FromHeader(Name = "x-requestid")] string requestId) { bool commandResult = false; @@ -38,7 +40,7 @@ public async Task CancelOrderAsync([FromBody] CancelOrderCommand var requestCancelOrder = new IdentifiedCommand(command, guid); _logger.LogInformation( - "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", requestCancelOrder.GetGenericTypeName(), nameof(requestCancelOrder.Command.OrderNumber), requestCancelOrder.Command.OrderNumber, @@ -57,8 +59,8 @@ public async Task CancelOrderAsync([FromBody] CancelOrderCommand [Route("ship")] [HttpPut] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] public async Task ShipOrderAsync([FromBody] ShipOrderCommand command, [FromHeader(Name = "x-requestid")] string requestId) { bool commandResult = false; @@ -68,7 +70,7 @@ public async Task ShipOrderAsync([FromBody] ShipOrderCommand comm var requestShipOrder = new IdentifiedCommand(command, guid); _logger.LogInformation( - "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", requestShipOrder.GetGenericTypeName(), nameof(requestShipOrder.Command.OrderNumber), requestShipOrder.Command.OrderNumber, @@ -87,9 +89,9 @@ public async Task ShipOrderAsync([FromBody] ShipOrderCommand comm [Route("{orderId:int}")] [HttpGet] - [ProducesResponseType(typeof(Order), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetOrderAsync(int orderId) + [ProducesResponseType(typeof(Order), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task GetOrderAsync(int orderId) { try { @@ -97,7 +99,7 @@ public async Task> GetOrderAsync(int orderId) //var order customer = await _mediator.Send(new GetOrderByIdQuery(orderId)); var order = await _orderQueries.GetOrderAsync(orderId); - return order; + return Ok(order); } catch { @@ -106,7 +108,7 @@ public async Task> GetOrderAsync(int orderId) } [HttpGet] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] public async Task>> GetOrdersAsync() { var userid = _identityService.GetUserIdentity(); @@ -117,7 +119,7 @@ public async Task>> GetOrdersAsync() [Route("cardtypes")] [HttpGet] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] public async Task>> GetCardTypesAsync() { var cardTypes = await _orderQueries.GetCardTypesAsync(); @@ -130,7 +132,7 @@ public async Task>> GetCardTypesAsync() public async Task> CreateOrderDraftFromBasketDataAsync([FromBody] CreateOrderDraftCommand createOrderDraftCommand) { _logger.LogInformation( - "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", createOrderDraftCommand.GetGenericTypeName(), nameof(createOrderDraftCommand.BuyerId), createOrderDraftCommand.BuyerId, diff --git a/src/Services/Ordering/Ordering.API/Dockerfile b/src/Services/Ordering/Ordering.API/Dockerfile index bb32682388..650e54cc61 100644 --- a/src/Services/Ordering/Ordering.API/Dockerfile +++ b/src/Services/Ordering/Ordering.API/Dockerfile @@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" @@ -32,7 +33,6 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" -COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" @@ -42,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" COPY "docker-compose.dcproj" "docker-compose.dcproj" -COPY "Directory.Packages.props" "Directory.Packages.props" COPY "NuGet.config" "NuGet.config" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" diff --git a/src/Services/Ordering/Ordering.API/Extensions/BasketItemExtensions.cs b/src/Services/Ordering/Ordering.API/Extensions/BasketItemExtensions.cs index 4e8ab1b15f..0f6122a130 100644 --- a/src/Services/Ordering/Ordering.API/Extensions/BasketItemExtensions.cs +++ b/src/Services/Ordering/Ordering.API/Extensions/BasketItemExtensions.cs @@ -1,4 +1,7 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Extensions; +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Models; + +using System.Collections.Generic; +using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand; public static class BasketItemExtensions { diff --git a/src/Services/Ordering/Ordering.API/Extensions/Extensions.cs b/src/Services/Ordering/Ordering.API/Extensions/Extensions.cs deleted file mode 100644 index 52de0f4cf7..0000000000 --- a/src/Services/Ordering/Ordering.API/Extensions/Extensions.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; - -internal static class Extensions -{ - public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) - { - var hcBuilder = services.AddHealthChecks(); - - hcBuilder - .AddSqlServer(_ => - configuration.GetRequiredConnectionString("OrderingDB"), - name: "OrderingDB-check", - tags: new string[] { "ready" }); - - return services; - } - - public static IServiceCollection AddDbContexts(this IServiceCollection services, IConfiguration configuration) - { - static void ConfigureSqlOptions(SqlServerDbContextOptionsBuilder sqlOptions) - { - sqlOptions.MigrationsAssembly(typeof(Program).Assembly.FullName); - - // Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency - - sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); - }; - - services.AddDbContext(options => - { - options.UseSqlServer(configuration.GetRequiredConnectionString("OrderingDB"), ConfigureSqlOptions); - }); - - services.AddDbContext(options => - { - options.UseSqlServer(configuration.GetRequiredConnectionString("OrderingDB"), ConfigureSqlOptions); - }); - - return services; - } - - public static IServiceCollection AddIntegrationServices(this IServiceCollection services) - { - services.AddTransient(); - services.AddTransient>( - sp => (DbConnection c) => new IntegrationEventLogService(c)); - - services.AddTransient(); - - return services; - } - - public static IServiceCollection AddApplicationOptions(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration); - services.Configure(options => - { - options.InvalidModelStateResponseFactory = context => - { - var problemDetails = new ValidationProblemDetails(context.ModelState) - { - Instance = context.HttpContext.Request.Path, - Status = StatusCodes.Status400BadRequest, - Detail = "Please refer to the errors property for additional details." - }; - - return new BadRequestObjectResult(problemDetails) - { - ContentTypes = { "application/problem+json", "application/problem+xml" } - }; - }; - }); - - return services; - } -} diff --git a/src/Services/Ordering/Ordering.API/Extensions/LinqSelectExtensions.cs b/src/Services/Ordering/Ordering.API/Extensions/LinqSelectExtensions.cs index a0bd830d72..c36385d6cb 100644 --- a/src/Services/Ordering/Ordering.API/Extensions/LinqSelectExtensions.cs +++ b/src/Services/Ordering/Ordering.API/Extensions/LinqSelectExtensions.cs @@ -13,7 +13,7 @@ public static IEnumerable> SelectTry(element, default, ex); + returnedValue = new SelectTryResult(element, default(TResult), ex); } yield return returnedValue; } diff --git a/src/Services/Ordering/Ordering.API/Extensions/OrderingApiTrace.cs b/src/Services/Ordering/Ordering.API/Extensions/OrderingApiTrace.cs deleted file mode 100644 index dfd2a78e56..0000000000 --- a/src/Services/Ordering/Ordering.API/Extensions/OrderingApiTrace.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers; - -internal static partial class OrderingApiTrace -{ - [LoggerMessage(EventId = 1, EventName = "OrderStatusUpdated", Level = LogLevel.Trace, Message = "Order with Id: {OrderId} has been successfully updated to status {Status} ({Id})")] - public static partial void LogOrderStatusUpdated(ILogger logger, int orderId, string status, int id); - - [LoggerMessage(EventId = 2, EventName = "PaymentMethodUpdated", Level = LogLevel.Trace, Message = "Order with Id: {OrderId} has been successfully updated with a payment method {PaymentMethod} ({Id})")] - public static partial void LogOrderPaymentMethodUpdated(ILogger logger, int orderId, string paymentMethod, int id); - - [LoggerMessage(EventId = 3, EventName = "BuyerAndPaymentValidatedOrUpdated", Level = LogLevel.Trace, Message = "Buyer {BuyerId} and related payment method were validated or updated for order Id: {OrderId}.")] - public static partial void LogOrderBuyerAndPaymentValidatedOrUpdated(ILogger logger, int buyerId, int orderId); -} diff --git a/src/Services/Ordering/Ordering.API/GlobalUsings.cs b/src/Services/Ordering/Ordering.API/GlobalUsings.cs index fc25908445..434ef8273c 100644 --- a/src/Services/Ordering/Ordering.API/GlobalUsings.cs +++ b/src/Services/Ordering/Ordering.API/GlobalUsings.cs @@ -1,45 +1,87 @@ -global using System.Data.Common; -global using System.Data.SqlClient; -global using System.Runtime.Serialization; +global using ApiModels = Microsoft.eShopOnContainers.Services.Ordering.API.Application.Models; +global using AppCommand = Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; +global using Autofac.Extensions.DependencyInjection; +global using Autofac; +global using Azure.Core; global using Azure.Identity; global using Dapper; global using FluentValidation; global using Google.Protobuf.Collections; global using Grpc.Core; +global using HealthChecks.UI.Client; global using MediatR; global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Diagnostics.HealthChecks; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc.Authorization; +global using Microsoft.AspNetCore.Mvc.Filters; global using Microsoft.AspNetCore.Mvc; -global using Microsoft.EntityFrameworkCore; +global using Microsoft.AspNetCore.Server.Kestrel.Core; +global using Microsoft.AspNetCore; +global using Azure.Messaging.ServiceBus; global using Microsoft.EntityFrameworkCore.Design; +global using Microsoft.EntityFrameworkCore; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions; -global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; -global using Microsoft.eShopOnContainers.Services.Ordering.API; +global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Behaviors; global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; -global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderStartedEvent; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers.OrderStockConfirmed; global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.EventHandling; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.AutofacModules; global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.Events; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents; global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Models; global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries; global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Validations; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Controllers; global using Microsoft.eShopOnContainers.Services.Ordering.API.Extensions; -global using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure; +global using GrpcOrdering; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.ActionResults; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Filters; global using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure; +global using Microsoft.eShopOnContainers.Services.Ordering.API; global using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; global using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Events; global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Exceptions; global using Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork; -global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories; +global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Diagnostics.HealthChecks; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; -global using Polly; +global using Microsoft.OpenApi.Models; global using Polly.Retry; -global using Services.Common; +global using Polly; +global using RabbitMQ.Client; +global using Serilog.Context; +global using Serilog; global using Swashbuckle.AspNetCore.SwaggerGen; -global using AppCommand = Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; -global using ApiModels = Microsoft.eShopOnContainers.Services.Ordering.API.Application.Models; +global using System.Collections.Generic; +global using System.Data.Common; +global using System.Data.SqlClient; +global using System.IdentityModel.Tokens.Jwt; +global using System.IO; +global using System.Linq; +global using System.Net; +global using System.Reflection; +global using System.Runtime.Serialization; +global using System.Threading.Tasks; +global using System.Threading; +global using System; +global using System.Collections.Generic; +global using Microsoft.IdentityModel.Tokens; diff --git a/src/Services/Ordering/Ordering.API/Grpc/OrderingService.cs b/src/Services/Ordering/Ordering.API/Grpc/OrderingService.cs index 1a2d17537e..969a09d465 100644 --- a/src/Services/Ordering/Ordering.API/Grpc/OrderingService.cs +++ b/src/Services/Ordering/Ordering.API/Grpc/OrderingService.cs @@ -1,8 +1,4 @@ -using GrpcOrdering; -using BasketItem = GrpcOrdering.BasketItem; -using CreateOrderDraftCommand = GrpcOrdering.CreateOrderDraftCommand; -using OrderDraftDTO = GrpcOrdering.OrderDraftDTO; -using OrderItemDTO = GrpcOrdering.OrderItemDTO; +namespace GrpcOrdering; public class OrderingService : OrderingGrpc.OrderingGrpcBase { @@ -19,7 +15,7 @@ public override async Task CreateOrderDraftFromBasketData(CreateO { _logger.LogInformation("Begin grpc call from method {Method} for ordering get order draft {CreateOrderDraftCommand}", context.Method, createOrderDraftCommand); _logger.LogTrace( - "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", + "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", createOrderDraftCommand.GetGenericTypeName(), nameof(createOrderDraftCommand.BuyerId), createOrderDraftCommand.BuyerId, @@ -29,6 +25,7 @@ public override async Task CreateOrderDraftFromBasketData(CreateO createOrderDraftCommand.BuyerId, this.MapBasketItems(createOrderDraftCommand.Items)); + var data = await _mediator.Send(command); if (data != null) diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs b/src/Services/Ordering/Ordering.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs new file mode 100644 index 0000000000..d886bf3714 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs @@ -0,0 +1,10 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.ActionResults; + +public class InternalServerErrorObjectResult : ObjectResult +{ + public InternalServerErrorObjectResult(object error) + : base(error) + { + StatusCode = StatusCodes.Status500InternalServerError; + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Auth/AuthorizationHeaderParameterOperationFilter.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Auth/AuthorizationHeaderParameterOperationFilter.cs new file mode 100644 index 0000000000..dbf8cf97c8 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Auth/AuthorizationHeaderParameterOperationFilter.cs @@ -0,0 +1,27 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Auth; + +public class AuthorizationHeaderParameterOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var filterPipeline = context.ApiDescription.ActionDescriptor.FilterDescriptors; + var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter); + var allowAnonymous = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter); + + if (isAuthorized && !allowAnonymous) + { + if (operation.Parameters == null) + operation.Parameters = new List(); + + + operation.Parameters.Add(new OpenApiParameter + { + Name = "Authorization", + In = ParameterLocation.Header, + Description = "access token", + Required = true + }); + } + } + +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/ApplicationModule.cs b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/ApplicationModule.cs new file mode 100644 index 0000000000..1ab7750422 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/ApplicationModule.cs @@ -0,0 +1,38 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.AutofacModules; + +public class ApplicationModule + : Autofac.Module +{ + + public string QueriesConnectionString { get; } + + public ApplicationModule(string qconstr) + { + QueriesConnectionString = qconstr; + + } + + protected override void Load(ContainerBuilder builder) + { + + builder.Register(c => new OrderQueries(QueriesConnectionString)) + .As() + .InstancePerLifetimeScope(); + + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + + builder.RegisterAssemblyTypes(typeof(CreateOrderCommandHandler).GetTypeInfo().Assembly) + .AsClosedTypesOf(typeof(IIntegrationEventHandler<>)); + + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs new file mode 100644 index 0000000000..2d59f17307 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/AutofacModules/MediatorModule.cs @@ -0,0 +1,36 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.AutofacModules; + +public class MediatorModule : Autofac.Module +{ + protected override void Load(ContainerBuilder builder) + { + builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly) + .AsImplementedInterfaces(); + + // Register all the Command classes (they implement IRequestHandler) in assembly holding the Commands + builder.RegisterAssemblyTypes(typeof(CreateOrderCommand).GetTypeInfo().Assembly) + .AsClosedTypesOf(typeof(IRequestHandler<,>)); + + // Register the DomainEventHandler classes (they implement INotificationHandler<>) in assembly holding the Domain Events + builder.RegisterAssemblyTypes(typeof(ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler).GetTypeInfo().Assembly) + .AsClosedTypesOf(typeof(INotificationHandler<>)); + + // Register the Command's Validators (Validators based on FluentValidation library) + builder + .RegisterAssemblyTypes(typeof(CreateOrderCommandValidator).GetTypeInfo().Assembly) + .Where(t => t.IsClosedTypeOf(typeof(IValidator<>))) + .AsImplementedInterfaces(); + + + builder.Register(context => + { + var componentContext = context.Resolve(); + return t => { object o; return componentContext.TryResolve(t, out o) ? o : null; }; + }); + + builder.RegisterGeneric(typeof(LoggingBehavior<,>)).As(typeof(IPipelineBehavior<,>)); + builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).As(typeof(IPipelineBehavior<,>)); + builder.RegisterGeneric(typeof(TransactionBehaviour<,>)).As(typeof(IPipelineBehavior<,>)); + + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Filters/AuthorizeCheckOperationFilter.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Filters/AuthorizeCheckOperationFilter.cs new file mode 100644 index 0000000000..b4689bf9a8 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Filters/AuthorizeCheckOperationFilter.cs @@ -0,0 +1,29 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Filters; + +public class AuthorizeCheckOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // Check for authorize attribute + var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType().Any() || + context.MethodInfo.GetCustomAttributes(true).OfType().Any(); + + if (!hasAuthorize) return; + + operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); + + var oAuthScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } + }; + + operation.Security = new List + { + new() + { + [ oAuthScheme ] = new [] { "orderingapi" } + } + }; + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs new file mode 100644 index 0000000000..eef48c502b --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -0,0 +1,60 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Filters; + +public class HttpGlobalExceptionFilter : IExceptionFilter +{ + private readonly IWebHostEnvironment env; + private readonly ILogger logger; + + public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger logger) + { + this.env = env; + this.logger = logger; + } + + public void OnException(ExceptionContext context) + { + logger.LogError(new EventId(context.Exception.HResult), + context.Exception, + context.Exception.Message); + + if (context.Exception.GetType() == typeof(OrderingDomainException)) + { + var problemDetails = new ValidationProblemDetails() + { + Instance = context.HttpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." + }; + + problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() }); + + context.Result = new BadRequestObjectResult(problemDetails); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + else + { + var json = new JsonErrorResponse + { + Messages = new[] { "An error occur.Try it again." } + }; + + if (env.IsDevelopment()) + { + json.DeveloperMessage = context.Exception; + } + + // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 + // It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information + context.Result = new InternalServerErrorObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + context.ExceptionHandled = true; + } + + private class JsonErrorResponse + { + public string[] Messages { get; set; } + + public object DeveloperMessage { get; set; } + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/20170330131634_IntegrationEventInitial.cs b/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/20170330131634_IntegrationEventInitial.cs index 2fc74e7a61..a6bbd8765d 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/20170330131634_IntegrationEventInitial.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/20170330131634_IntegrationEventInitial.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using System; namespace Ordering.API.Infrastructure.IntegrationEventMigrations { diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/IntegrationEventLogContextDesignTimeFactory.cs b/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/IntegrationEventLogContextDesignTimeFactory.cs index 52fe8f27a8..3841e3a205 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/IntegrationEventLogContextDesignTimeFactory.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/IntegrationEventMigrations/IntegrationEventLogContextDesignTimeFactory.cs @@ -1,4 +1,8 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.IntegrationEventMigrations +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; + +namespace Catalog.API.Infrastructure.IntegrationEventMigrations { public class IntegrationEventLogContextDesignTimeFactory : IDesignTimeDbContextFactory { diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170208181933_Initial.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170208181933_Initial.cs index 7d9a3c8401..a489f0e28c 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170208181933_Initial.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170208181933_Initial.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; +using System; namespace Ordering.API.Migrations { diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.cs index 157061fcf1..e0fb2dedf4 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Migrations/20170303085729_RequestsTable.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using System; namespace Ordering.API.Migrations { diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs b/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs index d7f8663fd3..3c0aca0092 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/OrderingContextSeed.cs @@ -59,7 +59,7 @@ private IEnumerable GetCardTypesFromFile(string contentRootPath, ILogg } catch (Exception ex) { - log.LogError(ex, "Error reading CSV headers"); + log.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return GetPredefinedCardTypes(); } @@ -67,13 +67,13 @@ private IEnumerable GetCardTypesFromFile(string contentRootPath, ILogg return File.ReadAllLines(csvFileCardTypes) .Skip(1) // skip header column .SelectTry(x => CreateCardType(x, ref id)) - .OnCaughtException(ex => { log.LogError(ex, "Error creating card while seeding database"); return null; }) + .OnCaughtException(ex => { log.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) .Where(x => x != null); } private CardType CreateCardType(string value, ref int id) { - if (string.IsNullOrEmpty(value)) + if (String.IsNullOrEmpty(value)) { throw new Exception("Orderstatus is null or empty"); } @@ -103,7 +103,7 @@ private IEnumerable GetOrderStatusFromFile(string contentRootPath, } catch (Exception ex) { - log.LogError(ex, "Error reading CSV headers"); + log.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return GetPredefinedOrderStatus(); } @@ -111,13 +111,13 @@ private IEnumerable GetOrderStatusFromFile(string contentRootPath, return File.ReadAllLines(csvFileOrderStatus) .Skip(1) // skip header row .SelectTry(x => CreateOrderStatus(x, ref id)) - .OnCaughtException(ex => { log.LogError(ex, "Error creating order status while seeding database"); return null; }) + .OnCaughtException(ex => { log.LogError(ex, "EXCEPTION ERROR: {Message}", ex.Message); return null; }) .Where(x => x != null); } private OrderStatus CreateOrderStatus(string value, ref int id) { - if (string.IsNullOrEmpty(value)) + if (String.IsNullOrEmpty(value)) { throw new Exception("Orderstatus is null or empty"); } @@ -144,7 +144,7 @@ private string[] GetHeaders(string[] requiredHeaders, string csvfile) if (csvheaders.Count() != requiredHeaders.Count()) { - throw new Exception($"requiredHeader count '{requiredHeaders.Count()}' is different then read header '{csvheaders.Count()}'"); + throw new Exception($"requiredHeader count '{ requiredHeaders.Count()}' is different then read header '{csvheaders.Count()}'"); } foreach (var requiredHeader in requiredHeaders) @@ -167,7 +167,7 @@ private AsyncRetryPolicy CreatePolicy(ILogger logger, strin sleepDurationProvider: retry => TimeSpan.FromSeconds(5), onRetry: (exception, timeSpan, retry, ctx) => { - logger.LogWarning(exception, "[{prefix}] Error seeding database (attempt {retry} of {retries})", prefix, retry, retries); + logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", prefix, exception.GetType().Name, exception.Message, retry, retries); } ); } diff --git a/src/Services/Ordering/Ordering.API/Ordering.API.csproj b/src/Services/Ordering/Ordering.API/Ordering.API.csproj index 4585cc4f65..7a5813400a 100644 --- a/src/Services/Ordering/Ordering.API/Ordering.API.csproj +++ b/src/Services/Ordering/Ordering.API/Ordering.API.csproj @@ -3,39 +3,71 @@ net7.0 aspnet-Ordering.API-20161122013547 - enable + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; ..\..\..\..\docker-compose.dcproj - Microsoft.eShopOnContainers.Services.Ordering.API + false + true + + PreserveNewest + PreserveNewest - - - - + + + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -45,13 +77,4 @@ - - - - PreserveNewest - true - PreserveNewest - - - - \ No newline at end of file + diff --git a/src/Services/Ordering/Ordering.API/Program.cs b/src/Services/Ordering/Ordering.API/Program.cs index 56d76de33e..66499d73e8 100644 --- a/src/Services/Ordering/Ordering.API/Program.cs +++ b/src/Services/Ordering/Ordering.API/Program.cs @@ -1,72 +1,109 @@ -var builder = WebApplication.CreateBuilder(args); +var configuration = GetConfiguration(); -builder.AddServiceDefaults(); +Log.Logger = CreateSerilogLogger(configuration); -builder.Services.AddGrpc(); -builder.Services.AddControllers(); - -builder.Services.AddHealthChecks(builder.Configuration); -builder.Services.AddDbContexts(builder.Configuration); -builder.Services.AddApplicationOptions(builder.Configuration); -builder.Services.AddIntegrationServices(); - -var services = builder.Services; - -services.AddMediatR(cfg => +try { - cfg.RegisterServicesFromAssemblyContaining(typeof(Program)); - - cfg.AddOpenBehavior(typeof(LoggingBehavior<,>)); - cfg.AddOpenBehavior(typeof(ValidatorBehavior<,>)); - cfg.AddOpenBehavior(typeof(TransactionBehavior<,>)); -}); - -// Register the command validators for the validator behavior (validators based on FluentValidation library) -services.AddSingleton, CancelOrderCommandValidator>(); -services.AddSingleton, CreateOrderCommandValidator>(); -services.AddSingleton>, IdentifiedCommandValidator>(); -services.AddSingleton, ShipOrderCommandValidator>(); - -services.AddScoped(sp => new OrderQueries(builder.Configuration.GetConnectionString("OrderingDB"))); -services.AddScoped(); -services.AddScoped(); -services.AddScoped(); - -// Add integration event handlers. -services.AddTransient, GracePeriodConfirmedIntegrationEventHandler>(); -services.AddTransient, OrderPaymentFailedIntegrationEventHandler>(); -services.AddTransient, OrderPaymentSucceededIntegrationEventHandler>(); -services.AddTransient, OrderStockConfirmedIntegrationEventHandler>(); -services.AddTransient, OrderStockRejectedIntegrationEventHandler>(); -services.AddTransient, UserCheckoutAcceptedIntegrationEventHandler>(); - -var app = builder.Build(); - -app.UseServiceDefaults(); - -app.MapGrpcService(); -app.MapControllers(); + Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); + var host = BuildWebHost(configuration, args); + + Log.Information("Applying migrations ({ApplicationContext})...", Program.AppName); + host.MigrateDbContext((context, services) => + { + var env = services.GetService(); + var settings = services.GetService>(); + var logger = services.GetService>(); + + new OrderingContextSeed() + .SeedAsync(context, env, settings, logger) + .Wait(); + }) + .MigrateDbContext((_, __) => { }); + + Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); + host.Run(); + + return 0; +} +catch (Exception ex) +{ + Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} -var eventBus = app.Services.GetRequiredService(); +IWebHost BuildWebHost(IConfiguration configuration, string[] args) => + WebHost.CreateDefaultBuilder(args) + .CaptureStartupErrors(false) + .ConfigureKestrel(options => + { + var ports = GetDefinedPorts(configuration); + options.Listen(IPAddress.Any, ports.httpPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + + options.Listen(IPAddress.Any, ports.grpcPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + + }) + .ConfigureAppConfiguration(x => x.AddConfiguration(configuration)) + .UseStartup() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseSerilog() + .Build(); + +Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) +{ + var seqServerUrl = configuration["Serilog:SeqServerUrl"]; + var logstashUrl = configuration["Serilog:LogstashgUrl"]; + return new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithProperty("ApplicationContext", Program.AppName) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) + .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl,null) + .ReadFrom.Configuration(configuration) + .CreateLogger(); +} -eventBus.Subscribe>(); -eventBus.Subscribe>(); -eventBus.Subscribe>(); -eventBus.Subscribe>(); -eventBus.Subscribe>(); -eventBus.Subscribe>(); +IConfiguration GetConfiguration() +{ + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + + var config = builder.Build(); + + if (config.GetValue("UseVault", false)) + { + TokenCredential credential = new ClientSecretCredential( + config["Vault:TenantId"], + config["Vault:ClientId"], + config["Vault:ClientSecret"]); + builder.AddAzureKeyVault(new Uri($"https://{config["Vault:Name"]}.vault.azure.net/"), credential); + } + + return builder.Build(); +} -using (var scope = app.Services.CreateScope()) +(int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config) { - var context = scope.ServiceProvider.GetRequiredService(); - var env = app.Services.GetService(); - var settings = app.Services.GetService>(); - var logger = app.Services.GetService>(); - await context.Database.MigrateAsync(); - - await new OrderingContextSeed().SeedAsync(context, env, settings, logger); - var integEventContext = scope.ServiceProvider.GetRequiredService(); - await integEventContext.Database.MigrateAsync(); + var grpcPort = config.GetValue("GRPC_PORT", 5001); + var port = config.GetValue("PORT", 80); + return (port, grpcPort); } -await app.RunAsync(); +public partial class Program +{ + + public static string Namespace = typeof(Startup).Namespace; + public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Properties/launchSettings.json b/src/Services/Ordering/Ordering.API/Properties/launchSettings.json index 64d0219263..9d9a764905 100644 --- a/src/Services/Ordering/Ordering.API/Properties/launchSettings.json +++ b/src/Services/Ordering/Ordering.API/Properties/launchSettings.json @@ -1,9 +1,25 @@ { + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55102/", + "sslPort": 0 + } + }, "profiles": { - "Ordering.API": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "/swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Microsoft.eShopOnContainers.Services.Ordering.API": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "http://localhost:5224/", + "launchUrl": "http://localhost:55102/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs new file mode 100644 index 0000000000..153f74ea12 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -0,0 +1,401 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; + +namespace Microsoft.eShopOnContainers.Services.Ordering.API; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public virtual IServiceProvider ConfigureServices(IServiceCollection services) + { + services + .AddGrpc(options => + { + options.EnableDetailedErrors = true; + }) + .Services + .AddApplicationInsights(Configuration) + .AddCustomMvc() + .AddHealthChecks(Configuration) + .AddCustomDbContext(Configuration) + .AddCustomSwagger(Configuration) + .AddCustomAuthentication(Configuration) + .AddCustomAuthorization(Configuration) + .AddCustomIntegrations(Configuration) + .AddCustomConfiguration(Configuration) + .AddEventBus(Configuration); + //configure autofac + + var container = new ContainerBuilder(); + container.Populate(services); + + container.RegisterModule(new MediatorModule()); + container.RegisterModule(new ApplicationModule(Configuration["ConnectionString"])); + + return new AutofacServiceProvider(container.Build()); + } + + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + //loggerFactory.AddAzureWebAppDiagnostics(); + //loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); + + var pathBase = Configuration["PATH_BASE"]; + if (!string.IsNullOrEmpty(pathBase)) + { + loggerFactory.CreateLogger().LogDebug("Using PATH BASE '{pathBase}'", pathBase); + app.UsePathBase(pathBase); + } + + app.UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Ordering.API V1"); + c.OAuthClientId("orderingswaggerui"); + c.OAuthAppName("Ordering Swagger UI"); + }); + + app.UseRouting(); + app.UseCors("CorsPolicy"); + ConfigureAuth(app); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + endpoints.MapDefaultControllerRoute(); + endpoints.MapControllers(); + endpoints.MapGet("/_proto/", async ctx => + { + ctx.Response.ContentType = "text/plain"; + using var fs = new FileStream(Path.Combine(env.ContentRootPath, "Proto", "basket.proto"), FileMode.Open, FileAccess.Read); + using var sr = new StreamReader(fs); + while (!sr.EndOfStream) + { + var line = await sr.ReadLineAsync(); + if (line != "/* >>" || line != "<< */") + { + await ctx.Response.WriteAsync(line); + } + } + }); + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + + ConfigureEventBus(app); + } + + + private void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); + } + + protected virtual void ConfigureAuth(IApplicationBuilder app) + { + app.UseAuthentication(); + app.UseAuthorization(); + } +} + +static class CustomExtensionsMethods +{ + public static IServiceCollection AddApplicationInsights(this IServiceCollection services, IConfiguration configuration) + { + services.AddApplicationInsightsTelemetry(configuration); + services.AddApplicationInsightsKubernetesEnricher(); + + return services; + } + + public static IServiceCollection AddCustomMvc(this IServiceCollection services) + { + // Add framework services. + services.AddControllers(options => + { + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + }) + // Added for functional tests + .AddApplicationPart(typeof(OrdersController).Assembly) + .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); + + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .SetIsOriginAllowed((host) => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + + return services; + } + + public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); + + hcBuilder + .AddSqlServer( + configuration["ConnectionString"], + name: "OrderingDB-check", + tags: new string[] { "orderingdb" }); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder + .AddAzureServiceBusTopic( + configuration["EventBusConnection"], + topicName: "eshop_event_bus", + name: "ordering-servicebus-check", + tags: new string[] { "servicebus" }); + } + else + { + hcBuilder + .AddRabbitMQ( + $"amqp://{configuration["EventBusConnection"]}", + name: "ordering-rabbitmqbus-check", + tags: new string[] { "rabbitmqbus" }); + } + + return services; + } + + public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext(options => + { + options.UseSqlServer(configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }, + ServiceLifetime.Scoped //Showing explicitly that the DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request) + ); + + services.AddDbContext(options => + { + options.UseSqlServer(configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }); + + return services; + } + + public static IServiceCollection AddCustomSwagger(this IServiceCollection services, IConfiguration configuration) + { + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "eShopOnContainers - Ordering HTTP API", + Version = "v1", + Description = "The Ordering Service HTTP API" + }); + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows() + { + Implicit = new OpenApiOAuthFlow() + { + AuthorizationUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/authorize"), + TokenUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/token"), + Scopes = new Dictionary() + { + { "orders", "Ordering API" } + } + } + } + }); + + options.OperationFilter(); + }); + + return services; + } + + public static IServiceCollection AddCustomIntegrations(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient>( + sp => (DbConnection c) => new IntegrationEventLogService(c)); + + services.AddTransient(); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusConnectionString = configuration["EventBusConnection"]; + + var subscriptionClientName = configuration["SubscriptionClientName"]; + + return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); + }); + } + else + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + + + var factory = new ConnectionFactory() + { + HostName = configuration["EventBusConnection"], + DispatchConsumersAsync = true + }; + + if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) + { + factory.UserName = configuration["EventBusUserName"]; + } + + if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) + { + factory.Password = configuration["EventBusPassword"]; + } + + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); + }); + } + + return services; + } + + public static IServiceCollection AddCustomConfiguration(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions(); + services.Configure(configuration); + services.Configure(options => + { + options.InvalidModelStateResponseFactory = context => + { + var problemDetails = new ValidationProblemDetails(context.ModelState) + { + Instance = context.HttpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." + }; + + return new BadRequestObjectResult(problemDetails) + { + ContentTypes = { "application/problem+json", "application/problem+xml" } + }; + }; + }); + + return services; + } + + public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) + { + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + string subscriptionName = configuration["SubscriptionClientName"]; + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, + eventBusSubcriptionsManager, iLifetimeScope, subscriptionName); + }); + } + else + { + services.AddSingleton(sp => + { + var subscriptionClientName = configuration["SubscriptionClientName"]; + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); + }); + } + + services.AddSingleton(); + + return services; + } + + public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) + { + // prevent from mapping "sub" claim to nameidentifier. + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); + + var identityUrl = configuration.GetValue("IdentityUrl"); + + services.AddAuthentication("Bearer").AddJwtBearer(options => + { + options.Authority = identityUrl; + options.RequireHttpsMetadata = false; + options.Audience = "orders"; + options.TokenValidationParameters.ValidateAudience = false; + }); + + return services; + } + public static IServiceCollection AddCustomAuthorization(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthorization(options => + { + options.AddPolicy("ApiScope", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim("scope", "orders"); + }); + }); + return services; + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/appsettings.Development.json b/src/Services/Ordering/Ordering.API/appsettings.Development.json deleted file mode 100644 index af4699517e..0000000000 --- a/src/Services/Ordering/Ordering.API/appsettings.Development.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ConnectionStrings": { - "OrderingDB": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;Encrypt=false" - } -} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/appsettings.json b/src/Services/Ordering/Ordering.API/appsettings.json index 091b536260..a10f3358bb 100644 --- a/src/Services/Ordering/Ordering.API/appsettings.json +++ b/src/Services/Ordering/Ordering.API/appsettings.json @@ -1,54 +1,32 @@ { - "Logging": { - "LogLevel": { + "ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;TrustServerCertificate=true", + "IdentityUrl": "http://localhost:5105", + "UseCustomizationData": false, + "Serilog": { + "SeqServerUrl": null, + "LogstashgUrl": null, + "MinimumLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "Kestrel": { - "Endpoints": { - "Http": { - "Url": "http://localhost:5224" - }, - "gRPC": { - "Url": "http://localhost:6224", - "Protocols": "Http2" + "Override": { + "Microsoft": "Warning", + "Microsoft.eShopOnContainers": "Information", + "System": "Warning" } } }, - "OpenApi": { - "Endpoint": { - "Name": "Ordering.API V1" - }, - "Document": { - "Description": "The Ordering Service HTTP API", - "Title": "eShopOnContainers - Ordering HTTP API", - "Version": "v1" - }, - "Auth": { - "ClientId": "orderingswaggerui", - "AppName": "Ordering Swagger UI" - } - }, - "ConnectionStrings": { - "EventBus": "localhost" - }, - "EventBus": { - "SubscriptionClientName": "Ordering", - "RetryCount": 5 - }, + "AzureServiceBusEnabled": false, + "SubscriptionClientName": "Ordering", + "GracePeriodTime": "1", + "CheckUpdateTime": "30000", "ApplicationInsights": { "InstrumentationKey": "" }, - "Identity": { - "Url": "http://localhost:5223", - "Audience": "orders", - "Scopes": { - "orders": "Ordering API" - } - }, - "UseCustomizationData": false, - "GracePeriodTime": "1", - "CheckUpdateTime": "30000" + "EventBusRetryCount": 5, + "EventBusConnection": "localhost", + "UseVault": false, + "Vault": { + "Name": "eshop", + "ClientId": "your-client-id", + "ClientSecret": "your-client-secret" + } } diff --git a/src/Services/Ordering/Ordering.API/azds.yaml b/src/Services/Ordering/Ordering.API/azds.yaml new file mode 100644 index 0000000000..77398a0a86 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/azds.yaml @@ -0,0 +1,56 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: ..\..\..\.. + dockerfile: Dockerfile +install: + chart: ../../../../k8s/helm/ordering-api + set: + replicaCount: 1 + image: + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + - $(spacePrefix)eshop$(hostSuffix) + inf: + k8s: + dns: $(spacePrefix)eshop$(hostSuffix) + values: + - values.dev.yaml? + - secrets.dev.yaml? + - inf.yaml + - app.yaml +configurations: + develop: + build: + useGitIgnore: true + dockerfile: Dockerfile.develop + args: + BUILD_CONFIGURATION: ${BUILD_CONFIGURATION:-Debug} + container: + sync: + - '**/Pages/**' + - '**/Views/**' + - '**/wwwroot/**' + - '!**/*.{sln,csproj}' + command: + - dotnet + - run + - --no-restore + - --no-build + - --no-launch-profile + - -c + - ${BUILD_CONFIGURATION:-Debug} + iterate: + processesToKill: + - dotnet + - vsdbg + buildCommands: + - - dotnet + - build + - --no-restore + - -c + - ${BUILD_CONFIGURATION:-Debug} diff --git a/src/Services/Ordering/Ordering.API/web.config b/src/Services/Ordering/Ordering.API/web.config new file mode 100644 index 0000000000..3d49211e56 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/web.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.BackgroundTasks/Dockerfile b/src/Services/Ordering/Ordering.BackgroundTasks/Dockerfile index 3bebc9661e..43348d40c4 100644 --- a/src/Services/Ordering/Ordering.BackgroundTasks/Dockerfile +++ b/src/Services/Ordering/Ordering.BackgroundTasks/Dockerfile @@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" @@ -32,7 +33,6 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" -COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" @@ -42,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" COPY "docker-compose.dcproj" "docker-compose.dcproj" -COPY "Directory.Packages.props" "Directory.Packages.props" COPY "NuGet.config" "NuGet.config" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" diff --git a/src/Services/Ordering/Ordering.BackgroundTasks/Extensions/CustomExtensionMethods.cs b/src/Services/Ordering/Ordering.BackgroundTasks/Extensions/CustomExtensionMethods.cs index b305e7920a..7010bbae8e 100644 --- a/src/Services/Ordering/Ordering.BackgroundTasks/Extensions/CustomExtensionMethods.cs +++ b/src/Services/Ordering/Ordering.BackgroundTasks/Extensions/CustomExtensionMethods.cs @@ -1,25 +1,145 @@ -namespace Ordering.BackgroundTasks.Extensions; +using Autofac; +using Azure.Messaging.ServiceBus; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using Serilog; -public static class CustomExtensionMethods +namespace Ordering.BackgroundTasks.Extensions { - public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) + public static class CustomExtensionMethods { - var hcBuilder = services.AddHealthChecks(); + public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); - hcBuilder.AddSqlServer(_ => - configuration.GetRequiredConnectionString("OrderingDB"), - name: "OrderingTaskDB-check", - tags: new string[] { "live", "ready" }); + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); - return services; - } + hcBuilder.AddSqlServer( + configuration["ConnectionString"], + name: "OrderingTaskDB-check", + tags: new string[] { "orderingtaskdb" }); - public static IServiceCollection AddApplicationOptions(this IServiceCollection services, IConfiguration configuration) - { - return services.Configure(configuration) - .Configure(o => + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder.AddAzureServiceBusTopic( + configuration["EventBusConnection"], + topicName: "eshop_event_bus", + name: "orderingtask-servicebus-check", + tags: new string[] { "servicebus" }); + } + else + { + hcBuilder.AddRabbitMQ( + $"amqp://{configuration["EventBusConnection"]}", + name: "orderingtask-rabbitmqbus-check", + tags: new string[] { "rabbitmqbus" }); + } + + return services; + } + + public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) { - o.ConnectionString = configuration.GetRequiredConnectionString("OrderingDB"); - }); + var subscriptionClientName = configuration["SubscriptionClientName"]; + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusConnectionString = configuration["EventBusConnection"]; + + return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); + }); + + services.AddSingleton(sp => + { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + string subscriptionName = configuration["SubscriptionClientName"]; + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, eventBusSubcriptionsManager, iLifetimeScope, subscriptionName); + }); + } + else + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + + var factory = new ConnectionFactory() + { + HostName = configuration["EventBusConnection"], + DispatchConsumersAsync = true + }; + + if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) + { + factory.UserName = configuration["EventBusUserName"]; + } + + if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) + { + factory.Password = configuration["EventBusPassword"]; + } + + var retryCount = 5; + + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); + }); + + services.AddSingleton(sp => + { + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + + var retryCount = 5; + + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); + }); + } + + services.AddSingleton(); + + return services; + } + + public static ILoggingBuilder UseSerilog(this ILoggingBuilder builder, IConfiguration configuration) + { + var seqServerUrl = configuration["Serilog:SeqServerUrl"]; + var logstashUrl = configuration["Serilog:LogstashgUrl"]; + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithProperty("ApplicationContext", Program.AppName) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) + .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl,null) + .ReadFrom.Configuration(configuration) + .CreateLogger(); + + return builder; + } } } diff --git a/src/Services/Ordering/Ordering.BackgroundTasks/GlobalUsings.cs b/src/Services/Ordering/Ordering.BackgroundTasks/GlobalUsings.cs deleted file mode 100644 index c4d5650601..0000000000 --- a/src/Services/Ordering/Ordering.BackgroundTasks/GlobalUsings.cs +++ /dev/null @@ -1,8 +0,0 @@ -global using Microsoft.AspNetCore.Builder; -global using Microsoft.Extensions.Configuration; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; -global using Ordering.BackgroundTasks.Extensions; -global using Ordering.BackgroundTasks.Services; -global using Services.Common; diff --git a/src/Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj b/src/Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj index 41ce41ed12..fe7da86807 100644 --- a/src/Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj +++ b/src/Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj @@ -2,18 +2,34 @@ net7.0 - enable dotnet-Ordering.BackgroundTasks-9D3E1DD6-405B-447F-8AAB-1708B36D260E - Linux + false + Linux - - - + + + + + + + + + + + + + + + + + - + + + diff --git a/src/Services/Ordering/Ordering.BackgroundTasks/Program.cs b/src/Services/Ordering/Ordering.BackgroundTasks/Program.cs index 7d408b4b81..ab30ba2d97 100644 --- a/src/Services/Ordering/Ordering.BackgroundTasks/Program.cs +++ b/src/Services/Ordering/Ordering.BackgroundTasks/Program.cs @@ -1,13 +1,35 @@ -var builder = WebApplication.CreateBuilder(args); +using Autofac.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Ordering.BackgroundTasks.Extensions; +using Serilog; +using System.IO; -builder.AddServiceDefaults(); +namespace Ordering.BackgroundTasks +{ + public class Program + { + public static readonly string AppName = typeof(Program).Assembly.GetName().Name; -builder.Services.AddHealthChecks(builder.Configuration); -builder.Services.AddApplicationOptions(builder.Configuration); -builder.Services.AddHostedService(); + public static void Main(string[] args) + { + CreateHostBuilder(args).Run(); + } -var app = builder.Build(); - -app.UseServiceDefaults(); - -await app.RunAsync(); + public static IHost CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()) + .ConfigureAppConfiguration((host, builder) => + { + builder.SetBasePath(Directory.GetCurrentDirectory()); + builder.AddJsonFile("appsettings.json", optional: true); + builder.AddJsonFile($"appsettings.{host.HostingEnvironment.EnvironmentName}.json", optional: true); + builder.AddEnvironmentVariables(); + builder.AddCommandLine(args); + }) + .ConfigureLogging((host, builder) => builder.UseSerilog(host.Configuration).AddSerilog()) + .Build(); + } +} diff --git a/src/Services/Ordering/Ordering.BackgroundTasks/Services/GracePeriodManagerService.cs b/src/Services/Ordering/Ordering.BackgroundTasks/Services/GracePeriodManagerService.cs index 016f0eb9cc..9f00f3f608 100644 --- a/src/Services/Ordering/Ordering.BackgroundTasks/Services/GracePeriodManagerService.cs +++ b/src/Services/Ordering/Ordering.BackgroundTasks/Services/GracePeriodManagerService.cs @@ -1,8 +1,14 @@ -using System.Data.SqlClient; -using Dapper; +using Dapper; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Ordering.BackgroundTasks.Events; +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Threading; +using System.Threading.Tasks; namespace Ordering.BackgroundTasks.Services { @@ -30,6 +36,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogDebug("GracePeriodManagerService background task is doing background work."); CheckConfirmedGracePeriodOrders(); + await Task.Delay(_settings.CheckUpdateTime, stoppingToken); } @@ -46,7 +53,7 @@ private void CheckConfirmedGracePeriodOrders() { var confirmGracePeriodEvent = new GracePeriodConfirmedIntegrationEvent(orderId); - _logger.LogInformation("Publishing integration event: {IntegrationEventId} - ({@IntegrationEvent})", confirmGracePeriodEvent.Id, confirmGracePeriodEvent); + _logger.LogInformation("----- Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", confirmGracePeriodEvent.Id, Program.AppName, confirmGracePeriodEvent); _eventBus.Publish(confirmGracePeriodEvent); } @@ -68,7 +75,7 @@ WHERE DATEDIFF(minute, [OrderDate], GETDATE()) >= @GracePeriodTime } catch (SqlException exception) { - _logger.LogCritical(exception, "Fatal error establishing database connection"); + _logger.LogCritical(exception, "FATAL ERROR: Database connections could not be opened: {Message}", exception.Message); } diff --git a/src/Services/Ordering/Ordering.BackgroundTasks/Startup.cs b/src/Services/Ordering/Ordering.BackgroundTasks/Startup.cs new file mode 100644 index 0000000000..403844e731 --- /dev/null +++ b/src/Services/Ordering/Ordering.BackgroundTasks/Startup.cs @@ -0,0 +1,48 @@ +namespace Ordering.BackgroundTasks +{ + using HealthChecks.UI.Client; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Diagnostics.HealthChecks; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using Ordering.BackgroundTasks.Extensions; + using Ordering.BackgroundTasks.Services; + + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public virtual void ConfigureServices(IServiceCollection services) + { + services.AddCustomHealthCheck(this.Configuration) + .Configure(this.Configuration) + .AddOptions() + .AddHostedService() + .AddEventBus(this.Configuration); + } + + + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + } + } +} diff --git a/src/Services/Ordering/Ordering.BackgroundTasks/appsettings.Development.json b/src/Services/Ordering/Ordering.BackgroundTasks/appsettings.Development.json index 19fe83b477..e203e9407e 100644 --- a/src/Services/Ordering/Ordering.BackgroundTasks/appsettings.Development.json +++ b/src/Services/Ordering/Ordering.BackgroundTasks/appsettings.Development.json @@ -5,8 +5,5 @@ "System": "Information", "Microsoft": "Information" } - }, - "ConnectionStrings": { - "OrderingDB": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;Encrypt=false" } } diff --git a/src/Services/Ordering/Ordering.BackgroundTasks/appsettings.json b/src/Services/Ordering/Ordering.BackgroundTasks/appsettings.json index bb061ce403..88e5d68588 100644 --- a/src/Services/Ordering/Ordering.BackgroundTasks/appsettings.json +++ b/src/Services/Ordering/Ordering.BackgroundTasks/appsettings.json @@ -1,17 +1,26 @@ { - "Logging": { - "LogLevel": { + "ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;", + "Serilog": { + "SeqServerUrl": null, + "LogstashgUrl": null, + "MinimumLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Override": { + "Microsoft": "Warning", + "Microsoft.eShopOnContainers": "Information", + "System": "Warning" + } } }, - "ConnectionStrings": { - "EventBus": "localhost" - }, - "EventBus": { - "SubscriptionClientName": "BackgroundTasks", - "RetryCount": 5 - }, + "SubscriptionClientName": "BackgroundTasks", "GracePeriodTime": "1", - "CheckUpdateTime": "1000" + "CheckUpdateTime": "1000", + "ApplicationInsights": { + "InstrumentationKey": "" + }, + "AzureServiceBusEnabled": false, + "EventBusRetryCount": 5, + "EventBusConnection": "", + "EventBusUserName": "", + "EventBusPassword": "" } \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/PaymentMethod.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/PaymentMethod.cs index 2a95e98b9c..e4e4904883 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/PaymentMethod.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/PaymentMethod.cs @@ -1,6 +1,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; -public class PaymentMethod : Entity +public class PaymentMethod + : Entity { private string _alias; private string _cardNumber; @@ -11,10 +12,12 @@ public class PaymentMethod : Entity private int _cardTypeId; public CardType CardType { get; private set; } + protected PaymentMethod() { } public PaymentMethod(int cardTypeId, string alias, string cardNumber, string securityNumber, string cardHolderName, DateTime expiration) { + _cardNumber = !string.IsNullOrWhiteSpace(cardNumber) ? cardNumber : throw new OrderingDomainException(nameof(cardNumber)); _securityNumber = !string.IsNullOrWhiteSpace(securityNumber) ? securityNumber : throw new OrderingDomainException(nameof(securityNumber)); _cardHolderName = !string.IsNullOrWhiteSpace(cardHolderName) ? cardHolderName : throw new OrderingDomainException(nameof(cardHolderName)); diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs index 0d4b819aee..c58f81b162 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs @@ -4,11 +4,11 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O public class Address : ValueObject { - public string Street { get; private set; } - public string City { get; private set; } - public string State { get; private set; } - public string Country { get; private set; } - public string ZipCode { get; private set; } + public String Street { get; private set; } + public String City { get; private set; } + public String State { get; private set; } + public String Country { get; private set; } + public String ZipCode { get; private set; } public Address() { } diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs index 8926524ef2..81a67324bf 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs @@ -1,4 +1,6 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; +using Microsoft.eShopOnContainers.Services.Ordering.Domain.Events; + +namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; public class Order : Entity, IAggregateRoot @@ -19,15 +21,15 @@ public class Order private string _description; + + // Draft orders have this set to true. Currently we don't check anywhere the draft status of an Order, but we could do it if needed -#pragma warning disable CS0414 // The field 'Order._isDraft' is assigned but its value is never used private bool _isDraft; -#pragma warning restore CS0414 // DDD Patterns comment // Using a private collection field, better for DDD Aggregate's encapsulation // so OrderItems cannot be added from "outside the AggregateRoot" directly to the collection, - // but only through the method OrderAggregateRoot.AddOrderItem() which includes behavior. + // but only through the method OrderAggrergateRoot.AddOrderItem() which includes behaviour. private readonly List _orderItems; public IReadOnlyCollection OrderItems => _orderItems; @@ -35,10 +37,8 @@ public class Order public static Order NewDraft() { - var order = new Order - { - _isDraft = true - }; + var order = new Order(); + order._isDraft = true; return order; } @@ -58,13 +58,13 @@ public Order(string userId, string userName, Address address, int cardTypeId, st Address = address; // Add the OrderStarterDomainEvent to the domain events collection - // to be raised/dispatched when committing changes into the Database [ After DbContext.SaveChanges() ] + // to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ] AddOrderStartedDomainEvent(userId, userName, cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration); } // DDD Patterns comment - // This Order AggregateRoot's method "AddOrderItem()" should be the only way to add Items to the Order, + // This Order AggregateRoot's method "AddOrderitem()" should be the only way to add Items to the Order, // so any behavior (discounts, etc.) and validations are controlled by the AggregateRoot // in order to maintain consistency between the whole Aggregate. public void AddOrderItem(int productId, string productName, decimal unitPrice, decimal discount, string pictureUrl, int units = 1) diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs index 8c3cc50fb4..aae09bc6af 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs @@ -23,11 +23,11 @@ public static IEnumerable List() => public static OrderStatus FromName(string name) { var state = List() - .SingleOrDefault(s => string.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase)); + .SingleOrDefault(s => String.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase)); if (state == null) { - throw new OrderingDomainException($"Possible values for OrderStatus: {string.Join(",", List().Select(s => s.Name))}"); + throw new OrderingDomainException($"Possible values for OrderStatus: {String.Join(",", List().Select(s => s.Name))}"); } return state; @@ -39,7 +39,7 @@ public static OrderStatus From(int id) if (state == null) { - throw new OrderingDomainException($"Possible values for OrderStatus: {string.Join(",", List().Select(s => s.Name))}"); + throw new OrderingDomainException($"Possible values for OrderStatus: {String.Join(",", List().Select(s => s.Name))}"); } return state; diff --git a/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs b/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs index 715c677da8..0ef1765642 100644 --- a/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs +++ b/src/Services/Ordering/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs @@ -1,5 +1,5 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Events; - + /// /// Event used when the order is paid /// diff --git a/src/Services/Ordering/Ordering.Domain/GlobalUsings.cs b/src/Services/Ordering/Ordering.Domain/GlobalUsings.cs index a7b4f4d403..33d18f36e6 100644 --- a/src/Services/Ordering/Ordering.Domain/GlobalUsings.cs +++ b/src/Services/Ordering/Ordering.Domain/GlobalUsings.cs @@ -1,7 +1,12 @@ -global using System.Reflection; -global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Exceptions; +global using global::Microsoft.eShopOnContainers.Services.Ordering.Domain.Exceptions; global using MediatR; +global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; global using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; global using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Events; -global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; +global using System.Collections.Generic; +global using System.Linq; +global using System.Reflection; +global using System.Threading.Tasks; +global using System.Threading; +global using System; \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.Domain/Ordering.Domain.csproj b/src/Services/Ordering/Ordering.Domain/Ordering.Domain.csproj index a742a7f5ae..c6508b17bf 100644 --- a/src/Services/Ordering/Ordering.Domain/Ordering.Domain.csproj +++ b/src/Services/Ordering/Ordering.Domain/Ordering.Domain.csproj @@ -2,12 +2,12 @@ net7.0 - enable - - + + + diff --git a/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs b/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs index 1a577361ae..a8e5c23adc 100644 --- a/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs +++ b/src/Services/Ordering/Ordering.Domain/SeedWork/Entity.cs @@ -37,7 +37,7 @@ public void ClearDomainEvents() public bool IsTransient() { - return this.Id == default; + return this.Id == default(Int32); } public override bool Equals(object obj) diff --git a/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs b/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs index eb8fc97175..8d313149dd 100644 --- a/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs +++ b/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs @@ -9,14 +9,14 @@ public abstract class Enumeration : IComparable protected Enumeration(int id, string name) => (Id, Name) = (id, name); public override string ToString() => Name; - + public static IEnumerable GetAll() where T : Enumeration => typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) .Select(f => f.GetValue(null)) .Cast(); - + public override bool Equals(object obj) { if (obj is not Enumeration otherValue) diff --git a/src/Services/Ordering/Ordering.Domain/SeedWork/IUnitOfWork.cs b/src/Services/Ordering/Ordering.Domain/SeedWork/IUnitOfWork.cs index 408815e111..b9c1216557 100644 --- a/src/Services/Ordering/Ordering.Domain/SeedWork/IUnitOfWork.cs +++ b/src/Services/Ordering/Ordering.Domain/SeedWork/IUnitOfWork.cs @@ -2,6 +2,6 @@ public interface IUnitOfWork : IDisposable { - Task SaveChangesAsync(CancellationToken cancellationToken = default); - Task SaveEntitiesAsync(CancellationToken cancellationToken = default); + Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); + Task SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken)); } diff --git a/src/Services/Ordering/Ordering.FunctionalTests/GlobalUsings.cs b/src/Services/Ordering/Ordering.FunctionalTests/GlobalUsings.cs index 5c3815a9f7..cee4b67336 100644 --- a/src/Services/Ordering/Ordering.FunctionalTests/GlobalUsings.cs +++ b/src/Services/Ordering/Ordering.FunctionalTests/GlobalUsings.cs @@ -1,9 +1,19 @@ -global using System.IO; -global using System.Net.Http; -global using System.Security.Claims; -global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Builder; global using Microsoft.AspNetCore.Hosting; global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Routing; global using Microsoft.AspNetCore.TestHost; +global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure; +global using Microsoft.eShopOnContainers.Services.Ordering.API; +global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using System.IO; +global using System.Net.Http; +global using System.Reflection; +global using System.Security.Claims; +global using System.Threading.Tasks; +global using System; diff --git a/src/Services/Ordering/Ordering.FunctionalTests/HttpClientExtensions.cs b/src/Services/Ordering/Ordering.FunctionalTests/HttpClientExtensions.cs new file mode 100644 index 0000000000..a4a0dd9c23 --- /dev/null +++ b/src/Services/Ordering/Ordering.FunctionalTests/HttpClientExtensions.cs @@ -0,0 +1,11 @@ +namespace Ordering.FunctionalTests; + +static class HttpClientExtensions +{ + public static HttpClient CreateIdempotentClient(this TestServer server) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("x-requestid", Guid.NewGuid().ToString()); + return client; + } +} diff --git a/src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj b/src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj index 29fe059ac2..9b9e67a586 100644 --- a/src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj +++ b/src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj @@ -2,29 +2,33 @@ net7.0 - enable - false + false - - PreserveNewest + + + + + + Always - + - - - - + + + + all runtime; build; native; contentfiles; analyzers - + + diff --git a/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs index f3fba40e3a..d03d543800 100644 --- a/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs +++ b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs @@ -1,39 +1,36 @@ -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.Hosting; - -namespace Ordering.FunctionalTests; +namespace Ordering.FunctionalTests; public class OrderingScenarioBase { - private class OrderingApplication : WebApplicationFactory + public TestServer CreateServer() { - public TestServer CreateServer() - { - return Server; - } + var path = Assembly.GetAssembly(typeof(OrderingScenarioBase)) + .Location; - protected override IHost CreateHost(IHostBuilder builder) - { - builder.ConfigureServices(services => + var hostBuilder = new WebHostBuilder() + .UseContentRoot(Path.GetDirectoryName(path)) + .ConfigureAppConfiguration(cb => { - services.AddSingleton(); - }); + cb.AddJsonFile("appsettings.json", optional: false) + .AddEnvironmentVariables(); + }).UseStartup(); - builder.ConfigureAppConfiguration(c => - { - var directory = Path.GetDirectoryName(typeof(OrderingScenarioBase).Assembly.Location)!; + var testServer = new TestServer(hostBuilder); - c.AddJsonFile(Path.Combine(directory, "appsettings.Ordering.json"), optional: false); - }); + testServer.Host + .MigrateDbContext((context, services) => + { + var env = services.GetService(); + var settings = services.GetService>(); + var logger = services.GetService>(); - return base.CreateHost(builder); - } - } + new OrderingContextSeed() + .SeedAsync(context, env, settings, logger) + .Wait(); + }) + .MigrateDbContext((_, __) => { }); - public TestServer CreateServer() - { - var factory = new OrderingApplication(); - return factory.CreateServer(); + return testServer; } public static class Get @@ -51,17 +48,4 @@ public static class Put public static string CancelOrder = "api/v1/orders/cancel"; public static string ShipOrder = "api/v1/orders/ship"; } - - private class AuthStartupFilter : IStartupFilter - { - public Action Configure(Action next) - { - return app => - { - app.UseMiddleware(); - - next(app); - }; - } - } } diff --git a/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs index 85dd4ab3d1..d49f41960d 100644 --- a/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs +++ b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs @@ -1,6 +1,9 @@ using System.Net; +using System.Net.Http; using System.Text; using System.Text.Json; +using System.Threading.Tasks; +using WebMVC.Services.ModelDTOs; using Xunit; namespace Ordering.FunctionalTests @@ -15,7 +18,6 @@ public async Task Get_get_all_stored_orders_and_response_ok_status_code() var response = await server.CreateClient() .GetAsync(Get.Orders); - var s = await response.Content.ReadAsStringAsync(); response.EnsureSuccessStatusCode(); } @@ -23,14 +25,10 @@ public async Task Get_get_all_stored_orders_and_response_ok_status_code() public async Task Cancel_order_no_order_created_bad_request_response() { using var server = CreateServer(); - var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json") - { - Headers = { { "x-requestid", Guid.NewGuid().ToString() } } - }; - var response = await server.CreateClient() + var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json"); + var response = await server.CreateIdempotentClient() .PutAsync(Put.CancelOrder, content); - var s = await response.Content.ReadAsStringAsync(); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -38,11 +36,8 @@ public async Task Cancel_order_no_order_created_bad_request_response() public async Task Ship_order_no_order_created_bad_request_response() { using var server = CreateServer(); - var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json") - { - Headers = { { "x-requestid", Guid.NewGuid().ToString() } } - }; - var response = await server.CreateClient() + var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json"); + var response = await server.CreateIdempotentClient() .PutAsync(Put.ShipOrder, content); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); @@ -50,7 +45,7 @@ public async Task Ship_order_no_order_created_bad_request_response() string BuildOrder() { - var order = new + var order = new OrderDTO() { OrderNumber = "-1" }; diff --git a/src/Services/Ordering/Ordering.FunctionalTests/OrderingTestStartup.cs b/src/Services/Ordering/Ordering.FunctionalTests/OrderingTestStartup.cs new file mode 100644 index 0000000000..7367042de7 --- /dev/null +++ b/src/Services/Ordering/Ordering.FunctionalTests/OrderingTestStartup.cs @@ -0,0 +1,27 @@ +namespace Ordering.FunctionalTests; + +public class OrderingTestsStartup : Startup +{ + public OrderingTestsStartup(IConfiguration env) : base(env) + { + } + + public override IServiceProvider ConfigureServices(IServiceCollection services) + { + // Added to avoid the Authorize data annotation in test environment. + // Property "SuppressCheckForUnhandledSecurityMetadata" in appsettings.json + services.Configure(Configuration); + return base.ConfigureServices(services); + } + protected override void ConfigureAuth(IApplicationBuilder app) + { + if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) + { + app.UseMiddleware(); + } + else + { + base.ConfigureAuth(app); + } + } +} diff --git a/src/Services/Ordering/Ordering.FunctionalTests/appsettings.Ordering.json b/src/Services/Ordering/Ordering.FunctionalTests/appsettings.json similarity index 60% rename from src/Services/Ordering/Ordering.FunctionalTests/appsettings.Ordering.json rename to src/Services/Ordering/Ordering.FunctionalTests/appsettings.json index 3557ebc491..c59b5284e1 100644 --- a/src/Services/Ordering/Ordering.FunctionalTests/appsettings.Ordering.json +++ b/src/Services/Ordering/Ordering.FunctionalTests/appsettings.json @@ -1,9 +1,6 @@ { - "ConnectionStrings": { - "OrderingDB": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;Encrypt=False;TrustServerCertificate=true", - "EventBus": "localhost" - }, "CheckUpdateTime": "30000", + "ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;", "EventBusConnection": "localhost", "ExternalCatalogBaseUrl": "http://localhost:5101", "GracePeriodTime": "1", diff --git a/src/Services/Ordering/Ordering.Infrastructure/GlobalUsings.cs b/src/Services/Ordering/Ordering.Infrastructure/GlobalUsings.cs index dc02c7a6d2..72667e11dc 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/GlobalUsings.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/GlobalUsings.cs @@ -1,12 +1,18 @@ -global using System.Data; -global using MediatR; -global using Microsoft.EntityFrameworkCore; +global using MediatR; global using Microsoft.EntityFrameworkCore.Design; global using Microsoft.EntityFrameworkCore.Metadata.Builders; global using Microsoft.EntityFrameworkCore.Storage; +global using Microsoft.EntityFrameworkCore; global using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; global using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; -global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Exceptions; global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; -global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.EntityConfigurations; global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; +global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; +global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Exceptions; +global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.EntityConfigurations; +global using System.Data; +global using System.Linq; +global using System.Threading.Tasks; +global using System.Threading; +global using System; +global using System.Collections.Generic; \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj b/src/Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj index d27c1178ec..bcf3ba5a5b 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj +++ b/src/Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj @@ -2,7 +2,6 @@ net7.0 - enable @@ -10,10 +9,11 @@ - - - - + + + + + diff --git a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs index b7ff89a681..3edbfbc8dd 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs @@ -38,7 +38,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new BuyerEntityTypeConfiguration()); } - public async Task SaveEntitiesAsync(CancellationToken cancellationToken = default) + public async Task SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken)) { // Dispatch Domain Events collection. // Choices: @@ -50,7 +50,7 @@ public async Task SaveEntitiesAsync(CancellationToken cancellationToken = // After executing this line all the changes (from the Command Handler and Domain Event Handlers) // performed through the DbContext will be committed - _ = await base.SaveChangesAsync(cancellationToken); + var result = await base.SaveChangesAsync(cancellationToken); return true; } @@ -106,8 +106,6 @@ public void RollbackTransaction() } } -#nullable enable - public class OrderingContextDesignFactory : IDesignTimeDbContextFactory { public OrderingContext CreateDbContext(string[] args) @@ -122,12 +120,12 @@ class NoMediator : IMediator { public IAsyncEnumerable CreateStream(IStreamRequest request, CancellationToken cancellationToken = default) { - return default!; + return default(IAsyncEnumerable); } public IAsyncEnumerable CreateStream(object request, CancellationToken cancellationToken = default) { - return default!; + return default(IAsyncEnumerable); } public Task Publish(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification @@ -142,17 +140,12 @@ public Task Publish(object notification, CancellationToken cancellationToken = d public Task Send(IRequest request, CancellationToken cancellationToken = default) { - return Task.FromResult(default!); + return Task.FromResult(default(TResponse)); } - public Task Send(object request, CancellationToken cancellationToken = default) + public Task Send(object request, CancellationToken cancellationToken = default) { return Task.FromResult(default(object)); } - - public Task Send(TRequest request, CancellationToken cancellationToken = default) where TRequest : IRequest - { - return Task.CompletedTask; - } } } diff --git a/src/Services/Ordering/Ordering.SignalrHub/AutofacModules/ApplicationModule.cs b/src/Services/Ordering/Ordering.SignalrHub/AutofacModules/ApplicationModule.cs new file mode 100644 index 0000000000..9d28154ea6 --- /dev/null +++ b/src/Services/Ordering/Ordering.SignalrHub/AutofacModules/ApplicationModule.cs @@ -0,0 +1,20 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.SignalrHub.AutofacModules; + +public class ApplicationModule + : Autofac.Module +{ + + public string QueriesConnectionString { get; } + + public ApplicationModule() + { + } + + protected override void Load(ContainerBuilder builder) + { + + builder.RegisterAssemblyTypes(typeof(OrderStatusChangedToAwaitingValidationIntegrationEvent).GetTypeInfo().Assembly) + .AsClosedTypesOf(typeof(IIntegrationEventHandler<>)); + + } +} diff --git a/src/Services/Ordering/Ordering.SignalrHub/Dockerfile b/src/Services/Ordering/Ordering.SignalrHub/Dockerfile index 12529612d5..82cb625a72 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/Dockerfile +++ b/src/Services/Ordering/Ordering.SignalrHub/Dockerfile @@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" @@ -32,7 +33,6 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" -COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" @@ -42,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" COPY "docker-compose.dcproj" "docker-compose.dcproj" -COPY "Directory.Packages.props" "Directory.Packages.props" COPY "NuGet.config" "NuGet.config" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" diff --git a/src/Services/Ordering/Ordering.SignalrHub/Extensions/Extensions.cs b/src/Services/Ordering/Ordering.SignalrHub/Extensions/Extensions.cs deleted file mode 100644 index ee11e7fea3..0000000000 --- a/src/Services/Ordering/Ordering.SignalrHub/Extensions/Extensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -internal static class Extensions -{ - public static IServiceCollection AddSignalR(this IServiceCollection services, IConfiguration configuration) - { - if (configuration.GetConnectionString("redis") is string redisConnection) - { - // TODO: Add a redis health check - services.AddSignalR().AddStackExchangeRedis(redisConnection); - } - else - { - services.AddSignalR(); - } - - // Configure hub auth (grab the token from the query string) - return services.Configure(JwtBearerDefaults.AuthenticationScheme, options => - { - options.Events = new JwtBearerEvents - { - OnMessageReceived = context => - { - var accessToken = context.Request.Query["access_token"]; - - var endpoint = context.HttpContext.GetEndpoint(); - - // Make sure this is a Hub endpoint. - if (endpoint?.Metadata.GetMetadata() is null) - { - return Task.CompletedTask; - } - - context.Token = accessToken; - - return Task.CompletedTask; - } - }; - }); - } - -} diff --git a/src/Services/Ordering/Ordering.SignalrHub/GlobalUsings.cs b/src/Services/Ordering/Ordering.SignalrHub/GlobalUsings.cs index f5db9ff53b..5cdca93456 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/GlobalUsings.cs +++ b/src/Services/Ordering/Ordering.SignalrHub/GlobalUsings.cs @@ -1,10 +1,33 @@ -global using Microsoft.AspNetCore.Authentication.JwtBearer; +global using Autofac.Extensions.DependencyInjection; +global using Autofac; +global using HealthChecks.UI.Client; +global using Microsoft.AspNetCore.Authentication.JwtBearer; global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Diagnostics.HealthChecks; +global using Microsoft.AspNetCore.Hosting; global using Microsoft.AspNetCore.SignalR; +global using Microsoft.AspNetCore; +global using Azure.Messaging.ServiceBus; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -global using Microsoft.eShopOnContainers.Services.Ordering.SignalrHub; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; +global using Microsoft.eShopOnContainers.Services.Ordering.SignalrHub.IntegrationEvents.Events; global using Microsoft.eShopOnContainers.Services.Ordering.SignalrHub.IntegrationEvents; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Diagnostics.HealthChecks; +global using Microsoft.Extensions.Logging; +global using Microsoft.eShopOnContainers.Services.Ordering.SignalrHub.AutofacModules; global using Microsoft.eShopOnContainers.Services.Ordering.SignalrHub.IntegrationEvents.EventHandling; -global using Microsoft.eShopOnContainers.Services.Ordering.SignalrHub.IntegrationEvents.Events; -global using Services.Common; +global using Microsoft.eShopOnContainers.Services.Ordering.SignalrHub; +global using RabbitMQ.Client; +global using Serilog.Context; +global using Serilog; +global using System.IdentityModel.Tokens.Jwt; +global using System.IO; +global using System.Reflection; +global using System.Threading.Tasks; +global using System; diff --git a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs index 02d118d49d..341b781ab5 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs @@ -16,9 +16,9 @@ public OrderStatusChangedToAwaitingValidationIntegrationEventHandler( public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); await _hubContext.Clients .Group(@event.BuyerName) diff --git a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToCancelledIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToCancelledIntegrationEventHandler.cs index 5c07407e6c..aba5091b96 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToCancelledIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToCancelledIntegrationEventHandler.cs @@ -16,13 +16,13 @@ public OrderStatusChangedToCancelledIntegrationEventHandler( public async Task Handle(OrderStatusChangedToCancelledIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); await _hubContext.Clients .Group(@event.BuyerName) .SendAsync("UpdatedOrderState", new { OrderId = @event.OrderId, Status = @event.OrderStatus }); } } -} +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs index 1b8488076a..03fc043569 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs @@ -16,9 +16,9 @@ public OrderStatusChangedToPaidIntegrationEventHandler( public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); await _hubContext.Clients .Group(@event.BuyerName) diff --git a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToShippedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToShippedIntegrationEventHandler.cs index ec448e8c5e..663f7ae3a4 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToShippedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToShippedIntegrationEventHandler.cs @@ -13,11 +13,12 @@ public OrderStatusChangedToShippedIntegrationEventHandler( _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + public async Task Handle(OrderStatusChangedToShippedIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); await _hubContext.Clients .Group(@event.BuyerName) diff --git a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs index b6b04f4b9a..aa662065d4 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs @@ -17,9 +17,9 @@ public OrderStatusChangedToStockConfirmedIntegrationEventHandler( public async Task Handle(OrderStatusChangedToStockConfirmedIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); await _hubContext.Clients .Group(@event.BuyerName) diff --git a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToSubmittedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToSubmittedIntegrationEventHandler.cs index fad3a38c7d..0f80420135 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToSubmittedIntegrationEventHandler.cs +++ b/src/Services/Ordering/Ordering.SignalrHub/IntegrationEvents/EventHandling/OrderStatusChangedToSubmittedIntegrationEventHandler.cs @@ -1,4 +1,4 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.SignalrHub.IntegrationEvents.EventHandling; +namespace Microsoft.eShopOnContainers.Services.Ordering.SignalrHub.IntegrationEvents.EventHandling; public class OrderStatusChangedToSubmittedIntegrationEventHandler : IIntegrationEventHandler @@ -17,9 +17,9 @@ public OrderStatusChangedToSubmittedIntegrationEventHandler( public async Task Handle(OrderStatusChangedToSubmittedIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); await _hubContext.Clients .Group(@event.BuyerName) diff --git a/src/Services/Ordering/Ordering.SignalrHub/NotificationHub.cs b/src/Services/Ordering/Ordering.SignalrHub/NotificationHub.cs index b229600965..9f545bba46 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/NotificationHub.cs +++ b/src/Services/Ordering/Ordering.SignalrHub/NotificationHub.cs @@ -3,6 +3,7 @@ [Authorize] public class NotificationsHub : Hub { + public override async Task OnConnectedAsync() { await Groups.AddToGroupAsync(Context.ConnectionId, Context.User.Identity.Name); diff --git a/src/Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj b/src/Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj index f24fd1e05a..ac69727e24 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj +++ b/src/Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj @@ -2,17 +2,39 @@ net7.0 - enable ..\..\..\..\docker-compose.dcproj + false + true - - + + + + + + + + + + + + + + + + + + + + + - + + + diff --git a/src/Services/Ordering/Ordering.SignalrHub/Program.cs b/src/Services/Ordering/Ordering.SignalrHub/Program.cs index 1b0abd0994..ed1edf59ee 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/Program.cs +++ b/src/Services/Ordering/Ordering.SignalrHub/Program.cs @@ -1,29 +1,63 @@ -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); - -builder.Services.AddSignalR(builder.Configuration); - -builder.Services.AddSingleton, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); -builder.Services.AddSingleton, OrderStatusChangedToCancelledIntegrationEventHandler>(); -builder.Services.AddSingleton, OrderStatusChangedToPaidIntegrationEventHandler>(); -builder.Services.AddSingleton, OrderStatusChangedToShippedIntegrationEventHandler>(); -builder.Services.AddSingleton, OrderStatusChangedToStockConfirmedIntegrationEventHandler>(); -builder.Services.AddSingleton, OrderStatusChangedToSubmittedIntegrationEventHandler>(); - -var app = builder.Build(); - -app.UseServiceDefaults(); - -app.MapHub("/hub/notificationhub"); - -var eventBus = app.Services.GetRequiredService(); - -eventBus.Subscribe(); -eventBus.Subscribe(); -eventBus.Subscribe(); -eventBus.Subscribe(); -eventBus.Subscribe(); -eventBus.Subscribe(); - -await app.RunAsync(); +var configuration = GetConfiguration(); + +Log.Logger = CreateSerilogLogger(configuration); + +try +{ + Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); + var host = BuildWebHost(configuration, args); + + Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); + host.Run(); + + return 0; +} +catch (Exception ex) +{ + Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} + + +static IWebHost BuildWebHost(IConfiguration configuration, string[] args) => + WebHost.CreateDefaultBuilder(args) + .CaptureStartupErrors(false) + .ConfigureAppConfiguration(x => x.AddConfiguration(configuration)) + .UseStartup() + .UseSerilog() + .Build(); + +static Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) +{ + var seqServerUrl = configuration["Serilog:SeqServerUrl"]; + var logstashUrl = configuration["Serilog:LogstashgUrl"]; + return new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithProperty("ApplicationContext", Program.AppName) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) + .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl, null) + .ReadFrom.Configuration(configuration) + .CreateLogger(); +} + +static IConfiguration GetConfiguration() +{ + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + + return builder.Build(); +} + +public partial class Program +{ + public static string Namespace = typeof(Startup).Namespace; + public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.SignalrHub/Properties/launchSettings.json b/src/Services/Ordering/Ordering.SignalrHub/Properties/launchSettings.json index f0a9157cfc..3ff683a082 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/Properties/launchSettings.json +++ b/src/Services/Ordering/Ordering.SignalrHub/Properties/launchSettings.json @@ -1,12 +1,27 @@ { + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51311/", + "sslPort": 0 + } + }, "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, "Ordering.SignalrHub": { "commandName": "Project", - "launchBrowser": false, - "applicationUrl": "http://localhost:5225/", + "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "http://localhost:51312/" } } } diff --git a/src/Services/Ordering/Ordering.SignalrHub/Startup.cs b/src/Services/Ordering/Ordering.SignalrHub/Startup.cs new file mode 100644 index 0000000000..608220bcd5 --- /dev/null +++ b/src/Services/Ordering/Ordering.SignalrHub/Startup.cs @@ -0,0 +1,256 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; + +namespace Microsoft.eShopOnContainers.Services.Ordering.SignalrHub; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public IServiceProvider ConfigureServices(IServiceCollection services) + { + services + .AddCustomHealthCheck(Configuration) + .AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .AllowAnyMethod() + .AllowAnyHeader() + .SetIsOriginAllowed((host) => true) + .AllowCredentials()); + }); + + if (Configuration.GetValue("IsClusterEnv") == bool.TrueString) + { + services + .AddSignalR() + .AddStackExchangeRedis(Configuration["SignalrStoreConnectionString"]); + } + else + { + services.AddSignalR(); + } + + if (Configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusConnectionString = Configuration["EventBusConnection"]; + + var subscriptionClientName = Configuration["SubscriptionClientName"]; + + return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); + }); + } + else + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + + + var factory = new ConnectionFactory() + { + HostName = Configuration["EventBusConnection"], + DispatchConsumersAsync = true + }; + + if (!string.IsNullOrEmpty(Configuration["EventBusUserName"])) + { + factory.UserName = Configuration["EventBusUserName"]; + } + + if (!string.IsNullOrEmpty(Configuration["EventBusPassword"])) + { + factory.Password = Configuration["EventBusPassword"]; + } + + var retryCount = 5; + if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(Configuration["EventBusRetryCount"]); + } + + return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); + }); + } + + ConfigureAuthService(services); + + RegisterEventBus(services); + + services.AddOptions(); + + //configure autofac + var container = new ContainerBuilder(); + container.RegisterModule(new ApplicationModule()); + container.Populate(services); + + return new AutofacServiceProvider(container.Build()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + //loggerFactory.AddConsole(Configuration.GetSection("Logging")); + //loggerFactory.AddDebug(); + //loggerFactory.AddAzureWebAppDiagnostics(); + //loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); + + var pathBase = Configuration["PATH_BASE"]; + + if (!string.IsNullOrEmpty(pathBase)) + { + loggerFactory.CreateLogger().LogDebug("Using PATH BASE '{pathBase}'", pathBase); + app.UsePathBase(pathBase); + } + + app.UseRouting(); + app.UseCors("CorsPolicy"); + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + endpoints.MapHub("/hub/notificationhub"); + }); + + ConfigureEventBus(app); + } + + private void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); + } + + private void ConfigureAuthService(IServiceCollection services) + { + // prevent from mapping "sub" claim to nameidentifier. + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); + + var identityUrl = Configuration.GetValue("IdentityUrl"); + + services.AddAuthentication("Bearer").AddJwtBearer(options => + { + options.Authority = identityUrl; + options.RequireHttpsMetadata = false; + options.Audience = "orders.signalrhub"; + options.TokenValidationParameters.ValidateAudience = false; + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/hub/notificationhub"))) + { + context.Token = accessToken; + } + return Task.CompletedTask; + } + }; + }); + services.AddAuthorization(options => + { + options.AddPolicy("ApiScope", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim("scope", "orders.signalrhub"); + }); + }); + } + + private void RegisterEventBus(IServiceCollection services) + { + if (Configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + string subscriptionName = Configuration["SubscriptionClientName"]; + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, + eventBusSubcriptionsManager, iLifetimeScope, subscriptionName); + }); + } + else + { + services.AddSingleton(sp => + { + var subscriptionClientName = Configuration["SubscriptionClientName"]; + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + + var retryCount = 5; + if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(Configuration["EventBusRetryCount"]); + } + + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); + }); + } + + services.AddSingleton(); + } +} + +public static class CustomExtensionMethods +{ + public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder + .AddAzureServiceBusTopic( + configuration["EventBusConnection"], + topicName: "eshop_event_bus", + name: "signalr-servicebus-check", + tags: new string[] { "servicebus" }); + } + else + { + hcBuilder + .AddRabbitMQ( + $"amqp://{configuration["EventBusConnection"]}", + name: "signalr-rabbitmqbus-check", + tags: new string[] { "rabbitmqbus" }); + } + + return services; + } +} diff --git a/src/Services/Ordering/Ordering.SignalrHub/appsettings.json b/src/Services/Ordering/Ordering.SignalrHub/appsettings.json index 887e4afdd1..e43c354a33 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/appsettings.json +++ b/src/Services/Ordering/Ordering.SignalrHub/appsettings.json @@ -1,19 +1,19 @@ { - "Logging": { - "LogLevel": { + "IdentityUrl": "http://localhost:5105", + "Serilog": { + "SeqServerUrl": null, + "LogstashgUrl": null, + "MinimumLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Override": { + "Microsoft": "Warning", + "Microsoft.eShopOnContainers": "Information", + "System": "Warning" + } } }, - "Identity": { - "Audience": "orders.signalrhub", - "Url": "http://localhost:5223" - }, - "EventBus": { - "SubscriptionClientName": "Ordering.signalrhub", - "RetryCount": 5 - }, - "ConnectionStrings": { - "EventBus": "localhost" - } + "AzureServiceBusEnabled": false, + "SubscriptionClientName": "Ordering.signalrhub", + "EventBusRetryCount": 5, + "EventBusConnection": "localhost" } \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.SignalrHub/azds.yaml b/src/Services/Ordering/Ordering.SignalrHub/azds.yaml new file mode 100644 index 0000000000..a985263947 --- /dev/null +++ b/src/Services/Ordering/Ordering.SignalrHub/azds.yaml @@ -0,0 +1,56 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: ..\..\..\.. + dockerfile: Dockerfile +install: + chart: ../../../../k8s/helm/ordering-signalrhub + set: + replicaCount: 1 + image: + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + - $(spacePrefix)eshop$(hostSuffix) + inf: + k8s: + dns: $(spacePrefix)eshop$(hostSuffix) + values: + - values.dev.yaml? + - secrets.dev.yaml? + - inf.yaml + - app.yaml +configurations: + develop: + build: + useGitIgnore: true + dockerfile: Dockerfile.develop + args: + BUILD_CONFIGURATION: ${BUILD_CONFIGURATION:-Debug} + container: + sync: + - '**/Pages/**' + - '**/Views/**' + - '**/wwwroot/**' + - '!**/*.{sln,csproj}' + command: + - dotnet + - run + - --no-restore + - --no-build + - --no-launch-profile + - -c + - ${BUILD_CONFIGURATION:-Debug} + iterate: + processesToKill: + - dotnet + - vsdbg + buildCommands: + - - dotnet + - build + - --no-restore + - -c + - ${BUILD_CONFIGURATION:-Debug} diff --git a/src/Services/Ordering/Ordering.UnitTests/Application/IdentifiedCommandHandlerTest.cs b/src/Services/Ordering/Ordering.UnitTests/Application/IdentifiedCommandHandlerTest.cs index 4c7c276e64..1b7d388649 100644 --- a/src/Services/Ordering/Ordering.UnitTests/Application/IdentifiedCommandHandlerTest.cs +++ b/src/Services/Ordering/Ordering.UnitTests/Application/IdentifiedCommandHandlerTest.cs @@ -23,16 +23,17 @@ public async Task Handler_sends_command_when_order_no_exists() _requestManager.Setup(x => x.ExistAsync(It.IsAny())) .Returns(Task.FromResult(false)); - _mediator.Setup(x => x.Send(It.IsAny>(), default)) + _mediator.Setup(x => x.Send(It.IsAny>(), default(System.Threading.CancellationToken))) .Returns(Task.FromResult(true)); - // Act - var handler = new CreateOrderIdentifiedCommandHandler(_mediator.Object, _requestManager.Object, _loggerMock.Object); - var result = await handler.Handle(fakeOrderCmd, CancellationToken.None); + //Act + var handler = new IdentifiedCommandHandler(_mediator.Object, _requestManager.Object, _loggerMock.Object); + var cltToken = new System.Threading.CancellationToken(); + var result = await handler.Handle(fakeOrderCmd, cltToken); - // Assert + //Assert Assert.True(result); - _mediator.Verify(x => x.Send(It.IsAny>(), default), Times.Once()); + _mediator.Verify(x => x.Send(It.IsAny>(), default(System.Threading.CancellationToken)), Times.Once()); } [Fact] @@ -45,15 +46,17 @@ public async Task Handler_sends_no_command_when_order_already_exists() _requestManager.Setup(x => x.ExistAsync(It.IsAny())) .Returns(Task.FromResult(true)); - _mediator.Setup(x => x.Send(It.IsAny>(), default)) + _mediator.Setup(x => x.Send(It.IsAny>(), default(System.Threading.CancellationToken))) .Returns(Task.FromResult(true)); - // Act - var handler = new CreateOrderIdentifiedCommandHandler(_mediator.Object, _requestManager.Object, _loggerMock.Object); - var result = await handler.Handle(fakeOrderCmd, CancellationToken.None); + //Act + var handler = new IdentifiedCommandHandler(_mediator.Object, _requestManager.Object, _loggerMock.Object); + var cltToken = new System.Threading.CancellationToken(); + var result = await handler.Handle(fakeOrderCmd, cltToken); - // Assert - _mediator.Verify(x => x.Send(It.IsAny>(), default), Times.Never()); + //Assert + Assert.False(result); + _mediator.Verify(x => x.Send(It.IsAny>(), default(System.Threading.CancellationToken)), Times.Never()); } private CreateOrderCommand FakeOrderRequest(Dictionary args = null) diff --git a/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs b/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs index 6d6cc19045..184a5f5d21 100644 --- a/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs +++ b/src/Services/Ordering/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs @@ -29,9 +29,9 @@ public async Task Handle_return_false_if_order_is_not_persisted() { ["cardExpiration"] = DateTime.Now.AddYears(1) }); _orderRepositoryMock.Setup(orderRepo => orderRepo.GetAsync(It.IsAny())) - .Returns(Task.FromResult(FakeOrder())); + .Returns(Task.FromResult(FakeOrder())); - _orderRepositoryMock.Setup(buyerRepo => buyerRepo.UnitOfWork.SaveChangesAsync(default)) + _orderRepositoryMock.Setup(buyerRepo => buyerRepo.UnitOfWork.SaveChangesAsync(default(CancellationToken))) .Returns(Task.FromResult(1)); _identityServiceMock.Setup(svc => svc.GetUserIdentity()).Returns(buyerId); @@ -39,7 +39,7 @@ public async Task Handle_return_false_if_order_is_not_persisted() var LoggerMock = new Mock>(); //Act var handler = new CreateOrderCommandHandler(_mediator.Object, _orderingIntegrationEventService.Object, _orderRepositoryMock.Object, _identityServiceMock.Object, LoggerMock.Object); - var cltToken = new CancellationToken(); + var cltToken = new System.Threading.CancellationToken(); var result = await handler.Handle(fakeOrderCmd, cltToken); //Assert diff --git a/src/Services/Ordering/Ordering.UnitTests/Application/OrdersWebApiTest.cs b/src/Services/Ordering/Ordering.UnitTests/Application/OrdersWebApiTest.cs index 0235c94b62..cfa14a3ec3 100644 --- a/src/Services/Ordering/Ordering.UnitTests/Application/OrdersWebApiTest.cs +++ b/src/Services/Ordering/Ordering.UnitTests/Application/OrdersWebApiTest.cs @@ -21,7 +21,7 @@ public OrdersWebApiTest() public async Task Cancel_order_with_requestId_success() { //Arrange - _mediatorMock.Setup(x => x.Send(It.IsAny>(), default)) + _mediatorMock.Setup(x => x.Send(It.IsAny>(), default(CancellationToken))) .Returns(Task.FromResult(true)); //Act @@ -29,7 +29,7 @@ public async Task Cancel_order_with_requestId_success() var actionResult = await orderController.CancelOrderAsync(new CancelOrderCommand(1), Guid.NewGuid().ToString()) as OkResult; //Assert - Assert.Equal((int)System.Net.HttpStatusCode.OK, actionResult.StatusCode); + Assert.Equal(actionResult.StatusCode, (int)System.Net.HttpStatusCode.OK); } @@ -37,22 +37,22 @@ public async Task Cancel_order_with_requestId_success() public async Task Cancel_order_bad_request() { //Arrange - _mediatorMock.Setup(x => x.Send(It.IsAny>(), default)) + _mediatorMock.Setup(x => x.Send(It.IsAny>(), default(CancellationToken))) .Returns(Task.FromResult(true)); //Act var orderController = new OrdersController(_mediatorMock.Object, _orderQueriesMock.Object, _identityServiceMock.Object, _loggerMock.Object); - var actionResult = await orderController.CancelOrderAsync(new CancelOrderCommand(1), string.Empty) as BadRequestResult; + var actionResult = await orderController.CancelOrderAsync(new CancelOrderCommand(1), String.Empty) as BadRequestResult; //Assert - Assert.Equal((int)System.Net.HttpStatusCode.BadRequest, actionResult.StatusCode); + Assert.Equal(actionResult.StatusCode, (int)System.Net.HttpStatusCode.BadRequest); } [Fact] public async Task Ship_order_with_requestId_success() { //Arrange - _mediatorMock.Setup(x => x.Send(It.IsAny>(), default)) + _mediatorMock.Setup(x => x.Send(It.IsAny>(), default(System.Threading.CancellationToken))) .Returns(Task.FromResult(true)); //Act @@ -60,7 +60,7 @@ public async Task Ship_order_with_requestId_success() var actionResult = await orderController.ShipOrderAsync(new ShipOrderCommand(1), Guid.NewGuid().ToString()) as OkResult; //Assert - Assert.Equal((int)System.Net.HttpStatusCode.OK, actionResult.StatusCode); + Assert.Equal(actionResult.StatusCode, (int)System.Net.HttpStatusCode.OK); } @@ -68,15 +68,15 @@ public async Task Ship_order_with_requestId_success() public async Task Ship_order_bad_request() { //Arrange - _mediatorMock.Setup(x => x.Send(It.IsAny>(), default)) + _mediatorMock.Setup(x => x.Send(It.IsAny>(), default(System.Threading.CancellationToken))) .Returns(Task.FromResult(true)); //Act var orderController = new OrdersController(_mediatorMock.Object, _orderQueriesMock.Object, _identityServiceMock.Object, _loggerMock.Object); - var actionResult = await orderController.ShipOrderAsync(new ShipOrderCommand(1), string.Empty) as BadRequestResult; + var actionResult = await orderController.ShipOrderAsync(new ShipOrderCommand(1), String.Empty) as BadRequestResult; //Assert - Assert.Equal((int)System.Net.HttpStatusCode.BadRequest, actionResult.StatusCode); + Assert.Equal(actionResult.StatusCode, (int)System.Net.HttpStatusCode.BadRequest); } [Fact] @@ -110,10 +110,10 @@ public async Task Get_order_success() //Act var orderController = new OrdersController(_mediatorMock.Object, _orderQueriesMock.Object, _identityServiceMock.Object, _loggerMock.Object); - var actionResult = await orderController.GetOrderAsync(fakeOrderId); + var actionResult = await orderController.GetOrderAsync(fakeOrderId) as OkObjectResult; //Assert - Assert.Same(actionResult.Value, fakeDynamicResult); + Assert.Equal(actionResult.StatusCode, (int)System.Net.HttpStatusCode.OK); } [Fact] diff --git a/src/Services/Ordering/Ordering.UnitTests/GlobalUsings.cs b/src/Services/Ordering/Ordering.UnitTests/GlobalUsings.cs index 86a296fecd..9fa77aef72 100644 --- a/src/Services/Ordering/Ordering.UnitTests/GlobalUsings.cs +++ b/src/Services/Ordering/Ordering.UnitTests/GlobalUsings.cs @@ -1,15 +1,23 @@ -global using MediatR; -global using Microsoft.AspNetCore.Mvc; -global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; +global using System; global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Models; -global using Microsoft.eShopOnContainers.Services.Ordering.API.Controllers; -global using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services; -global using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; -global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Events; -global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Exceptions; -global using Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork; +global using MediatR; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; global using Microsoft.Extensions.Logging; global using Moq; -global using UnitTest.Ordering; +global using System.Collections.Generic; +global using System.Threading.Tasks; global using Xunit; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services; +global using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; +global using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; +global using System.Threading; +global using global::Ordering.API.Application.IntegrationEvents; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Controllers; +global using System.Linq; +global using Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork; +global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Exceptions; +global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Events; +global using UnitTest.Ordering; diff --git a/src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj b/src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj index 688e5297fb..e004f9c8c2 100644 --- a/src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj +++ b/src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj @@ -2,22 +2,20 @@ net7.0 - enable false - false false - - - - - + + + + + all runtime; build; native; contentfiles; analyzers - + diff --git a/src/Services/Payment/Payment.API/Dockerfile b/src/Services/Payment/Payment.API/Dockerfile index d1bcc6bdaa..4b17cb3bd4 100644 --- a/src/Services/Payment/Payment.API/Dockerfile +++ b/src/Services/Payment/Payment.API/Dockerfile @@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" @@ -32,7 +33,6 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" -COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" @@ -42,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" COPY "docker-compose.dcproj" "docker-compose.dcproj" -COPY "Directory.Packages.props" "Directory.Packages.props" COPY "NuGet.config" "NuGet.config" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" diff --git a/src/Services/Payment/Payment.API/GlobalUsings.cs b/src/Services/Payment/Payment.API/GlobalUsings.cs index 575aaedc45..e87eb53bb6 100644 --- a/src/Services/Payment/Payment.API/GlobalUsings.cs +++ b/src/Services/Payment/Payment.API/GlobalUsings.cs @@ -1,7 +1,29 @@ -global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +global using Autofac.Extensions.DependencyInjection; +global using Autofac; +global using Azure.Core; +global using Azure.Identity; +global using HealthChecks.UI.Client; +global using Microsoft.AspNetCore.Diagnostics.HealthChecks; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore; +global using Azure.Messaging.ServiceBus; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -global using Microsoft.eShopOnContainers.Payment.API; -global using Microsoft.eShopOnContainers.Payment.API.IntegrationEvents.EventHandling; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; global using Microsoft.eShopOnContainers.Payment.API.IntegrationEvents.Events; +global using Microsoft.Extensions.Diagnostics.HealthChecks; global using Microsoft.Extensions.Options; -global using Services.Common; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using Microsoft.eShopOnContainers.Payment.API.IntegrationEvents.EventHandling; +global using Microsoft.eShopOnContainers.Payment.API; +global using RabbitMQ.Client; +global using Serilog.Context; +global using Serilog; +global using System.Threading.Tasks; +global using System; +global using System.IO; +global using Microsoft.AspNetCore.Hosting; \ No newline at end of file diff --git a/src/Services/Payment/Payment.API/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs b/src/Services/Payment/Payment.API/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs index 140f7acad2..ce9aa4d739 100644 --- a/src/Services/Payment/Payment.API/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs +++ b/src/Services/Payment/Payment.API/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs @@ -1,5 +1,5 @@ namespace Microsoft.eShopOnContainers.Payment.API.IntegrationEvents.EventHandling; - + public class OrderStatusChangedToStockConfirmedIntegrationEventHandler : IIntegrationEventHandler { @@ -14,16 +14,16 @@ public OrderStatusChangedToStockConfirmedIntegrationEventHandler( { _eventBus = eventBus; _settings = settings.Value; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); _logger.LogTrace("PaymentSettings: {@PaymentSettings}", _settings); } public async Task Handle(OrderStatusChangedToStockConfirmedIntegrationEvent @event) { - using (_logger.BeginScope(new List> { new("IntegrationEventContext", @event.Id) })) + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) { - _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event); + _logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); IntegrationEvent orderPaymentIntegrationEvent; @@ -42,7 +42,7 @@ public async Task Handle(OrderStatusChangedToStockConfirmedIntegrationEvent @eve orderPaymentIntegrationEvent = new OrderPaymentFailedIntegrationEvent(@event.OrderId); } - _logger.LogInformation("Publishing integration event: {IntegrationEventId} - ({@IntegrationEvent})", orderPaymentIntegrationEvent.Id, orderPaymentIntegrationEvent); + _logger.LogInformation("----- Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", orderPaymentIntegrationEvent.Id, Program.AppName, orderPaymentIntegrationEvent); _eventBus.Publish(orderPaymentIntegrationEvent); diff --git a/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs b/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs index 7b7be67d70..adac9f2bdb 100644 --- a/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs +++ b/src/Services/Payment/Payment.API/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs @@ -1,5 +1,5 @@ namespace Microsoft.eShopOnContainers.Payment.API.IntegrationEvents.Events; - + public record OrderStatusChangedToStockConfirmedIntegrationEvent : IntegrationEvent { public int OrderId { get; } diff --git a/src/Services/Payment/Payment.API/Payment.API.csproj b/src/Services/Payment/Payment.API/Payment.API.csproj index 01e401dd12..02c255fa6e 100644 --- a/src/Services/Payment/Payment.API/Payment.API.csproj +++ b/src/Services/Payment/Payment.API/Payment.API.csproj @@ -2,12 +2,37 @@ net7.0 - enable ..\..\..\..\docker-compose.dcproj + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + true - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Services/Payment/Payment.API/Program.cs b/src/Services/Payment/Payment.API/Program.cs index ee68397db1..3e49d1ee5a 100644 --- a/src/Services/Payment/Payment.API/Program.cs +++ b/src/Services/Payment/Payment.API/Program.cs @@ -1,17 +1,75 @@ -var builder = WebApplication.CreateBuilder(args); +var configuration = GetConfiguration(); -builder.AddServiceDefaults(); +Log.Logger = CreateSerilogLogger(configuration); -builder.Services.Configure(builder.Configuration); +try +{ + Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); + var host = BuildWebHost(configuration, args); -builder.Services.AddTransient(); + Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); + host.Run(); -var app = builder.Build(); + return 0; +} +catch (Exception ex) +{ + Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} -app.UseServiceDefaults(); -var eventBus = app.Services.GetRequiredService(); +IWebHost BuildWebHost(IConfiguration configuration, string[] args) => + WebHost.CreateDefaultBuilder(args) + .CaptureStartupErrors(false) + .ConfigureAppConfiguration(x => x.AddConfiguration(configuration)) + .UseStartup() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseSerilog() + .Build(); -eventBus.Subscribe(); +Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) +{ + var seqServerUrl = configuration["Serilog:SeqServerUrl"]; + var logstashUrl = configuration["Serilog:LogstashgUrl"]; + return new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithProperty("ApplicationContext", Program.AppName) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) + .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl, null) + .ReadFrom.Configuration(configuration) + .CreateLogger(); +} -await app.RunAsync(); +IConfiguration GetConfiguration() +{ + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + + var config = builder.Build(); + + if (config.GetValue("UseVault", false)) + { + TokenCredential credential = new ClientSecretCredential( + config["Vault:TenantId"], + config["Vault:ClientId"], + config["Vault:ClientSecret"]); + builder.AddAzureKeyVault(new Uri($"https://{config["Vault:Name"]}.vault.azure.net/"), credential); + } + + return builder.Build(); +} + +public partial class Program +{ + public static string Namespace = typeof(Startup).Namespace; + public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); +} \ No newline at end of file diff --git a/src/Services/Payment/Payment.API/Properties/launchSettings.json b/src/Services/Payment/Payment.API/Properties/launchSettings.json index 8d137d73fd..5eac4c0921 100644 --- a/src/Services/Payment/Payment.API/Properties/launchSettings.json +++ b/src/Services/Payment/Payment.API/Properties/launchSettings.json @@ -1,12 +1,29 @@ { + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:63336/", + "sslPort": 0 + } + }, "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, "Payment.API": { "commandName": "Project", - "launchBrowser": false, - "applicationUrl": "http://localhost:5226", + "launchBrowser": true, + "launchUrl": "api/values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "http://localhost:3331" } } } \ No newline at end of file diff --git a/src/Services/Payment/Payment.API/Startup.cs b/src/Services/Payment/Payment.API/Startup.cs new file mode 100644 index 0000000000..c880f2a75b --- /dev/null +++ b/src/Services/Payment/Payment.API/Startup.cs @@ -0,0 +1,178 @@ +namespace Microsoft.eShopOnContainers.Payment.API; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public IServiceProvider ConfigureServices(IServiceCollection services) + { + services.AddCustomHealthCheck(Configuration); + services.Configure(Configuration); + + RegisterAppInsights(services); + + if (Configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusConnectionString = Configuration["EventBusConnection"]; + var subscriptionClientName = Configuration["SubscriptionClientName"]; + + return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); + }); + } + else + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + var factory = new ConnectionFactory() + { + HostName = Configuration["EventBusConnection"], + DispatchConsumersAsync = true + }; + + if (!string.IsNullOrEmpty(Configuration["EventBusUserName"])) + { + factory.UserName = Configuration["EventBusUserName"]; + } + + if (!string.IsNullOrEmpty(Configuration["EventBusPassword"])) + { + factory.Password = Configuration["EventBusPassword"]; + } + + var retryCount = 5; + if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(Configuration["EventBusRetryCount"]); + } + + return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); + }); + } + + RegisterEventBus(services); + + var container = new ContainerBuilder(); + container.Populate(services); + return new AutofacServiceProvider(container.Build()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + //loggerFactory.AddAzureWebAppDiagnostics(); + //loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); + + var pathBase = Configuration["PATH_BASE"]; + if (!string.IsNullOrEmpty(pathBase)) + { + app.UsePathBase(pathBase); + } + + ConfigureEventBus(app); + + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + } + + private void RegisterAppInsights(IServiceCollection services) + { + services.AddApplicationInsightsTelemetry(Configuration); + services.AddApplicationInsightsKubernetesEnricher(); + } + + private void RegisterEventBus(IServiceCollection services) + { + if (Configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + string subscriptionName = Configuration["SubscriptionClientName"]; + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, + eventBusSubcriptionsManager, iLifetimeScope, subscriptionName); + }); + } + else + { + services.AddSingleton(sp => + { + var subscriptionClientName = Configuration["SubscriptionClientName"]; + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + + var retryCount = 5; + if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(Configuration["EventBusRetryCount"]); + } + + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); + }); + } + + services.AddTransient(); + services.AddSingleton(); + } + + private void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + eventBus.Subscribe(); + } +} + +public static class CustomExtensionMethods +{ + public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder + .AddAzureServiceBusTopic( + configuration["EventBusConnection"], + topicName: "eshop_event_bus", + name: "payment-servicebus-check", + tags: new string[] { "servicebus" }); + } + else + { + hcBuilder + .AddRabbitMQ( + $"amqp://{configuration["EventBusConnection"]}", + name: "payment-rabbitmqbus-check", + tags: new string[] { "rabbitmqbus" }); + } + + return services; + } +} diff --git a/src/Services/Payment/Payment.API/appsettings.Development.json b/src/Services/Payment/Payment.API/appsettings.Development.json index e2f168cc88..fa8ce71a97 100644 --- a/src/Services/Payment/Payment.API/appsettings.Development.json +++ b/src/Services/Payment/Payment.API/appsettings.Development.json @@ -1,8 +1,6 @@ { "Logging": { - "Console": { - "IncludeScopes": false - }, + "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", diff --git a/src/Services/Payment/Payment.API/appsettings.json b/src/Services/Payment/Payment.API/appsettings.json index 71e1aa6af0..9964a8bd28 100644 --- a/src/Services/Payment/Payment.API/appsettings.json +++ b/src/Services/Payment/Payment.API/appsettings.json @@ -1,16 +1,21 @@ { - "Logging": { - "LogLevel": { + "Serilog": { + "SeqServerUrl": null, + "LogstashgUrl": null, + "MinimumLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Override": { + "Microsoft": "Warning", + "Microsoft.eShopOnContainers": "Information", + "System": "Warning" + } } }, - "ConnectionStrings": { - "EventBus": "localhost" + "PaymentSucceeded": true, + "AzureServiceBusEnabled": false, + "SubscriptionClientName": "Payment", + "ApplicationInsights": { + "InstrumentationKey": "" }, - "EventBus": { - "SubscriptionClientName": "Payment", - "RetryCount": 5 - }, - "PaymentSucceeded": true -} \ No newline at end of file + "EventBusRetryCount": 5 +} diff --git a/src/Services/Payment/Payment.API/azds.yaml b/src/Services/Payment/Payment.API/azds.yaml new file mode 100644 index 0000000000..2536d43716 --- /dev/null +++ b/src/Services/Payment/Payment.API/azds.yaml @@ -0,0 +1,56 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: ..\..\..\.. + dockerfile: Dockerfile +install: + chart: ../../../../k8s/helm/payment-api + set: + replicaCount: 1 + image: + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + - $(spacePrefix)eshop$(hostSuffix) + inf: + k8s: + dns: $(spacePrefix)eshop$(hostSuffix) + values: + - values.dev.yaml? + - secrets.dev.yaml? + - inf.yaml + - app.yaml +configurations: + develop: + build: + useGitIgnore: true + dockerfile: Dockerfile.develop + args: + BUILD_CONFIGURATION: ${BUILD_CONFIGURATION:-Debug} + container: + sync: + - '**/Pages/**' + - '**/Views/**' + - '**/wwwroot/**' + - '!**/*.{sln,csproj}' + command: + - dotnet + - run + - --no-restore + - --no-build + - --no-launch-profile + - -c + - ${BUILD_CONFIGURATION:-Debug} + iterate: + processesToKill: + - dotnet + - vsdbg + buildCommands: + - - dotnet + - build + - --no-restore + - -c + - ${BUILD_CONFIGURATION:-Debug} diff --git a/src/Services/Services.Common/AuthorizeCheckOperationFilter.cs b/src/Services/Services.Common/AuthorizeCheckOperationFilter.cs deleted file mode 100644 index fd7d14b3f6..0000000000 --- a/src/Services/Services.Common/AuthorizeCheckOperationFilter.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.Configuration; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Services.Common; -internal class AuthorizeCheckOperationFilter : IOperationFilter -{ - private readonly IConfiguration _configuration; - - public AuthorizeCheckOperationFilter(IConfiguration configuration) - { - _configuration = configuration; - } - - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - // Check for authorize attribute - var hasAuthorize = context.MethodInfo.DeclaringType?.GetCustomAttributes(true)?.OfType().Any() ?? false - || context.MethodInfo.GetCustomAttributes(true).OfType().Any(); - - if (!hasAuthorize) return; - - operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); - operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); - - var oAuthScheme = new OpenApiSecurityScheme - { - Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } - }; - - var identitySection = _configuration.GetSection("Identity"); - var scopes = identitySection.GetRequiredSection("Scopes").GetChildren().Select(r => r.Key).ToArray(); - - operation.Security = new List - { - new() - { - [ oAuthScheme ] = scopes - } - }; - } -} diff --git a/src/Services/Services.Common/CommonExtensions.cs b/src/Services/Services.Common/CommonExtensions.cs deleted file mode 100644 index 1d666edd2c..0000000000 --- a/src/Services/Services.Common/CommonExtensions.cs +++ /dev/null @@ -1,447 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using Azure.Identity; -using HealthChecks.UI.Client; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.OpenApi.Models; -using RabbitMQ.Client; - -namespace Services.Common; - -public static class CommonExtensions -{ - public static WebApplicationBuilder AddServiceDefaults(this WebApplicationBuilder builder) - { - // Shared configuration via key vault - builder.Configuration.AddKeyVault(); - - // Shared app insights configuration - builder.Services.AddApplicationInsights(builder.Configuration); - - // Default health checks assume the event bus and self health checks - builder.Services.AddDefaultHealthChecks(builder.Configuration); - - // Add the event bus - builder.Services.AddEventBus(builder.Configuration); - - builder.Services.AddDefaultAuthentication(builder.Configuration); - - builder.Services.AddDefaultOpenApi(builder.Configuration); - - // Add the accessor - builder.Services.AddHttpContextAccessor(); - - return builder; - } - - public static WebApplication UseServiceDefaults(this WebApplication app) - { - if (!app.Environment.IsDevelopment()) - { - app.UseExceptionHandler("/Home/Error"); - } - - var pathBase = app.Configuration["PATH_BASE"]; - - if (!string.IsNullOrEmpty(pathBase)) - { - app.UsePathBase(pathBase); - app.UseRouting(); - - var identitySection = app.Configuration.GetSection("Identity"); - - if (identitySection.Exists()) - { - // We have to add the auth middleware to the pipeline here - app.UseAuthentication(); - app.UseAuthorization(); - } - } - - app.UseDefaultOpenApi(app.Configuration); - - app.MapDefaultHealthChecks(); - - return app; - } - - public static async Task CheckHealthAsync(this WebApplication app) - { - app.Logger.LogInformation("Running health checks..."); - - // Do a health check on startup, this will throw an exception if any of the checks fail - var report = await app.Services.GetRequiredService().CheckHealthAsync(); - - if (report.Status == HealthStatus.Unhealthy) - { - app.Logger.LogCritical("Health checks failed!"); - foreach (var entry in report.Entries) - { - if (entry.Value.Status == HealthStatus.Unhealthy) - { - app.Logger.LogCritical("{Check}: {Status}", entry.Key, entry.Value.Status); - } - } - - return false; - } - - return true; - } - - public static IApplicationBuilder UseDefaultOpenApi(this WebApplication app, IConfiguration configuration) - { - var openApiSection = configuration.GetSection("OpenApi"); - - if (!openApiSection.Exists()) - { - return app; - } - - app.UseSwagger(); - app.UseSwaggerUI(setup => - { - /// { - /// "OpenApi": { - /// "Endpoint: { - /// "Name": - /// }, - /// "Auth": { - /// "ClientId": .., - /// "AppName": .. - /// } - /// } - /// } - - var pathBase = configuration["PATH_BASE"]; - var authSection = openApiSection.GetSection("Auth"); - var endpointSection = openApiSection.GetRequiredSection("Endpoint"); - - var swaggerUrl = endpointSection["Url"] ?? $"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json"; - - setup.SwaggerEndpoint(swaggerUrl, endpointSection.GetRequiredValue("Name")); - - if (authSection.Exists()) - { - setup.OAuthClientId(authSection.GetRequiredValue("ClientId")); - setup.OAuthAppName(authSection.GetRequiredValue("AppName")); - } - }); - - // Add a redirect from the root of the app to the swagger endpoint - app.MapGet("/", () => Results.Redirect("/swagger")).ExcludeFromDescription(); - - return app; - } - - public static IServiceCollection AddDefaultOpenApi(this IServiceCollection services, IConfiguration configuration) - { - var openApi = configuration.GetSection("OpenApi"); - - if (!openApi.Exists()) - { - return services; - } - - services.AddEndpointsApiExplorer(); - - return services.AddSwaggerGen(options => - { - /// { - /// "OpenApi": { - /// "Document": { - /// "Title": .. - /// "Version": .. - /// "Description": .. - /// } - /// } - /// } - var document = openApi.GetRequiredSection("Document"); - - var version = document.GetRequiredValue("Version") ?? "v1"; - - options.SwaggerDoc(version, new OpenApiInfo - { - Title = document.GetRequiredValue("Title"), - Version = version, - Description = document.GetRequiredValue("Description") - }); - - var identitySection = configuration.GetSection("Identity"); - - if (!identitySection.Exists()) - { - // No identity section, so no authentication open api definition - return; - } - - // { - // "Identity": { - // "ExternalUrl": "http://identity", - // "Scopes": { - // "basket": "Basket API" - // } - // } - // } - - var identityUrlExternal = identitySection["ExternalUrl"] ?? identitySection.GetRequiredValue("Url"); - var scopes = identitySection.GetRequiredSection("Scopes").GetChildren().ToDictionary(p => p.Key, p => p.Value); - - options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme - { - Type = SecuritySchemeType.OAuth2, - Flows = new OpenApiOAuthFlows() - { - Implicit = new OpenApiOAuthFlow() - { - AuthorizationUrl = new Uri($"{identityUrlExternal}/connect/authorize"), - TokenUrl = new Uri($"{identityUrlExternal}/connect/token"), - Scopes = scopes, - } - } - }); - - options.OperationFilter(); - }); - } - - public static IServiceCollection AddDefaultAuthentication(this IServiceCollection services, IConfiguration configuration) - { - // { - // "Identity": { - // "Url": "http://identity", - // "Audience": "basket" - // } - // } - - var identitySection = configuration.GetSection("Identity"); - - if (!identitySection.Exists()) - { - // No identity section, so no authentication - return services; - } - - // prevent from mapping "sub" claim to nameidentifier. - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); - - services.AddAuthentication().AddJwtBearer(options => - { - var identityUrl = identitySection.GetRequiredValue("Url"); - var audience = identitySection.GetRequiredValue("Audience"); - - options.Authority = identityUrl; - options.RequireHttpsMetadata = false; - options.Audience = audience; - options.TokenValidationParameters.ValidateAudience = false; - }); - - return services; - } - - public static ConfigurationManager AddKeyVault(this ConfigurationManager configuration) - { - // { - // "Vault": { - // "Name": "myvault", - // "TenantId": "mytenantid", - // "ClientId": "myclientid", - // } - // } - - var vaultSection = configuration.GetSection("Vault"); - - if (!vaultSection.Exists()) - { - return configuration; - } - - var credential = new ClientSecretCredential( - vaultSection.GetRequiredValue("TenantId"), - vaultSection.GetRequiredValue("ClientId"), - vaultSection.GetRequiredValue("ClientSecret")); - - var name = vaultSection.GetRequiredValue("Name"); - - configuration.AddAzureKeyVault(new Uri($"https://{name}.vault.azure.net/"), credential); - - return configuration; - } - - public static IServiceCollection AddApplicationInsights(this IServiceCollection services, IConfiguration configuration) - { - var appInsightsSection = configuration.GetSection("ApplicationInsights"); - - // No instrumentation key, so no application insights - if (string.IsNullOrEmpty(appInsightsSection["InstrumentationKey"])) - { - return services; - } - - services.AddApplicationInsightsTelemetry(configuration); - services.AddApplicationInsightsKubernetesEnricher(); - return services; - } - - public static IHealthChecksBuilder AddDefaultHealthChecks(this IServiceCollection services, IConfiguration configuration) - { - var hcBuilder = services.AddHealthChecks(); - - // Health check for the application itself - hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); - - // { - // "EventBus": { - // "ProviderName": "ServiceBus | RabbitMQ", - // } - // } - - var eventBusSection = configuration.GetSection("EventBus"); - - if (!eventBusSection.Exists()) - { - return hcBuilder; - } - - return eventBusSection["ProviderName"]?.ToLowerInvariant() switch - { - "servicebus" => hcBuilder.AddAzureServiceBusTopic( - _ => configuration.GetRequiredConnectionString("EventBus"), - _ => "eshop_event_bus", - name: "servicebus", - tags: new string[] { "ready" }), - - _ => hcBuilder.AddRabbitMQ( - _ => $"amqp://{configuration.GetRequiredConnectionString("EventBus")}", - name: "rabbitmq", - tags: new string[] { "ready" }) - }; - } - - public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) - { - // { - // "ConnectionStrings": { - // "EventBus": "..." - // }, - - // { - // "EventBus": { - // "ProviderName": "ServiceBus | RabbitMQ", - // ... - // } - // } - - // { - // "EventBus": { - // "ProviderName": "ServiceBus", - // "SubscriptionClientName": "eshop_event_bus" - // } - // } - - // { - // "EventBus": { - // "ProviderName": "RabbitMQ", - // "SubscriptionClientName": "...", - // "UserName": "...", - // "Password": "...", - // "RetryCount": 1 - // } - // } - - var eventBusSection = configuration.GetSection("EventBus"); - - if (!eventBusSection.Exists()) - { - return services; - } - - if (string.Equals(eventBusSection["ProviderName"], "ServiceBus", StringComparison.OrdinalIgnoreCase)) - { - services.AddSingleton(sp => - { - var serviceBusConnectionString = configuration.GetRequiredConnectionString("EventBus"); - - return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); - }); - - services.AddSingleton(sp => - { - var serviceBusPersisterConnection = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - var eventBusSubscriptionsManager = sp.GetRequiredService(); - string subscriptionName = eventBusSection.GetRequiredValue("SubscriptionClientName"); - - return new EventBusServiceBus(serviceBusPersisterConnection, logger, - eventBusSubscriptionsManager, sp, subscriptionName); - }); - } - else - { - services.AddSingleton(sp => - { - var logger = sp.GetRequiredService>(); - - var factory = new ConnectionFactory() - { - HostName = configuration.GetRequiredConnectionString("EventBus"), - DispatchConsumersAsync = true - }; - - if (!string.IsNullOrEmpty(eventBusSection["UserName"])) - { - factory.UserName = eventBusSection["UserName"]; - } - - if (!string.IsNullOrEmpty(eventBusSection["Password"])) - { - factory.Password = eventBusSection["Password"]; - } - - var retryCount = eventBusSection.GetValue("RetryCount", 5); - - return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); - }); - - services.AddSingleton(sp => - { - var subscriptionClientName = eventBusSection.GetRequiredValue("SubscriptionClientName"); - var rabbitMQPersistentConnection = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - var eventBusSubscriptionsManager = sp.GetRequiredService(); - var retryCount = eventBusSection.GetValue("RetryCount", 5); - - return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubscriptionsManager, subscriptionClientName, retryCount); - }); - } - - services.AddSingleton(); - return services; - } - - public static void MapDefaultHealthChecks(this IEndpointRouteBuilder routes) - { - routes.MapHealthChecks("/hc", new HealthCheckOptions() - { - Predicate = _ => true, - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse - }); - - routes.MapHealthChecks("/liveness", new HealthCheckOptions - { - Predicate = r => r.Name.Contains("self") - }); - } -} diff --git a/src/Services/Services.Common/ConfigurationExtensions.cs b/src/Services/Services.Common/ConfigurationExtensions.cs deleted file mode 100644 index 8d8c11cdd0..0000000000 --- a/src/Services/Services.Common/ConfigurationExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Microsoft.Extensions.Configuration; - -public static class ConfigurationExtensions -{ - public static string GetRequiredValue(this IConfiguration configuration, string name) => - configuration[name] ?? throw new InvalidOperationException($"Configuration missing value for: {(configuration is IConfigurationSection s ? s.Path + ":" + name : name)}"); - - public static string GetRequiredConnectionString(this IConfiguration configuration, string name) => - configuration.GetConnectionString(name) ?? throw new InvalidOperationException($"Configuration missing value for: {(configuration is IConfigurationSection s ? s.Path + ":ConnectionStrings:" + name : "ConnectionStrings:" + name)}"); -} diff --git a/src/Services/Services.Common/HttpClientAuthorizationDelegatingHandler.cs b/src/Services/Services.Common/HttpClientAuthorizationDelegatingHandler.cs deleted file mode 100644 index 09b24573c3..0000000000 --- a/src/Services/Services.Common/HttpClientAuthorizationDelegatingHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Net.Http.Headers; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; - -namespace Services.Common; - -public class HttpClientAuthorizationDelegatingHandler - : DelegatingHandler -{ - private readonly IHttpContextAccessor _httpContextAccessor; - - public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - if (_httpContextAccessor.HttpContext is HttpContext context) - { - var accessToken = await context.GetTokenAsync("access_token"); - - if (accessToken is not null) - { - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - } - } - - return await base.SendAsync(request, cancellationToken); - } -} diff --git a/src/Services/Services.Common/JsonDefaults.cs b/src/Services/Services.Common/JsonDefaults.cs deleted file mode 100644 index 5798a4a86c..0000000000 --- a/src/Services/Services.Common/JsonDefaults.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Text.Json; - -namespace Services.Common; - -public static class JsonDefaults -{ - public static readonly JsonSerializerOptions CaseInsensitiveOptions = new() - { - PropertyNameCaseInsensitive = true - }; -} diff --git a/src/Services/Services.Common/Services.Common.csproj b/src/Services/Services.Common/Services.Common.csproj deleted file mode 100644 index 8e73f01432..0000000000 --- a/src/Services/Services.Common/Services.Common.csproj +++ /dev/null @@ -1,56 +0,0 @@ - - - - net7.0 - enable - enable - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Services/Webhooks/Webhooks.API/Controllers/HomeController.cs b/src/Services/Webhooks/Webhooks.API/Controllers/HomeController.cs new file mode 100644 index 0000000000..2a7951ec3f --- /dev/null +++ b/src/Services/Webhooks/Webhooks.API/Controllers/HomeController.cs @@ -0,0 +1,11 @@ +namespace Webhooks.API.Controllers; + +public class HomeController : Controller +{ + // GET: // + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } + +} diff --git a/src/Services/Webhooks/Webhooks.API/Controllers/WebhookSubscriptionRequest.cs b/src/Services/Webhooks/Webhooks.API/Controllers/WebhookSubscriptionRequest.cs index d768bce26c..069cd22afc 100644 --- a/src/Services/Webhooks/Webhooks.API/Controllers/WebhookSubscriptionRequest.cs +++ b/src/Services/Webhooks/Webhooks.API/Controllers/WebhookSubscriptionRequest.cs @@ -19,7 +19,7 @@ public IEnumerable Validate(ValidationContext validationContex yield return new ValidationResult("Url is not valid", new[] { nameof(Url) }); } - var isOk = Enum.TryParse(Event, ignoreCase: true, result: out WebhookType whtype); + var isOk = Enum.TryParse(Event, ignoreCase: true, result: out WebhookType whtype); if (!isOk) { yield return new ValidationResult($"{Event} is invalid event name", new[] { nameof(Event) }); diff --git a/src/Services/Webhooks/Webhooks.API/Controllers/WebhooksController.cs b/src/Services/Webhooks/Webhooks.API/Controllers/WebhooksController.cs index 18c283256b..5f9513480f 100644 --- a/src/Services/Webhooks/Webhooks.API/Controllers/WebhooksController.cs +++ b/src/Services/Webhooks/Webhooks.API/Controllers/WebhooksController.cs @@ -17,7 +17,7 @@ public WebhooksController(WebhooksContext dbContext, IIdentityService identitySe [Authorize] [HttpGet] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] public async Task ListByUser() { var userId = _identityService.GetUserIdentity(); @@ -27,8 +27,8 @@ public async Task ListByUser() [Authorize] [HttpGet("{id:int}")] - [ProducesResponseType(typeof(WebhookSubscription), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(WebhookSubscription), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task GetByUserAndId(int id) { var userId = _identityService.GetUserIdentity(); @@ -42,9 +42,9 @@ public async Task GetByUserAndId(int id) [Authorize] [HttpPost] - [ProducesResponseType(StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status418ImATeapot)] + [ProducesResponseType((int)HttpStatusCode.Created)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType(418)] public async Task SubscribeWebhook(WebhookSubscriptionRequest request) { if (!ModelState.IsValid) @@ -71,14 +71,14 @@ public async Task SubscribeWebhook(WebhookSubscriptionRequest req } else { - return StatusCode(StatusCodes.Status418ImATeapot, "Grant URL invalid"); + return StatusCode(418, "Grant url can't be validated"); } } [Authorize] [HttpDelete("{id:int}")] - [ProducesResponseType(StatusCodes.Status202Accepted)] - [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType((int)HttpStatusCode.Accepted)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task UnsubscribeWebhook(int id) { var userId = _identityService.GetUserIdentity(); diff --git a/src/Services/Webhooks/Webhooks.API/Dockerfile b/src/Services/Webhooks/Webhooks.API/Dockerfile index 63d8fdb6bd..5db5f3f90c 100644 --- a/src/Services/Webhooks/Webhooks.API/Dockerfile +++ b/src/Services/Webhooks/Webhooks.API/Dockerfile @@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" @@ -32,7 +33,6 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" -COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" @@ -42,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" COPY "docker-compose.dcproj" "docker-compose.dcproj" -COPY "Directory.Packages.props" "Directory.Packages.props" COPY "NuGet.config" "NuGet.config" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" diff --git a/src/Services/Webhooks/Webhooks.API/Dockerfile.develop b/src/Services/Webhooks/Webhooks.API/Dockerfile.develop index 22b14c1a30..e981b09f72 100644 --- a/src/Services/Webhooks/Webhooks.API/Dockerfile.develop +++ b/src/Services/Webhooks/Webhooks.API/Dockerfile.develop @@ -11,6 +11,7 @@ COPY ["BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj", "Build COPY ["BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj", "BuildingBlocks/EventBus/EventBusServiceBus/"] COPY ["BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj", "BuildingBlocks/EventBus/IntegrationEventLogEF/"] COPY ["BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj", "BuildingBlocks/WebHostCustomization/WebHost.Customization/"] +COPY ["BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj", "BuildingBlocks/Devspaces.Support/"] COPY ["Services/Webhooks/Webhooks.API/Webhooks.API.csproj", "Services/Webhooks/Webhooks.API/"] COPY ["NuGet.config", "NuGet.config"] diff --git a/src/Services/Webhooks/Webhooks.API/Extensions/Extensions.cs b/src/Services/Webhooks/Webhooks.API/Extensions/Extensions.cs deleted file mode 100644 index b7dd7cb1d7..0000000000 --- a/src/Services/Webhooks/Webhooks.API/Extensions/Extensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -internal static class Extensions -{ - public static IServiceCollection AddDbContexts(this IServiceCollection services, IConfiguration configuration) - { - services.AddDbContext(options => - { - options.UseSqlServer(configuration.GetRequiredConnectionString("WebHooksDB"), - sqlServerOptionsAction: sqlOptions => - { - sqlOptions.MigrationsAssembly(typeof(Program).Assembly.FullName); - - //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency - sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); - }); - }); - - return services; - } - - public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) - { - var hcBuilder = services.AddHealthChecks(); - - hcBuilder - .AddSqlServer(_ => - configuration.GetRequiredConnectionString("WebHooksDB"), - name: "WebhooksApiDb-check", - tags: new string[] { "ready" }); - - return services; - } - - public static IServiceCollection AddHttpClientServices(this IServiceCollection services) - { - // Add http client services - services.AddHttpClient("GrantClient") - .SetHandlerLifetime(TimeSpan.FromMinutes(5)); - - return services; - } - - public static IServiceCollection AddIntegrationServices(this IServiceCollection services) - { - return services.AddTransient>( - sp => (DbConnection c) => new IntegrationEventLogService(c)); - } -} diff --git a/src/Services/Webhooks/Webhooks.API/GlobalUsings.cs b/src/Services/Webhooks/Webhooks.API/GlobalUsings.cs index ec333301cd..90840639b2 100644 --- a/src/Services/Webhooks/Webhooks.API/GlobalUsings.cs +++ b/src/Services/Webhooks/Webhooks.API/GlobalUsings.cs @@ -1,18 +1,51 @@ -global using System.ComponentModel.DataAnnotations; -global using System.Data.Common; -global using System.Linq; -global using System.Net.Http; -global using System.Text; -global using System.Text.Json; +global using Autofac.Extensions.DependencyInjection; +global using Autofac; +global using Devspaces.Support; +global using HealthChecks.UI.Client; +global using Microsoft.AspNetCore.Authentication.JwtBearer; global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Diagnostics.HealthChecks; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc.Filters; global using Microsoft.AspNetCore.Mvc; -global using Microsoft.EntityFrameworkCore; +global using Microsoft.AspNetCore; +global using Azure.Messaging.ServiceBus; global using Microsoft.EntityFrameworkCore.Design; +global using Microsoft.EntityFrameworkCore; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; -global using Services.Common; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Diagnostics.HealthChecks; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Microsoft.OpenApi.Models; +global using RabbitMQ.Client; +global using Swashbuckle.AspNetCore.SwaggerGen; +global using System.Collections.Generic; +global using System.ComponentModel.DataAnnotations; +global using System.Data.Common; +global using System.IdentityModel.Tokens.Jwt; +global using System.Linq; +global using System.Net.Http; +global using System.Net; +global using System.Reflection; +global using System.Text.Json; +global using System.Text; +global using System.Threading.Tasks; +global using System.Threading; +global using System; +global using Webhooks.API.Exceptions; +global using Webhooks.API.Infrastructure.ActionResult; global using Webhooks.API.Infrastructure; global using Webhooks.API.IntegrationEvents; global using Webhooks.API.Model; global using Webhooks.API.Services; +global using Webhooks.API; +global using Microsoft.IdentityModel.Tokens; diff --git a/src/Services/Webhooks/Webhooks.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs b/src/Services/Webhooks/Webhooks.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs new file mode 100644 index 0000000000..d71e8b55f8 --- /dev/null +++ b/src/Services/Webhooks/Webhooks.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs @@ -0,0 +1,9 @@ +namespace Webhooks.API.Infrastructure.ActionResult; + +class InternalServerErrorObjectResult : ObjectResult +{ + public InternalServerErrorObjectResult(object error) : base(error) + { + StatusCode = StatusCodes.Status500InternalServerError; + } +} diff --git a/src/Services/Webhooks/Webhooks.API/Infrastructure/AuthorizeCheckOperationFilter.cs b/src/Services/Webhooks/Webhooks.API/Infrastructure/AuthorizeCheckOperationFilter.cs new file mode 100644 index 0000000000..13b0fcbdb3 --- /dev/null +++ b/src/Services/Webhooks/Webhooks.API/Infrastructure/AuthorizeCheckOperationFilter.cs @@ -0,0 +1,29 @@ +namespace Webhooks.API.Infrastructure; + +public class AuthorizeCheckOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // Check for authorize attribute + var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType().Any() || + context.MethodInfo.GetCustomAttributes(true).OfType().Any(); + + if (!hasAuthorize) return; + + operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); + + var oAuthScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } + }; + + operation.Security = new List + { + new() + { + [ oAuthScheme ] = new [] { "webhooksapi" } + } + }; + } +} diff --git a/src/Services/Webhooks/Webhooks.API/Infrastructure/HttpGlobalExceptionFilter.cs b/src/Services/Webhooks/Webhooks.API/Infrastructure/HttpGlobalExceptionFilter.cs new file mode 100644 index 0000000000..39eab99080 --- /dev/null +++ b/src/Services/Webhooks/Webhooks.API/Infrastructure/HttpGlobalExceptionFilter.cs @@ -0,0 +1,58 @@ +namespace Webhooks.API.Infrastructure; + +public class HttpGlobalExceptionFilter : IExceptionFilter +{ + private readonly IWebHostEnvironment _env; + private readonly ILogger _logger; + + public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger logger) + { + _env = env; + _logger = logger; + } + + public void OnException(ExceptionContext context) + { + _logger.LogError(new EventId(context.Exception.HResult), + context.Exception, + context.Exception.Message); + + if (context.Exception.GetType() == typeof(WebhooksDomainException)) + { + var problemDetails = new ValidationProblemDetails() + { + Instance = context.HttpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." + }; + + problemDetails.Errors.Add("DomainValidations", new [] { context.Exception.Message }); + + context.Result = new BadRequestObjectResult(problemDetails); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + else + { + var json = new JsonErrorResponse + { + Messages = new[] { "An error occurred." } + }; + + if (_env.IsDevelopment()) + { + json.DeveloperMessage = context.Exception; + } + + context.Result = new InternalServerErrorObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + context.ExceptionHandled = true; + } + + private class JsonErrorResponse + { + public string[] Messages { get; set; } + + public object DeveloperMessage { get; set; } + } +} diff --git a/src/Services/Webhooks/Webhooks.API/IntegrationEvents/ProductPriceChangedIntegrationEventHandler.cs b/src/Services/Webhooks/Webhooks.API/IntegrationEvents/ProductPriceChangedIntegrationEventHandler.cs index bb164c0d1c..1172d0820f 100644 --- a/src/Services/Webhooks/Webhooks.API/IntegrationEvents/ProductPriceChangedIntegrationEventHandler.cs +++ b/src/Services/Webhooks/Webhooks.API/IntegrationEvents/ProductPriceChangedIntegrationEventHandler.cs @@ -2,8 +2,8 @@ public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler { - public Task Handle(ProductPriceChangedIntegrationEvent @event) + public async Task Handle(ProductPriceChangedIntegrationEvent @event) { - return Task.CompletedTask; + int i = 0; } } diff --git a/src/Services/Webhooks/Webhooks.API/Migrations/20190118091148_Initial.cs b/src/Services/Webhooks/Webhooks.API/Migrations/20190118091148_Initial.cs index a8b5123b84..06a8a16c13 100644 --- a/src/Services/Webhooks/Webhooks.API/Migrations/20190118091148_Initial.cs +++ b/src/Services/Webhooks/Webhooks.API/Migrations/20190118091148_Initial.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; +using System; namespace Webhooks.API.Migrations { diff --git a/src/Services/Webhooks/Webhooks.API/Program.cs b/src/Services/Webhooks/Webhooks.API/Program.cs index bb1e491901..027814057b 100644 --- a/src/Services/Webhooks/Webhooks.API/Program.cs +++ b/src/Services/Webhooks/Webhooks.API/Program.cs @@ -1,34 +1,19 @@ -var builder = WebApplication.CreateBuilder(args); - -builder.AddServiceDefaults(); - -builder.Services.AddControllers(); -builder.Services.AddDbContexts(builder.Configuration); -builder.Services.AddHealthChecks(builder.Configuration); -builder.Services.AddHttpClientServices(); -builder.Services.AddIntegrationServices(); - -builder.Services.AddTransient(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); - -builder.Services.AddTransient(); -builder.Services.AddTransient(); -builder.Services.AddTransient(); - -var app = builder.Build(); - -app.UseServiceDefaults(); - -app.MapControllers(); - -var eventBus = app.Services.GetRequiredService(); - -eventBus.Subscribe(); -eventBus.Subscribe(); -eventBus.Subscribe(); - -app.Services.MigrateDbContext((_, __) => { }); - -await app.RunAsync(); +CreateWebHostBuilder(args).Build() + .MigrateDbContext((_, __) => { }) + .Run(); + + +IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .ConfigureAppConfiguration((builderContext, config) => + { + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, builder) => + { + builder.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + builder.AddConsole(); + builder.AddDebug(); + builder.AddAzureWebAppDiagnostics(); + }); \ No newline at end of file diff --git a/src/Services/Webhooks/Webhooks.API/Properties/launchSettings.json b/src/Services/Webhooks/Webhooks.API/Properties/launchSettings.json index 1fdcd55ad4..5332915995 100644 --- a/src/Services/Webhooks/Webhooks.API/Properties/launchSettings.json +++ b/src/Services/Webhooks/Webhooks.API/Properties/launchSettings.json @@ -1,12 +1,27 @@ { + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:62486", + "sslPort": 0 + } + }, "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, "Webhooks.API": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "http://localhost:5227", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "http://localhost:5000" }, "Docker": { "commandName": "Docker", diff --git a/src/Services/Webhooks/Webhooks.API/Startup.cs b/src/Services/Webhooks/Webhooks.API/Startup.cs new file mode 100644 index 0000000000..54ed81de82 --- /dev/null +++ b/src/Services/Webhooks/Webhooks.API/Startup.cs @@ -0,0 +1,328 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; + +namespace Webhooks.API; +public class Startup +{ + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + + public IServiceProvider ConfigureServices(IServiceCollection services) + { + services + .AddAppInsight(Configuration) + .AddCustomRouting(Configuration) + .AddCustomDbContext(Configuration) + .AddSwagger(Configuration) + .AddCustomHealthCheck(Configuration) + .AddDevspaces() + .AddHttpClientServices(Configuration) + .AddIntegrationServices(Configuration) + .AddEventBus(Configuration) + .AddCustomAuthentication(Configuration) + .AddSingleton() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient(); + + var container = new ContainerBuilder(); + container.Populate(services); + return new AutofacServiceProvider(container.Build()); + } + + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + var pathBase = Configuration["PATH_BASE"]; + + if (!string.IsNullOrEmpty(pathBase)) + { + loggerFactory.CreateLogger("init").LogDebug("Using PATH BASE '{PathBase}'", pathBase); + app.UsePathBase(pathBase); + } + + app.UseRouting(); + app.UseCors("CorsPolicy"); + ConfigureAuth(app); + + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + endpoints.MapControllers(); + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + + app.UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Webhooks.API V1"); + c.OAuthClientId("webhooksswaggerui"); + c.OAuthAppName("WebHooks Service Swagger UI"); + }); + + ConfigureEventBus(app); + } + + protected virtual void ConfigureAuth(IApplicationBuilder app) + { + app.UseAuthentication(); + app.UseAuthorization(); + } + + protected virtual void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + eventBus.Subscribe(); + eventBus.Subscribe(); + eventBus.Subscribe(); + } +} + +internal static class CustomExtensionMethods +{ + public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) + { + services.AddApplicationInsightsTelemetry(configuration); + services.AddApplicationInsightsKubernetesEnricher(); + + return services; + } + + public static IServiceCollection AddCustomRouting(this IServiceCollection services, IConfiguration configuration) + { + services.AddControllers(options => + { + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + }); + + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .SetIsOriginAllowed((host) => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + + return services; + } + + public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) + { + services.AddEntityFrameworkSqlServer() + .AddDbContext(options => + { + options.UseSqlServer(configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }); + + return services; + } + + public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration) + { + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "eShopOnContainers - Webhooks HTTP API", + Version = "v1", + Description = "The Webhooks Microservice HTTP API. This is a simple webhooks CRUD registration entrypoint" + }); + + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows() + { + Implicit = new OpenApiOAuthFlow() + { + AuthorizationUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/authorize"), + TokenUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/token"), + Scopes = new Dictionary() + { + { "webhooks", "Webhooks API" } + } + } + } + }); + + options.OperationFilter(); + }); + + return services; + } + public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) + { + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubscriptionManager = sp.GetRequiredService(); + string subscriptionName = configuration["SubscriptionClientName"]; + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, + eventBusSubscriptionManager, iLifetimeScope, subscriptionName); + }); + + } + else + { + services.AddSingleton(sp => + { + var subscriptionClientName = configuration["SubscriptionClientName"]; + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubscriptionManager = sp.GetRequiredService(); + + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubscriptionManager, subscriptionClientName, retryCount); + }); + } + + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + return services; + } + + public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + hcBuilder + .AddCheck("self", () => HealthCheckResult.Healthy()) + .AddSqlServer( + configuration["ConnectionString"], + name: "WebhooksApiDb-check", + tags: new string[] { "webhooksdb" }); + + return services; + } + + public static IServiceCollection AddHttpClientServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(); + services.AddHttpClient("extendedhandlerlifetime").SetHandlerLifetime(Timeout.InfiniteTimeSpan); + //add http client services + services.AddHttpClient("GrantClient") + .SetHandlerLifetime(TimeSpan.FromMinutes(5)) + .AddDevspacesSupport(); + return services; + } + + public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddTransient>( + sp => (DbConnection c) => new IntegrationEventLogService(c)); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var subscriptionClientName = configuration["SubscriptionClientName"]; + return new DefaultServiceBusPersisterConnection(configuration["EventBusConnection"]); + }); + } + else + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + + var factory = new ConnectionFactory() + { + HostName = configuration["EventBusConnection"], + DispatchConsumersAsync = true + }; + + if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) + { + factory.UserName = configuration["EventBusUserName"]; + } + + if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) + { + factory.Password = configuration["EventBusPassword"]; + } + + var retryCount = 5; + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); + }); + } + + return services; + } + + public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) + { + // prevent from mapping "sub" claim to nameidentifier. + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); + + var identityUrl = configuration.GetValue("IdentityUrl"); + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + + }).AddJwtBearer(options => + { + options.Authority = identityUrl; + options.RequireHttpsMetadata = false; + options.Audience = "webhooks"; + options.TokenValidationParameters.ValidateAudience = false; + }); + + return services; + } + + + public static IServiceCollection AddCustomAuthorization(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthorization(options => + { + options.AddPolicy("ApiScope", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim("scope", "webhooks"); + }); + }); + return services; + } +} diff --git a/src/Services/Webhooks/Webhooks.API/Webhooks.API.csproj b/src/Services/Webhooks/Webhooks.API/Webhooks.API.csproj index 8183be07cd..ddbd22afde 100644 --- a/src/Services/Webhooks/Webhooks.API/Webhooks.API.csproj +++ b/src/Services/Webhooks/Webhooks.API/Webhooks.API.csproj @@ -2,18 +2,35 @@ net7.0 - enable + InProcess Linux + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + true - + + + + + + + + + + + + + + + + - diff --git a/src/Services/Webhooks/Webhooks.API/appsettings.Development.json b/src/Services/Webhooks/Webhooks.API/appsettings.Development.json index ce8d5a4ffd..f4f8f9d265 100644 --- a/src/Services/Webhooks/Webhooks.API/appsettings.Development.json +++ b/src/Services/Webhooks/Webhooks.API/appsettings.Development.json @@ -6,7 +6,5 @@ "Microsoft": "Information" } }, - "ConnectionStrings": { - "WebHooksDB": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word;TrustServerCertificate=true" - } + "ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word;TrustServerCertificate=true" } diff --git a/src/Services/Webhooks/Webhooks.API/appsettings.json b/src/Services/Webhooks/Webhooks.API/appsettings.json index 8d103c53eb..200ea4c967 100644 --- a/src/Services/Webhooks/Webhooks.API/appsettings.json +++ b/src/Services/Webhooks/Webhooks.API/appsettings.json @@ -1,43 +1,10 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Warning" } }, "AllowedHosts": "*", - "OpenApi": { - "Endpoint": { - "Name": "Webhooks.API V1" - }, - "Document": { - "Description": "The Webhooks Microservice HTTP API. This is a simple webhooks CRUD registration entrypoint", - "Title": "eShopOnContainers - Webhooks HTTP API", - "Version": "v1" - }, - "Auth": { - "ClientId": "webhooksswaggerui", - "AppName": "WebHooks Service Swagger UI" - } - }, - "ConnectionStrings": { - "EventBus": "localhost" - }, - "EventBus": { - "SubscriptionClientName": "Webhooks", - "RetryCount": 5 - }, - "ApplicationInsights": { - "InstrumentationKey": "" - }, - "Identity": { - "Url": "http://localhost:5223", - "Audience": "webhooks", - "Scopes": { - "webhooks": "Webhooks API" - } - }, - "UseCustomizationData": false, - "GracePeriodTime": "1", - "CheckUpdateTime": "30000" + "SubscriptionClientName": "Webhooks", + "EventBusRetryCount": 5 } diff --git a/src/Services/Webhooks/Webhooks.API/azds.yaml b/src/Services/Webhooks/Webhooks.API/azds.yaml new file mode 100644 index 0000000000..38dfe92a20 --- /dev/null +++ b/src/Services/Webhooks/Webhooks.API/azds.yaml @@ -0,0 +1,54 @@ +kind: helm-release +apiVersion: 1.1 +build: + context: ..\..\..\.. + dockerfile: Dockerfile +install: + chart: ../../../../k8s/helm/webhooks-api + set: + image: + tag: $(tag) + pullPolicy: Never + ingress: + annotations: + kubernetes.io/ingress.class: traefik-azds + hosts: + - $(spacePrefix)eshop$(hostSuffix) + inf: + k8s: + dns: $(spacePrefix)eshop$(hostSuffix) + values: + - values.dev.yaml? + - secrets.dev.yaml? + - inf.yaml + - app.yaml +configurations: + develop: + build: + useGitIgnore: true + dockerfile: Dockerfile.develop + container: + syncTarget: /src + sync: + - '**/Pages/**' + - '**/Views/**' + - '**/wwwroot/**' + - '!**/*.{sln,csproj}' + command: + - dotnet + - run + - --no-restore + - --no-build + - --no-launch-profile + - -c + - ${Configuration:-Debug} + iterate: + processesToKill: + - dotnet + - vsdbg + buildCommands: + - - dotnet + - build + - --no-restore + - -c + - ${Configuration:-Debug} diff --git a/src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj b/src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj index bb22aa4730..bd3ab30a1b 100644 --- a/src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj +++ b/src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj @@ -2,21 +2,50 @@ net7.0 - enable true false false false - false - false - - - - - + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + Always + + + Always + + + Always + + + Always + + + PreserveNewest + + + + + + + + all runtime; build; native; contentfiles; analyzers @@ -24,14 +53,17 @@ - - - + + + PreserveNewest + + + diff --git a/src/Tests/Services/Application.FunctionalTests/Extensions/HttpClientExtensions.cs b/src/Tests/Services/Application.FunctionalTests/Extensions/HttpClientExtensions.cs new file mode 100644 index 0000000000..b17edce910 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Extensions/HttpClientExtensions.cs @@ -0,0 +1,11 @@ +namespace FunctionalTests.Extensions; + +static class HttpClientExtensions +{ + public static HttpClient CreateIdempotentClient(this TestServer server) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("x-requestid", Guid.NewGuid().ToString()); + return client; + } +} diff --git a/src/Tests/Services/Application.FunctionalTests/GlobalUsings.cs b/src/Tests/Services/Application.FunctionalTests/GlobalUsings.cs index fa4a337592..5bacd265b9 100644 --- a/src/Tests/Services/Application.FunctionalTests/GlobalUsings.cs +++ b/src/Tests/Services/Application.FunctionalTests/GlobalUsings.cs @@ -1,14 +1,35 @@ -global using System; -global using System.Collections.Generic; -global using System.Linq; +global using Microsoft.AspNetCore.TestHost; +global using System; global using System.Net.Http; +global using Microsoft.AspNetCore.Http; global using System.Security.Claims; -global using System.Text; -global using System.Text.Json; global using System.Threading.Tasks; -global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.Extensions.Configuration; +global using System.IO; +global using System.Reflection; +global using FunctionalTests.Middleware; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.eShopOnContainers.Services.Basket.API; +global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; +global using Microsoft.eShopOnContainers.Services.Catalog.API; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using FunctionalTests.Extensions; +global using FunctionalTests.Services.Basket; global using Microsoft.eShopOnContainers.Services.Basket.API.Model; global using Microsoft.eShopOnContainers.WebMVC.ViewModels; +global using System.Text.Json; +global using System.Collections.Generic; +global using System.Linq; +global using System.Text; global using WebMVC.Services.ModelDTOs; global using Xunit; - +global using Microsoft.eShopOnContainers.Services.Ordering.API; +global using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure; +global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; +global using FunctionalTests.Services.Catalog; +global using Microsoft.eShopOnContainers.Services.Catalog.API.Model; +global using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; \ No newline at end of file diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketScenariosBase.cs b/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketScenariosBase.cs new file mode 100644 index 0000000000..71487d7b41 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketScenariosBase.cs @@ -0,0 +1,42 @@ +namespace FunctionalTests.Services.Basket; + +public class BasketScenariosBase +{ + private const string ApiUrlBase = "api/v1/basket"; + + + public TestServer CreateServer() + { + var path = Assembly.GetAssembly(typeof(BasketScenariosBase)) + .Location; + + var hostBuilder = new WebHostBuilder() + .UseContentRoot(Path.GetDirectoryName(path)) + .ConfigureAppConfiguration(cb => + { + cb.AddJsonFile("Services/Basket/appsettings.json", optional: false) + .AddEnvironmentVariables(); + }).UseStartup(); + + return new TestServer(hostBuilder); + } + + public static class Get + { + public static string GetBasket(int id) + { + return $"{ApiUrlBase}/{id}"; + } + + public static string GetBasketByCustomer(string customerId) + { + return $"{ApiUrlBase}/{customerId}"; + } + } + + public static class Post + { + public static string CreateBasket = $"{ApiUrlBase}/"; + public static string CheckoutOrder = $"{ApiUrlBase}/checkout"; + } +} diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketTestsStartup.cs b/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketTestsStartup.cs new file mode 100644 index 0000000000..23523a9f42 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketTestsStartup.cs @@ -0,0 +1,21 @@ +namespace FunctionalTests.Services.Basket; +using Microsoft.eShopOnContainers.Services.Basket.API; +class BasketTestsStartup : Startup +{ + public BasketTestsStartup(IConfiguration env) : base(env) + { + } + + protected override void ConfigureAuth(IApplicationBuilder app) + { + if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) + { + app.UseMiddleware(); + app.UseAuthorization(); + } + else + { + base.ConfigureAuth(app); + } + } +} diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Basket/appsettings.json b/src/Tests/Services/Application.FunctionalTests/Services/Basket/appsettings.json new file mode 100644 index 0000000000..3ed325899f --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Basket/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "IdentityUrl": "http://localhost:5105", + "IdentityUrlExternal": "http://localhost:5105", + "ConnectionString": "127.0.0.1", + "isTest": "true", + "EventBusConnection": "localhost", + "SubscriptionClientName": "Basket" +} diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Catalog/CatalogScenariosBase.cs b/src/Tests/Services/Application.FunctionalTests/Services/Catalog/CatalogScenariosBase.cs new file mode 100644 index 0000000000..ec762a9471 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Catalog/CatalogScenariosBase.cs @@ -0,0 +1,53 @@ +namespace FunctionalTests.Services.Catalog; +using Microsoft.eShopOnContainers.Services.Catalog.API; + +public class CatalogScenariosBase +{ + public TestServer CreateServer() + { + var path = Assembly.GetAssembly(typeof(CatalogScenariosBase)) + .Location; + + var hostBuilder = new WebHostBuilder() + .UseContentRoot(Path.GetDirectoryName(path)) + .ConfigureAppConfiguration(cb => + { + cb.AddJsonFile("Services/Catalog/appsettings.json", optional: false) + .AddEnvironmentVariables(); + }).UseStartup(); + + var testServer = new TestServer(hostBuilder); + + testServer.Host + .MigrateDbContext((context, services) => + { + var env = services.GetService(); + var settings = services.GetService>(); + var logger = services.GetService>(); + + new CatalogContextSeed() + .SeedAsync(context, env, settings, logger) + .Wait(); + }) + .MigrateDbContext((_, __) => { }); + + return testServer; + } + + public static class Get + { + public static string Orders = "api/v1/orders"; + + public static string Items = "api/v1/catalog/items"; + + public static string ProductByName(string name) + { + return $"api/v1/catalog/items/withname/{name}"; + } + } + + public static class Put + { + public static string UpdateCatalogProduct = "api/v1/catalog/items"; + } +} diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Catalog/appsettings.json b/src/Tests/Services/Application.FunctionalTests/Services/Catalog/appsettings.json new file mode 100644 index 0000000000..0cd61e36b2 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Catalog/appsettings.json @@ -0,0 +1,9 @@ +{ + "ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word", + "ExternalCatalogBaseUrl": "http://localhost:5101", + "IdentityUrl": "http://localhost:5105", + "isTest": "true", + "EventBusConnection": "localhost", + "PicBaseUrl": "http://localhost:5101/api/v1/catalog/items/[0]/pic/", + "SubscriptionClientName": "Catalog" +} diff --git a/src/Tests/Services/Application.FunctionalTests/Services/IntegrationEventsScenarios.cs b/src/Tests/Services/Application.FunctionalTests/Services/IntegrationEventsScenarios.cs index 38a4c9b042..86466766f6 100644 --- a/src/Tests/Services/Application.FunctionalTests/Services/IntegrationEventsScenarios.cs +++ b/src/Tests/Services/Application.FunctionalTests/Services/IntegrationEventsScenarios.cs @@ -1,7 +1,4 @@ namespace FunctionalTests.Services; - -using Basket.FunctionalTests.Base; -using Catalog.FunctionalTests; using Microsoft.eShopOnContainers.Services.Basket.API.Model; using Microsoft.eShopOnContainers.Services.Catalog.API.Model; using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; @@ -15,7 +12,7 @@ public async Task Post_update_product_price_and_catalog_and_basket_list_modified string userId = "JohnId"; using var catalogServer = new CatalogScenariosBase().CreateServer(); - using var basketServer = new BasketScenarioBase().CreateServer(); + using var basketServer = new BasketScenariosBase().CreateServer(); var catalogClient = catalogServer.CreateClient(); var basketClient = basketServer.CreateClient(); @@ -25,7 +22,7 @@ public async Task Post_update_product_price_and_catalog_and_basket_list_modified // AND a user basket filled with products var basket = ComposeBasket(userId, originalCatalogProducts.Data.Take(3)); var res = await basketClient.PostAsync( - BasketScenarioBase.Post.Basket, + BasketScenariosBase.Post.CreateBasket, new StringContent(JsonSerializer.Serialize(basket), UTF8Encoding.UTF8, "application/json") ); @@ -63,7 +60,7 @@ private async Task GetUpdatedBasketItem(decimal newPrice, int produc while (continueLoop && counter < 20) { //get the basket and verify that the price of the modified product is updated - var basketGetResponse = await basketClient.GetAsync(BasketScenarioBase.Get.GetBasketByCustomer(userId)); + var basketGetResponse = await basketClient.GetAsync(BasketScenariosBase.Get.GetBasketByCustomer(userId)); var basketUpdated = JsonSerializer.Deserialize(await basketGetResponse.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true @@ -87,7 +84,7 @@ private async Task GetUpdatedBasketItem(decimal newPrice, int produc private async Task> GetCatalogAsync(HttpClient catalogClient) { - var response = await catalogClient.GetAsync(CatalogScenariosBase.Get.Items(paginated: false)); + var response = await catalogClient.GetAsync(CatalogScenariosBase.Get.Items); var items = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize>(items, new JsonSerializerOptions { diff --git a/src/Tests/Services/Application.FunctionalTests/Services/OrderingScenarios.cs b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenarios.cs similarity index 74% rename from src/Tests/Services/Application.FunctionalTests/Services/OrderingScenarios.cs rename to src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenarios.cs index e006a6998b..e97a95e9d3 100644 --- a/src/Tests/Services/Application.FunctionalTests/Services/OrderingScenarios.cs +++ b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenarios.cs @@ -1,46 +1,31 @@ -using Basket.FunctionalTests.Base; -using Ordering.FunctionalTests; +namespace FunctionalTests.Services.Ordering; -namespace FunctionalTests.Services.Ordering; - -public class OrderingScenarios +public class OrderingScenarios : OrderingScenariosBase { [Fact] public async Task Cancel_basket_and_check_order_status_cancelled() { - using var orderServer = new OrderingScenarioBase().CreateServer(); - using var basketServer = new BasketScenarioBase().CreateServer(); + using var orderServer = new OrderingScenariosBase().CreateServer(); + using var basketServer = new BasketScenariosBase().CreateServer(); // Expected data var cityExpected = $"city-{Guid.NewGuid()}"; var orderStatusExpected = "cancelled"; - var basketClient = basketServer.CreateClient(); - var orderClient = orderServer.CreateClient(); + var basketClient = basketServer.CreateIdempotentClient(); + var orderClient = orderServer.CreateIdempotentClient(); // GIVEN a basket is created - var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json") - { - Headers = { { "x-requestid", Guid.NewGuid().ToString() } } - }; - await basketClient.PostAsync(BasketScenarioBase.Post.Basket, contentBasket); + var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); + await basketClient.PostAsync(BasketScenariosBase.Post.CreateBasket, contentBasket); // AND basket checkout is sent - await basketClient.PostAsync( - BasketScenarioBase.Post.CheckoutOrder, - new StringContent(BuildCheckout(cityExpected), UTF8Encoding.UTF8, "application/json") - { - Headers = { { "x-requestid", Guid.NewGuid().ToString() } } - }); + await basketClient.PostAsync(BasketScenariosBase.Post.CheckoutOrder, new StringContent(BuildCheckout(cityExpected), UTF8Encoding.UTF8, "application/json")); // WHEN Order is created in Ordering.api var newOrder = await TryGetNewOrderCreated(cityExpected, orderClient); - Assert.NotNull(newOrder); // AND Order is cancelled in Ordering.api - await orderClient.PutAsync(OrderingScenarioBase.Put.CancelOrder, new StringContent(BuildCancelOrder(newOrder.OrderNumber), UTF8Encoding.UTF8, "application/json") - { - Headers = { { "x-requestid", Guid.NewGuid().ToString() } } - }); + await orderClient.PutAsync(OrderingScenariosBase.Put.CancelOrder, new StringContent(BuildCancelOrder(newOrder.OrderNumber), UTF8Encoding.UTF8, "application/json")); // AND the requested order is retrieved var order = await TryGetOrder(newOrder.OrderNumber, orderClient); @@ -51,7 +36,7 @@ await basketClient.PostAsync( async Task TryGetOrder(string orderNumber, HttpClient orderClient) { - var ordersGetResponse = await orderClient.GetStringAsync(OrderingScenarioBase.Get.Orders); + var ordersGetResponse = await orderClient.GetStringAsync(OrderingScenariosBase.Get.Orders); var orders = JsonSerializer.Deserialize>(ordersGetResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true @@ -68,7 +53,7 @@ private async Task TryGetNewOrderCreated(string city, HttpClient orderCli while (counter < 20) { //get the orders and verify that the new order has been created - var ordersGetResponse = await orderClient.GetStringAsync(OrderingScenarioBase.Get.Orders); + var ordersGetResponse = await orderClient.GetStringAsync(OrderingScenariosBase.Get.Orders); var orders = JsonSerializer.Deserialize>(ordersGetResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true @@ -83,12 +68,12 @@ private async Task TryGetNewOrderCreated(string city, HttpClient orderCli var lastOrder = orders.OrderByDescending(o => o.Date).First(); int.TryParse(lastOrder.OrderNumber, out int id); - var orderDetails = await orderClient.GetStringAsync(OrderingScenarioBase.Get.OrderBy(id)); + var orderDetails = await orderClient.GetStringAsync(OrderingScenariosBase.Get.OrderBy(id)); order = JsonSerializer.Deserialize(orderDetails, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - + order.City = city; if (IsOrderCreated(order, city)) diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenariosBase.cs b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenariosBase.cs new file mode 100644 index 0000000000..fce6bca182 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenariosBase.cs @@ -0,0 +1,63 @@ +namespace FunctionalTests.Services.Ordering; + +public class OrderingScenariosBase +{ + public TestServer CreateServer() + { + var path = Assembly.GetAssembly(typeof(OrderingScenariosBase)) + .Location; + + var hostBuilder = new WebHostBuilder() + .UseContentRoot(Path.GetDirectoryName(path)) + .ConfigureAppConfiguration(cb => + { + cb.AddJsonFile("Services/Ordering/appsettings.json", optional: false) + .AddEnvironmentVariables(); + }).UseStartup(); + + var testServer = new TestServer(hostBuilder); + + testServer.Host + .MigrateDbContext((context, services) => + { + var env = services.GetService(); + var settings = services.GetService>(); + var logger = services.GetService>(); + + new OrderingContextSeed() + .SeedAsync(context, env, settings, logger) + .Wait(); + }) + .MigrateDbContext((_, __) => { }); + + return testServer; + } + + public static class Get + { + public static string Orders = "api/v1/orders"; + + public static string OrderBy(int id) + { + return $"api/v1/orders/{id}"; + } + } + + public static class Post + { + public static string AddNewOrder = "api/v1/orders/new"; + } + + public static class Put + { + public static string CancelOrder = "api/v1/orders/cancel"; + } + + public static class Delete + { + public static string OrderBy(int id) + { + return $"api/v1/orders/{id}"; + } + } +} diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingTestsStartup.cs b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingTestsStartup.cs new file mode 100644 index 0000000000..f6f3d8e51a --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingTestsStartup.cs @@ -0,0 +1,22 @@ +namespace FunctionalTests.Services.Ordering; +using Microsoft.eShopOnContainers.Services.Ordering.API; + +public class OrderingTestsStartup : Startup +{ + public OrderingTestsStartup(IConfiguration env) : base(env) + { + } + + protected override void ConfigureAuth(IApplicationBuilder app) + { + if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) + { + app.UseMiddleware(); + app.UseAuthorization(); + } + else + { + base.ConfigureAuth(app); + } + } +} diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Ordering/appsettings.json b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/appsettings.json new file mode 100644 index 0000000000..b94888de26 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/appsettings.json @@ -0,0 +1,11 @@ +{ + "ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word;", + "ExternalCatalogBaseUrl": "http://localhost:5101", + "IdentityUrl": "http://localhost:5105", + "isTest": "true", + "EventBusConnection": "localhost", + "CheckUpdateTime": "30000", + "GracePeriodTime": "1", + "SubscriptionClientName": "Ordering", + "IdentityUrlExternal": "http://localhost:5105" +} diff --git a/src/Web/WebMVC/AppSettings.cs b/src/Web/WebMVC/AppSettings.cs index b1a8b6851c..ae0a026505 100644 --- a/src/Web/WebMVC/AppSettings.cs +++ b/src/Web/WebMVC/AppSettings.cs @@ -2,6 +2,28 @@ public class AppSettings { + //public Connectionstrings ConnectionStrings { get; set; } public string PurchaseUrl { get; set; } + public string SignalrHubUrl { get; set; } + public bool ActivateCampaignDetailFunction { get; set; } + public Logging Logging { get; set; } public bool UseCustomizationData { get; set; } } + +public class Connectionstrings +{ + public string DefaultConnection { get; set; } +} + +public class Logging +{ + public bool IncludeScopes { get; set; } + public Loglevel LogLevel { get; set; } +} + +public class Loglevel +{ + public string Default { get; set; } + public string System { get; set; } + public string Microsoft { get; set; } +} diff --git a/src/Web/WebMVC/Controllers/AccountController.cs b/src/Web/WebMVC/Controllers/AccountController.cs index 7acdac9e2b..78282f9a91 100644 --- a/src/Web/WebMVC/Controllers/AccountController.cs +++ b/src/Web/WebMVC/Controllers/AccountController.cs @@ -11,9 +11,17 @@ public AccountController(ILogger logger) } [Authorize(AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] - public IActionResult SignIn(string returnUrl) + public async Task SignIn(string returnUrl) { - _logger.LogInformation("User {@User} authenticated", User.Identity.Name); + var user = User as ClaimsPrincipal; + var token = await HttpContext.GetTokenAsync("access_token"); + + _logger.LogInformation("----- User {@User} authenticated into {AppName}", user, Program.AppName); + + if (token != null) + { + ViewData["access_token"] = token; + } // "Catalog" because UrlHelper doesn't support nameof() for controllers // https://github.com/aspnet/Mvc/issues/5853 @@ -29,6 +37,6 @@ public async Task Signout() // https://github.com/aspnet/Mvc/issues/5853 var homeUrl = Url.Action(nameof(CatalogController.Index), "Catalog"); return new SignOutResult(OpenIdConnectDefaults.AuthenticationScheme, - new AuthenticationProperties { RedirectUri = homeUrl }); + new AspNetCore.Authentication.AuthenticationProperties { RedirectUri = homeUrl }); } } diff --git a/src/Web/WebMVC/Controllers/TestController.cs b/src/Web/WebMVC/Controllers/TestController.cs new file mode 100644 index 0000000000..3054038eec --- /dev/null +++ b/src/Web/WebMVC/Controllers/TestController.cs @@ -0,0 +1,52 @@ +namespace WebMVC.Controllers; + +class TestPayload +{ + public int CatalogItemId { get; set; } + + public string BasketId { get; set; } + + public int Quantity { get; set; } +} + +[Authorize] +public class TestController : Controller +{ + private readonly IHttpClientFactory _client; + private readonly IIdentityParser _appUserParser; + + public TestController(IHttpClientFactory client, IIdentityParser identityParser) + { + _client = client; + _appUserParser = identityParser; + } + + public async Task Ocelot() + { + var url = "http://apigw/shopping/api/v1/basket/items"; + + var payload = new TestPayload() + { + CatalogItemId = 1, + Quantity = 1, + BasketId = _appUserParser.Parse(User).Id + }; + + var content = new StringContent(JsonSerializer.Serialize(payload), System.Text.Encoding.UTF8, "application/json"); + + + var response = await _client.CreateClient(nameof(IBasketService)) + .PostAsync(url, content); + + if (response.IsSuccessStatusCode) + { + var str = await response.Content.ReadAsStringAsync(); + + return Ok(str); + } + else + { + return Ok(new { response.StatusCode, response.ReasonPhrase }); + } + } +} diff --git a/src/Web/WebMVC/Dockerfile b/src/Web/WebMVC/Dockerfile index ee0adb5b24..9170a2c41e 100644 --- a/src/Web/WebMVC/Dockerfile +++ b/src/Web/WebMVC/Dockerfile @@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" @@ -32,7 +33,6 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" -COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" @@ -42,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" COPY "docker-compose.dcproj" "docker-compose.dcproj" -COPY "Directory.Packages.props" "Directory.Packages.props" COPY "NuGet.config" "NuGet.config" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" diff --git a/src/Web/WebMVC/Dockerfile.develop b/src/Web/WebMVC/Dockerfile.develop index 4d5c287748..93555db686 100644 --- a/src/Web/WebMVC/Dockerfile.develop +++ b/src/Web/WebMVC/Dockerfile.develop @@ -6,6 +6,7 @@ EXPOSE 80 WORKDIR /src COPY ["Web/WebMVC/WebMVC.csproj", "Web/WebMVC/"] +COPY ["BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj", "BuildingBlocks/Devspaces.Support/"] COPY ["NuGet.config", "NuGet.config"] RUN dotnet restore "Web/WebMVC/WebMVC.csproj" diff --git a/src/Web/WebMVC/Extensions/Extensions.cs b/src/Web/WebMVC/Extensions/Extensions.cs deleted file mode 100644 index 4e9a87410a..0000000000 --- a/src/Web/WebMVC/Extensions/Extensions.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Fix samesite issue when running eShop from docker-compose locally as by default http protocol is being used -// Refer to https://github.com/dotnet-architecture/eShopOnContainers/issues/1391 -using Yarp.ReverseProxy.Forwarder; - -internal static class Extensions -{ - public static void AddHealthChecks(this IServiceCollection services, IConfiguration configuration) - { - services.AddHealthChecks() - .AddUrlGroup(_ => new Uri(configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }); - } - - public static void AddApplicationServices(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration); - - if (configuration["DPConnectionString"] is string redisConnection) - { - services.AddDataProtection(opts => - { - opts.ApplicationDiscriminator = "eshop.webmvc"; - }) - .PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(redisConnection), "DataProtection-Keys"); - } - } - - // Adds all Http client services - public static void AddHttpClientServices(this IServiceCollection services) - { - // Register delegating handlers - services.AddTransient() - .AddTransient(); - - // Add http client services - services.AddHttpClient() - .SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Sample. Default lifetime is 2 minutes - .AddHttpMessageHandler(); - - services.AddHttpClient(); - - services.AddHttpClient() - .AddHttpMessageHandler() - .AddHttpMessageHandler(); - - // Add custom application services - services.AddTransient, IdentityParser>(); - } - - public static void AddAuthenticationServices(this IServiceCollection services, IConfiguration configuration) - { - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); - - var identityUrl = configuration.GetRequiredValue("IdentityUrl"); - var callBackUrl = configuration.GetRequiredValue("CallBackUrl"); - var sessionCookieLifetime = configuration.GetValue("SessionCookieLifetimeMinutes", 60); - - // Add Authentication services - services.AddAuthentication(options => - { - options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; - }) - .AddCookie(options => options.ExpireTimeSpan = TimeSpan.FromMinutes(sessionCookieLifetime)) - .AddOpenIdConnect(options => - { - options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.Authority = identityUrl; - options.SignedOutRedirectUri = callBackUrl; - options.ClientId = "mvc"; - options.ClientSecret = "secret"; - options.ResponseType = "code"; - options.SaveTokens = true; - options.GetClaimsFromUserInfoEndpoint = true; - options.RequireHttpsMetadata = false; - options.Scope.Add("openid"); - options.Scope.Add("profile"); - options.Scope.Add("orders"); - options.Scope.Add("basket"); - options.Scope.Add("webshoppingagg"); - options.Scope.Add("orders.signalrhub"); - options.Scope.Add("webhooks"); - }); - } - - public static IEndpointConventionBuilder MapForwardSignalR(this WebApplication app) - { - // Forward the SignalR traffic to the bff - var destination = app.Configuration.GetRequiredValue("PurchaseUrl"); - var authTransformer = new BffAuthTransformer(); - var requestConfig = new ForwarderRequestConfig(); - - return app.MapForwarder("/hub/notificationhub/{**any}", destination, requestConfig, authTransformer); - } - - private sealed class BffAuthTransformer : HttpTransformer - { - public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken) - { - // Set the access token as a bearer token for the outgoing request - var accessToken = await httpContext.GetTokenAsync("access_token"); - - if (accessToken is not null) - { - proxyRequest.Headers.Authorization = new("Bearer", accessToken); - } - - await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken); - } - } -} diff --git a/src/Web/WebMVC/Extensions/HttpClientExtensions.cs b/src/Web/WebMVC/Extensions/HttpClientExtensions.cs new file mode 100644 index 0000000000..d1b11c9f7d --- /dev/null +++ b/src/Web/WebMVC/Extensions/HttpClientExtensions.cs @@ -0,0 +1,28 @@ +namespace Microsoft.eShopOnContainers.WebMVC.Extensions; + +public static class HttpClientExtensions +{ + public static void SetBasicAuthentication(this HttpClient client, string userName, string password) => + client.DefaultRequestHeaders.Authorization = new BasicAuthenticationHeaderValue(userName, password); + + public static void SetToken(this HttpClient client, string scheme, string token) => + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(scheme, token); + + public static void SetBearerToken(this HttpClient client, string token) => + client.SetToken(JwtConstants.TokenType, token); +} + +public class BasicAuthenticationHeaderValue : AuthenticationHeaderValue +{ + public BasicAuthenticationHeaderValue(string userName, string password) + : base("Basic", EncodeCredential(userName, password)) + { } + + private static string EncodeCredential(string userName, string password) + { + Encoding encoding = Encoding.GetEncoding("iso-8859-1"); + string credential = String.Format("{0}:{1}", userName, password); + + return Convert.ToBase64String(encoding.GetBytes(credential)); + } +} diff --git a/src/Web/WebMVC/Extensions/SessionExtensions.cs b/src/Web/WebMVC/Extensions/SessionExtensions.cs new file mode 100644 index 0000000000..8175c26c4f --- /dev/null +++ b/src/Web/WebMVC/Extensions/SessionExtensions.cs @@ -0,0 +1,16 @@ +public static class SessionExtensions +{ + public static void SetObject(this ISession session, string key, object value) => + session.SetString(key,JsonSerializer.Serialize(value)); + + public static T GetObject(this ISession session, string key) + { + var value = session.GetString(key); + + return value == null ? default(T) :JsonSerializer.Deserialize(value, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + } +} + diff --git a/src/Web/WebMVC/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs b/src/Web/WebMVC/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs new file mode 100644 index 0000000000..e9f21a4f0f --- /dev/null +++ b/src/Web/WebMVC/Infrastructure/HttpClientAuthorizationDelegatingHandler.cs @@ -0,0 +1,40 @@ +namespace WebMVC.Infrastructure; + +public class HttpClientAuthorizationDelegatingHandler + : DelegatingHandler +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var authorizationHeader = _httpContextAccessor.HttpContext + .Request.Headers["Authorization"]; + + if (!string.IsNullOrEmpty(authorizationHeader)) + { + request.Headers.Add("Authorization", new List() { authorizationHeader }); + } + + var token = await GetToken(); + + if (token != null) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + + return await base.SendAsync(request, cancellationToken); + } + + async Task GetToken() + { + const string ACCESS_TOKEN = "access_token"; + + return await _httpContextAccessor.HttpContext + .GetTokenAsync(ACCESS_TOKEN); + } +} diff --git a/src/Web/WebMVC/Infrastructure/WebContextSeed.cs b/src/Web/WebMVC/Infrastructure/WebContextSeed.cs index c20f39690e..6ee7678e18 100644 --- a/src/Web/WebMVC/Infrastructure/WebContextSeed.cs +++ b/src/Web/WebMVC/Infrastructure/WebContextSeed.cs @@ -1,12 +1,14 @@ namespace WebMVC.Infrastructure; +using Serilog; public class WebContextSeed { public static void Seed(IApplicationBuilder applicationBuilder, IWebHostEnvironment env) { - var settings = applicationBuilder + var log = Serilog.Log.Logger; + + var settings = (AppSettings)applicationBuilder .ApplicationServices.GetRequiredService>().Value; - var log = applicationBuilder.ApplicationServices.GetRequiredService>(); var useCustomizationData = settings.UseCustomizationData; var contentRootPath = env.ContentRootPath; @@ -27,7 +29,7 @@ static void GetPreconfiguredCSS(string contentRootPath, string webroot, ILogger string overrideCssFile = Path.Combine(contentRootPath, "Setup", "override.css"); if (!File.Exists(overrideCssFile)) { - log.LogError("Override css file '{FileName}' does not exists.", overrideCssFile); + log.Error("Override css file '{FileName}' does not exists.", overrideCssFile); return; } @@ -36,7 +38,7 @@ static void GetPreconfiguredCSS(string contentRootPath, string webroot, ILogger } catch (Exception ex) { - log.LogError(ex, "Error getting preconfigured css"); + log.Error(ex, "EXCEPTION ERROR: {Message}", ex.Message); } } @@ -47,7 +49,7 @@ static void GetPreconfiguredImages(string contentRootPath, string webroot, ILogg string imagesZipFile = Path.Combine(contentRootPath, "Setup", "images.zip"); if (!File.Exists(imagesZipFile)) { - log.LogError("Zip file '{ZipFileName}' does not exists.", imagesZipFile); + log.Error("Zip file '{ZipFileName}' does not exists.", imagesZipFile); return; } @@ -68,14 +70,14 @@ static void GetPreconfiguredImages(string contentRootPath, string webroot, ILogg } else { - log.LogWarning("Skipped file '{FileName}' in zip file '{ZipFileName}'", entry.Name, imagesZipFile); + log.Warning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile); } } } catch (Exception ex) { - log.LogError(ex, "Error getting preconfigured images"); + log.Error(ex, "EXCEPTION ERROR: {Message}", ex.Message); } } -} +} \ No newline at end of file diff --git a/src/Web/WebMVC/Program.cs b/src/Web/WebMVC/Program.cs index 34d407fec9..48a72fcf8c 100644 --- a/src/Web/WebMVC/Program.cs +++ b/src/Web/WebMVC/Program.cs @@ -1,35 +1,68 @@ -var builder = WebApplication.CreateBuilder(args); +var configuration = GetConfiguration(); -builder.AddServiceDefaults(); +Log.Logger = CreateSerilogLogger(configuration); -builder.Services.AddHttpForwarder(); -builder.Services.AddControllersWithViews(); +try +{ + Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); + var host = BuildWebHost(configuration, args); -builder.Services.AddHealthChecks(builder.Configuration); -builder.Services.AddApplicationServices(builder.Configuration); -builder.Services.AddAuthenticationServices(builder.Configuration); -builder.Services.AddHttpClientServices(); + Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); + host.Run(); -var app = builder.Build(); + return 0; +} +catch (Exception ex) +{ + Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} -app.UseServiceDefaults(); +IWebHost BuildWebHost(IConfiguration configuration, string[] args) => + WebHost.CreateDefaultBuilder(args) + .CaptureStartupErrors(false) + .ConfigureAppConfiguration(x => x.AddConfiguration(configuration)) + .UseStartup() + .UseSerilog() + .Build(); -app.UseStaticFiles(); +Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) +{ + var seqServerUrl = configuration["Serilog:SeqServerUrl"]; + var logstashUrl = configuration["Serilog:LogstashgUrl"]; + var cfg = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .Enrich.WithProperty("ApplicationContext", Program.AppName) + .Enrich.FromLogContext() + .WriteTo.Console(); + if (!string.IsNullOrWhiteSpace(seqServerUrl)) + { + cfg.WriteTo.Seq(seqServerUrl); + } + if (!string.IsNullOrWhiteSpace(logstashUrl)) + { + cfg.WriteTo.Http(logstashUrl,null); + } + return cfg.CreateLogger(); +} -// Fix samesite issue when running eShop from docker-compose locally as by default http protocol is being used -// Refer to https://github.com/dotnet-architecture/eShopOnContainers/issues/1391 -app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax }); +IConfiguration GetConfiguration() +{ + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); -app.UseRouting(); + return builder.Build(); +} -app.UseAuthentication(); -app.UseAuthorization(); -app.MapControllerRoute("default", "{controller=Catalog}/{action=Index}/{id?}"); -app.MapControllerRoute("defaultError", "{controller=Error}/{action=Error}"); -app.MapControllers(); -app.MapForwardSignalR(); - -WebContextSeed.Seed(app, app.Environment); - -await app.RunAsync(); +public partial class Program +{ + private static readonly string _namespace = typeof(Startup).Namespace; + public static readonly string AppName = _namespace.Substring(_namespace.LastIndexOf('.', _namespace.LastIndexOf('.') - 1) + 1); +} \ No newline at end of file diff --git a/src/Web/WebMVC/Properties/launchSettings.json b/src/Web/WebMVC/Properties/launchSettings.json index cc41e62fe1..bf529db405 100644 --- a/src/Web/WebMVC/Properties/launchSettings.json +++ b/src/Web/WebMVC/Properties/launchSettings.json @@ -1,9 +1,23 @@ { + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5100", + "sslPort": 0 + } + }, "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, "Microsoft.eShopOnContainers.WebMVC": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "http://localhost:5331", + "launchUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Web/WebMVC/Services/BasketService.cs b/src/Web/WebMVC/Services/BasketService.cs index 0cfcb408f0..eeadf37b7a 100644 --- a/src/Web/WebMVC/Services/BasketService.cs +++ b/src/Web/WebMVC/Services/BasketService.cs @@ -29,14 +29,17 @@ public async Task GetBasket(ApplicationUser user) var responseString = await response.Content.ReadAsStringAsync(); return string.IsNullOrEmpty(responseString) ? new Basket() { BuyerId = user.Id } : - JsonSerializer.Deserialize(responseString, JsonDefaults.CaseInsensitiveOptions); + JsonSerializer.Deserialize(responseString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); } public async Task UpdateBasket(Basket basket) { var uri = API.Basket.UpdateBasket(_basketByPassUrl); - var basketContent = new StringContent(JsonSerializer.Serialize(basket), Encoding.UTF8, "application/json"); + var basketContent = new StringContent(JsonSerializer.Serialize(basket), System.Text.Encoding.UTF8, "application/json"); var response = await _apiClient.PostAsync(uri, basketContent); @@ -48,9 +51,9 @@ public async Task UpdateBasket(Basket basket) public async Task Checkout(BasketDTO basket) { var uri = API.Basket.CheckoutBasket(_basketByPassUrl); - var basketContent = new StringContent(JsonSerializer.Serialize(basket), Encoding.UTF8, "application/json"); - - _logger.LogInformation("Uri checkout {uri}", uri); + var basketContent = new StringContent(JsonSerializer.Serialize(basket), System.Text.Encoding.UTF8, "application/json"); + + _logger.LogInformation("Uri chechout {uri}", uri); var response = await _apiClient.PostAsync(uri, basketContent); @@ -71,7 +74,7 @@ public async Task SetQuantities(ApplicationUser user, Dictionary SetQuantities(ApplicationUser user, Dictionary(jsonResponse, JsonDefaults.CaseInsensitiveOptions); + return JsonSerializer.Deserialize(jsonResponse, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); } public async Task GetOrderDraft(string basketId) @@ -88,7 +94,10 @@ public async Task GetOrderDraft(string basketId) var responseString = await _apiClient.GetStringAsync(uri); - var response = JsonSerializer.Deserialize(responseString, JsonDefaults.CaseInsensitiveOptions); + var response = JsonSerializer.Deserialize(responseString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); return response; } @@ -104,7 +113,7 @@ public async Task AddItemToBasket(ApplicationUser user, int productId) Quantity = 1 }; - var basketContent = new StringContent(JsonSerializer.Serialize(newItem), Encoding.UTF8, "application/json"); + var basketContent = new StringContent(JsonSerializer.Serialize(newItem), System.Text.Encoding.UTF8, "application/json"); var response = await _apiClient.PostAsync(uri, basketContent); } diff --git a/src/Web/WebMVC/Services/CatalogService.cs b/src/Web/WebMVC/Services/CatalogService.cs index 41e3754247..96ad9c3c0f 100644 --- a/src/Web/WebMVC/Services/CatalogService.cs +++ b/src/Web/WebMVC/Services/CatalogService.cs @@ -23,7 +23,10 @@ public async Task GetCatalogItems(int page, int take, int? brand, int? var responseString = await _httpClient.GetStringAsync(uri); - var catalog = JsonSerializer.Deserialize(responseString, JsonDefaults.CaseInsensitiveOptions); + var catalog = JsonSerializer.Deserialize(responseString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); return catalog; } @@ -37,10 +40,10 @@ public async Task> GetBrands() var items = new List(); items.Add(new SelectListItem() { Value = null, Text = "All", Selected = true }); - + using var brands = JsonDocument.Parse(responseString); - foreach (JsonElement brand in brands.RootElement.EnumerateArray()) + foreach (JsonElement brand in brands.RootElement.EnumerateArray()) { items.Add(new SelectListItem() { @@ -60,7 +63,7 @@ public async Task> GetTypes() var items = new List(); items.Add(new SelectListItem() { Value = null, Text = "All", Selected = true }); - + using var catalogTypes = JsonDocument.Parse(responseString); foreach (JsonElement catalogType in catalogTypes.RootElement.EnumerateArray()) diff --git a/src/Web/WebMVC/Services/OrderingService.cs b/src/Web/WebMVC/Services/OrderingService.cs index fee7bbee60..f1422c51f3 100644 --- a/src/Web/WebMVC/Services/OrderingService.cs +++ b/src/Web/WebMVC/Services/OrderingService.cs @@ -23,7 +23,10 @@ async public Task GetOrder(ApplicationUser user, string id) var responseString = await _httpClient.GetStringAsync(uri); - var response = JsonSerializer.Deserialize(responseString, JsonDefaults.CaseInsensitiveOptions); + var response = JsonSerializer.Deserialize(responseString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); return response; } @@ -34,7 +37,10 @@ async public Task> GetMyOrders(ApplicationUser user) var responseString = await _httpClient.GetStringAsync(uri); - var response = JsonSerializer.Deserialize>(responseString, JsonDefaults.CaseInsensitiveOptions); + var response = JsonSerializer.Deserialize>(responseString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); return response; } @@ -49,7 +55,7 @@ async public Task CancelOrder(string orderId) }; var uri = API.Order.CancelOrder(_remoteServiceBaseUrl); - var orderContent = new StringContent(JsonSerializer.Serialize(order), Encoding.UTF8, "application/json"); + var orderContent = new StringContent(JsonSerializer.Serialize(order), System.Text.Encoding.UTF8, "application/json"); var response = await _httpClient.PutAsync(uri, orderContent); @@ -69,7 +75,7 @@ async public Task ShipOrder(string orderId) }; var uri = API.Order.ShipOrder(_remoteServiceBaseUrl); - var orderContent = new StringContent(JsonSerializer.Serialize(order), Encoding.UTF8, "application/json"); + var orderContent = new StringContent(JsonSerializer.Serialize(order), System.Text.Encoding.UTF8, "application/json"); var response = await _httpClient.PutAsync(uri, orderContent); diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs new file mode 100644 index 0000000000..004959d386 --- /dev/null +++ b/src/Web/WebMVC/Startup.cs @@ -0,0 +1,189 @@ +namespace Microsoft.eShopOnContainers.WebMVC; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the IoC container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllersWithViews() + .Services + .AddAppInsight(Configuration) + .AddHealthChecks(Configuration) + .AddCustomMvc(Configuration) + .AddDevspaces() + .AddHttpClientServices(Configuration); + + IdentityModelEventSource.ShowPII = true; // Caution! Do NOT use in production: https://aka.ms/IdentityModel/PII + + services.AddCustomAuthentication(Configuration); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Error"); + } + + var pathBase = Configuration["PATH_BASE"]; + + if (!string.IsNullOrEmpty(pathBase)) + { + app.UsePathBase(pathBase); + } + + app.UseStaticFiles(); + app.UseSession(); + + WebContextSeed.Seed(app, env); + + // Fix samesite issue when running eShop from docker-compose locally as by default http protocol is being used + // Refer to https://github.com/dotnet-architecture/eShopOnContainers/issues/1391 + app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = AspNetCore.Http.SameSiteMode.Lax }); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute("default", "{controller=Catalog}/{action=Index}/{id?}"); + endpoints.MapControllerRoute("defaultError", "{controller=Error}/{action=Error}"); + endpoints.MapControllers(); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + }); + } +} + +static class ServiceCollectionExtensions +{ + + public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) + { + services.AddApplicationInsightsTelemetry(configuration); + services.AddApplicationInsightsKubernetesEnricher(); + + return services; + } + + public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) + { + services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy()) + .AddUrlGroup(new Uri(configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }); + + return services; + } + + public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions(); + services.Configure(configuration); + services.AddSession(); + services.AddDistributedMemoryCache(); + + if (configuration.GetValue("IsClusterEnv") == bool.TrueString) + { + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.webmvc"; + }) + .PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(configuration["DPConnectionString"]), "DataProtection-Keys"); + } + + return services; + } + + // Adds all Http client services + public static IServiceCollection AddHttpClientServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(); + + //register delegating handlers + services.AddTransient(); + services.AddTransient(); + + //set 5 min as the lifetime for each HttpMessageHandler int the pool + services.AddHttpClient("extendedhandlerlifetime").SetHandlerLifetime(TimeSpan.FromMinutes(5)).AddDevspacesSupport(); + + //add http client services + services.AddHttpClient() + .SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Sample. Default lifetime is 2 minutes + .AddHttpMessageHandler() + .AddDevspacesSupport(); + + services.AddHttpClient() + .AddDevspacesSupport(); + + services.AddHttpClient() + .AddHttpMessageHandler() + .AddHttpMessageHandler() + .AddDevspacesSupport(); + + + //add custom application services + services.AddTransient, IdentityParser>(); + + return services; + } + + + public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) + { + var identityUrl = configuration.GetValue("IdentityUrl"); + var callBackUrl = configuration.GetValue("CallBackUrl"); + var sessionCookieLifetime = configuration.GetValue("SessionCookieLifetimeMinutes", 60); + + // Add Authentication services + + services.AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddCookie(setup => setup.ExpireTimeSpan = TimeSpan.FromMinutes(sessionCookieLifetime)) + .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => + { + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.Authority = identityUrl.ToString(); + options.SignedOutRedirectUri = callBackUrl.ToString(); + options.ClientId = "mvc"; + options.ClientSecret = "secret"; + options.ResponseType = "code"; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + options.RequireHttpsMetadata = false; + options.Scope.Add("openid"); + options.Scope.Add("profile"); + options.Scope.Add("orders"); + options.Scope.Add("basket"); + options.Scope.Add("webshoppingagg"); + options.Scope.Add("orders.signalrhub"); + options.Scope.Add("webhooks"); + }); + + return services; + } +} diff --git a/src/Web/WebMVC/ViewModels/Order.cs b/src/Web/WebMVC/ViewModels/Order.cs index fec4da0f02..af358336a7 100644 --- a/src/Web/WebMVC/ViewModels/Order.cs +++ b/src/Web/WebMVC/ViewModels/Order.cs @@ -1,7 +1,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.ViewModels; public class Order -{ +{ [JsonConverter(typeof(NumberToStringConverter))] public string OrderNumber { get; set; } @@ -45,7 +45,7 @@ public class Order public List ActionCodeSelectList => GetActionCodesByCurrentState(); - + public List OrderItems { get; set; } [Required] diff --git a/src/Web/WebMVC/Views/Shared/_Layout.cshtml b/src/Web/WebMVC/Views/Shared/_Layout.cshtml index 6cb3f43fba..9a634b78a4 100644 --- a/src/Web/WebMVC/Views/Shared/_Layout.cshtml +++ b/src/Web/WebMVC/Views/Shared/_Layout.cshtml @@ -86,23 +86,35 @@ @RenderSection("scripts", required: false) + + @using Microsoft.AspNetCore.Authentication; + @using Microsoft.Extensions.Options + @inject IOptions settings +