diff --git a/.github/workflows/albx-kitt.yml b/.github/workflows/albx-kitt.yml deleted file mode 100644 index 1c6ad0d..0000000 --- a/.github/workflows/albx-kitt.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Build and deploy .NET Core application to Web App albx-kitt -on: - workflow_dispatch: - push: - branches: [ main ] - paths: - - 'src/LemonBot.Web/**' - - 'src/KITT.Core/**' - - 'src/KITT.Web.App/**' - - 'src/KITT.Web.App.Tools/**' - - 'src/KITT.Web.App.UI/**' - - 'src/KITT.Web.Models/**' -env: - AZURE_WEBAPP_NAME: albx-kitt - AZURE_WEBAPP_PACKAGE_PATH: ./src/LemonBot.Web/published - CONFIGURATION: Release - DOTNET_VERSION: 8.0.x - WORKING_DIRECTORY: ./src/LemonBot.Web -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Setup client appsettings values - uses: microsoft/variable-substitution@v1 - with: - files: ./src/KITT.Web.App/wwwroot/appsettings.json - env: - AzureAdB2C.Authority: ${{ secrets.KITT_ADB2C_AUTHORITY }} - AzureAdB2C.ClientId: ${{ secrets.KITT_ADB2C_CLIENTID }} - ApiApp.Tenant: ${{ secrets.KITT_APIAPP_TENANT }} - ApiApp.AppId: ${{ secrets.KITT_APIAPP_APPID }} - - name: Restore - run: dotnet restore "${{ env.WORKING_DIRECTORY }}" - - name: Build - run: dotnet build "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-restore - - name: Publish - run: dotnet publish "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-build --output "${{ env.AZURE_WEBAPP_PACKAGE_PATH }}" - - name: Publish Artifacts - uses: actions/upload-artifact@v4 - with: - name: webapp - path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} - update-database: - runs-on: ubuntu-latest - needs: build - steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Install Entity Framework Core tools - run: | - dotnet new tool-manifest - dotnet tool install dotnet-ef - - name: Generate SQL script - run: dotnet ef migrations script --idempotent -p ./src/KITT.Core/KITT.Core.csproj -s ${{ env.WORKING_DIRECTORY }}/LemonBot.Web.csproj -o ./scripts/update-database.sql - - name: Azure login - uses: Azure/login@v1 - with: - creds: ${{ secrets.KITT_SQL_AZURE_CREDENTIALS }} - - name: Update Database - uses: Azure/sql-action@v2 - with: - connection-string: ${{ secrets.KITT_CONNECTIONSTRING }} - path: ./scripts/update-database.sql - deploy: - runs-on: ubuntu-latest - needs: [ build, update-database ] - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v4 - with: - name: webapp - path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} - - name: Deploy to Azure WebApp - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEBAPP_NAME }} - publish-profile: ${{ secrets.KITT_WEBAPP_PUBLISHPROFILE }} - package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml new file mode 100644 index 0000000..60b7030 --- /dev/null +++ b/.github/workflows/azure-dev.yml @@ -0,0 +1,94 @@ +# Run when commits are pushed to 109-road-to-aspire +on: + workflow_dispatch: + push: + # Run when commits are pushed to mainline branch (main or master) + # Set this to the mainline branch you are using + branches: + - main + +# Set up permissions for deploying with secretless Azure federated credentials +# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication +permissions: + id-token: write + contents: read + +env: + DOTNET_VERSION: 9.0.x + +jobs: + update-database: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Install Entity Framework Core tools + run: | + dotnet new tool-manifest + dotnet tool install dotnet-ef + - name: Generate SQL script + run: dotnet ef migrations script --idempotent -p ./src/KITT.Core/KITT.Core.csproj -s ./src/KITT.Support.Seeder/KITT.Support.Seeder.csproj -o ./scripts/update-database.sql + - name: Azure login + uses: Azure/login@v1 + with: + creds: ${{ secrets.KITT_SQL_AZURE_CREDENTIALS }} + - name: Update Database + uses: Azure/sql-action@v2 + with: + connection-string: ${{ secrets.AZURE_KITT_DATABASE }} + path: ./scripts/update-database.sql + build: + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install azd + uses: Azure/setup-azd@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.x.x + 9.x.x + + - name: Log in with Azure (Federated Credentials) + run: | + azd auth login ` + --client-id "$Env:AZURE_CLIENT_ID" ` + --federated-credential-provider "github" ` + --tenant-id "$Env:AZURE_TENANT_ID" + shell: pwsh + + + - name: Provision Infrastructure + run: azd provision --no-prompt + env: + AZURE_WEB_APP_ID: ${{ secrets.AZURE_WEB_APP_ID }} + AZURE_WEB_APP_SECRET: ${{ secrets.AZURE_WEB_APP_SECRET }} + AZURE_ENTRA_ID_TENANT_ID: ${{ secrets.AZURE_ENTRA_ID_TENANT_ID }} + AZURE_KITT_DATABASE: ${{ secrets.AZURE_KITT_DATABASE }} + AZURE_CMS_API_APP_ID: ${{ secrets.AZURE_CMS_API_APP_ID }} + AZURE_ENTRA_ID_DOMAIN_NAME: ${{ secrets.AZURE_ENTRA_ID_DOMAIN_NAME }} + AZURE_PROPOSALS_API_APP_ID: ${{ secrets.AZURE_PROPOSALS_API_APP_ID }} + + - name: Deploy Application + run: azd deploy --no-prompt + env: + AZURE_WEB_APP_ID: ${{ secrets.AZURE_WEB_APP_ID }} + AZURE_WEB_APP_SECRET: ${{ secrets.AZURE_WEB_APP_SECRET }} + AZURE_ENTRA_ID_TENANT_ID: ${{ secrets.AZURE_ENTRA_ID_TENANT_ID }} + AZURE_KITT_DATABASE: ${{ secrets.AZURE_KITT_DATABASE }} + AZURE_CMS_API_APP_ID: ${{ secrets.AZURE_CMS_API_APP_ID }} + AZURE_ENTRA_ID_DOMAIN_NAME: ${{ secrets.AZURE_ENTRA_ID_DOMAIN_NAME }} + AZURE_PROPOSALS_API_APP_ID: ${{ secrets.AZURE_PROPOSALS_API_APP_ID }} + diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml deleted file mode 100644 index de405cc..0000000 --- a/.github/workflows/build-and-test.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Builds and runs all the tests - -on: - pull_request: - branches: [ main ] - paths: - - '**.cs' - - '**.razor' - - '**.cshtml' - - '**.csproj' - -env: - DOTNET_VERSION: 8.0.x - -jobs: - build: - name: Build on ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v2 - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Setup appsettings values - uses: microsoft/variable-substitution@v1 - with: - files: ./src/LemonBot.Web/appsettings.json - env: - AzureAdB2C.Instance: ${{ secrets.TEST_ADB2C_INSTANCE }} - AzureAdB2C.ClientId: ${{ secrets.TEST_ADB2C_CLIENT }} - AzureAdB2C.Domain: ${{ secrets.TEST_ADB2C_DOMAIN }} - AzureAdB2C.SignUpSignInPolicyId: ${{ secrets.TEST_ADB2C_POLICY }} - - name: Restore dependencies - run: dotnet restore - - name: Build - run: dotnet build --no-restore - - name: Test - run: dotnet test --no-build --verbosity normal diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yml index 8e7cea1..bc3271c 100644 --- a/.github/workflows/code-analysis.yml +++ b/.github/workflows/code-analysis.yml @@ -7,7 +7,7 @@ on: branches: [main] env: - DOTNET_VERSION: 8.0.x + DOTNET_VERSION: 9.0.x jobs: analyze: diff --git a/.github/workflows/kitt-bot-api.yml b/.github/workflows/kitt-bot-api.yml deleted file mode 100644 index 6147987..0000000 --- a/.github/workflows/kitt-bot-api.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Build and deploy Bot Functions -on: - push: - branches: [ main ] - paths: - - 'src/KITT.Bot.Functions/**' -env: - AZURE_FUNCTIONAPP_NAME: kitt-bot-api - AZURE_FUNCTIONAPP_PACKAGE_PATH: src/KITT.Bot.Functions/published - CONFIGURATION: Release - DOTNET_CORE_VERSION: 6.0.x - WORKING_DIRECTORY: src/KITT.Bot.Functions -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Setup .NET SDK - uses: actions/setup-dotnet@v3 - with: - dotnet-version: ${{ env.DOTNET_CORE_VERSION }} - - name: Restore - run: dotnet restore "${{ env.WORKING_DIRECTORY }}" - - name: Build - run: dotnet build "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-restore - - name: Publish - run: dotnet publish "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-build --output "${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}" - - name: Publish Artifacts - uses: actions/upload-artifact@v4 - with: - name: functionapp - path: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} - deploy: - runs-on: ubuntu-latest - needs: build - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v4 - with: - name: functionapp - path: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} - - name: Deploy to Azure Function App - uses: Azure/functions-action@v1 - with: - app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }} - publish-profile: ${{ secrets.kitt_bot_api_845D }} - package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} diff --git a/.github/workflows/kitt-bot.yml b/.github/workflows/kitt-bot.yml deleted file mode 100644 index 9b269fb..0000000 --- a/.github/workflows/kitt-bot.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Build and deploy BOT to Azure Web Job -on: - push: - branches: [ main ] - paths: - - 'src/LemonBot/**' - - 'src/LemonBot.Clients/**' - - 'src/LemonBot.Commands/**' -env: - AZURE_WEBAPP_NAME: kitt-bot - AZURE_PACKAGE_PATH: ./output/app_data/jobs/continuous/LemonBot - CONFIGURATION: Release - DOTNET_VERSION: 8.0.x - WORKING_DIRECTORY: ./src/LemonBot - ZIP_FILENAME: 'LemonBot.zip' - ZIP_PATH: './output' - ZIP_FILEPATH: './output/LemonBot.zip' - ZIP_WORKING_DIRECTORY: 'app_data/jobs/continuous/LemonBot' -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Restore - run: dotnet restore "${{ env.WORKING_DIRECTORY }}" - - name: Build - run: dotnet build "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-restore - - name: Publish - run: dotnet publish "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-build --output "${{ env.AZURE_PACKAGE_PATH }}" - - name: Create ZIP file - uses: thedoctor0/zip-release@0.7.1 - with: - type: 'zip' - filename: ${{ env.ZIP_FILENAME }} - directory: ${{ env.ZIP_PATH }} - path: ${{ env.ZIP_WORKING_DIRECTORY }} - exclusions: 'worker.exe app_data/**/unix/*' - - name: Deploy - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEBAPP_NAME }} - package: ${{ env.ZIP_FILEPATH }} - publish-profile: ${{ secrets.KITT_BOT_PUBLISHPROFILE }} diff --git a/.github/workflows/kitt-telegram.yml b/.github/workflows/kitt-telegram.yml deleted file mode 100644 index 4b8fba1..0000000 --- a/.github/workflows/kitt-telegram.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Build and deploy Telegram Functions -on: - workflow_dispatch: - push: - branches: [ main ] - paths: - - 'src/KITT.Telegram.Functions/**' -env: - AZURE_FUNCTIONAPP_NAME: kitt-telegram - AZURE_FUNCTIONAPP_PACKAGE_PATH: src/KITT.Telegram.Functions/published - CONFIGURATION: Release - DOTNET_CORE_VERSION: 8.0.x - WORKING_DIRECTORY: src/KITT.Telegram.Functions -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Setup .NET SDK - uses: actions/setup-dotnet@v3 - with: - dotnet-version: ${{ env.DOTNET_CORE_VERSION }} - - name: Restore - run: dotnet restore "${{ env.WORKING_DIRECTORY }}" - - name: Build - run: dotnet build "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-restore - - name: Publish - run: dotnet publish "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-build --output "${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}" - - name: Publish Artifacts - uses: actions/upload-artifact@v4 - with: - name: functionapp - path: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} - deploy: - runs-on: ubuntu-latest - needs: build - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v4 - with: - name: functionapp - path: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} - - name: Deploy to Azure Function App - uses: Azure/functions-action@v1 - with: - app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }} - publish-profile: ${{ secrets.KITT_TELEGRAM_PUBLISHPROFILE }} - package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} diff --git a/.gitignore b/.gitignore index a3fbdeb..dc173ee 100644 --- a/.gitignore +++ b/.gitignore @@ -355,4 +355,5 @@ MigrationBackup/ # files to exclude *.sql *.job -profile.arm.json \ No newline at end of file +profile.arm.json +.azure diff --git a/LemonBot.sln b/LemonBot.sln index e79c7df..90747f1 100644 --- a/LemonBot.sln +++ b/LemonBot.sln @@ -11,8 +11,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LemonBot.Commands", "src\Le EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LemonBot.Clients", "src\LemonBot.Clients\LemonBot.Clients.csproj", "{D2F9C1BB-F01E-459E-8C7E-F225F83A47B1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LemonBot.Web", "src\LemonBot.Web\LemonBot.Web.csproj", "{D2265674-E203-4073-9910-C9412A220B80}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "KITT", "KITT", "{53F76412-0B22-4C62-8519-834A82747AD4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Web.Models", "src\KITT.Web.Models\KITT.Web.Models.csproj", "{A9018405-BC9E-41FD-955A-5A3B4AED27A4}" @@ -23,12 +21,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B32B1475 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Core.Test", "tests\KITT.Core.Test\KITT.Core.Test.csproj", "{AB7A75D3-84BA-4AFD-BC49-D0C7545FB235}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Web.App", "src\KITT.Web.App\KITT.Web.App.csproj", "{9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Web.App.Test", "tests\KITT.Web.App.Test\KITT.Web.App.Test.csproj", "{C981DD09-2572-47B7-B336-A31EC6E3B7E7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LemonBot.Web.Test.Integration", "tests\LemonBot.Web.Test.Integration\LemonBot.Web.Test.Integration.csproj", "{78414030-A691-4D48-84FA-A42E4ED034E7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Web.App.UI", "src\KITT.Web.App.UI\KITT.Web.App.UI.csproj", "{2B84BEBB-A29E-4BDE-893F-BBCCE979A0BE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Bot.Functions", "src\KITT.Bot.Functions\KITT.Bot.Functions.csproj", "{EB1A10AF-8561-42E1-9348-6C8669F09176}" @@ -39,6 +31,42 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Telegram.Functions", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Telegram.Functions.Test", "tests\KITT.Telegram.Functions.Test\KITT.Telegram.Functions.Test.csproj", "{92122C69-6CEC-4FEA-BAC5-AC7F8197C6FE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Cms.Web.Api", "src\KITT.Cms.Web.Api\KITT.Cms.Web.Api.csproj", "{406A74C4-AF3B-43C0-9D54-111B7CE24EBF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Cms.Web.Models", "src\KITT.Cms.Web.Models\KITT.Cms.Web.Models.csproj", "{7E0161FF-5AB4-451E-A95A-45D15BFC8C29}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Web.Shared", "src\KITT.Web.Shared\KITT.Web.Shared.csproj", "{A701C3B3-E70B-4386-BBD4-39B9376966EE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.AppHost", "src\KITT.AppHost\KITT.AppHost.csproj", "{7C417964-199A-40D7-99AD-77A356F6B8D0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.ServiceDefaults", "src\KITT.ServiceDefaults\KITT.ServiceDefaults.csproj", "{F1D748E1-2241-446F-88D0-CEFCCCEB7A93}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Web.App", "src\KITT.Web.App\KITT.Web.App\KITT.Web.App.csproj", "{4CBD2E55-3636-445A-93BB-84D549D64501}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Web.App.Client", "src\KITT.Web.App\KITT.Web.App.Client\KITT.Web.App.Client.csproj", "{C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Cms", "Cms", "{90F26E81-71A2-4438-ACD9-64D3AC5636AF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KITT.Cms.Web.App", "src\KITT.Cms.Web.App\KITT.Cms.Web.App.csproj", "{E51D737B-4337-47BD-84AF-97086FDDA158}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Proposals", "Proposals", "{5C9DDC07-E0A9-48AA-8D75-0188C65B2D5E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KITT.Proposals.Web.Api", "src\KITT.Proposals.Web.Api\KITT.Proposals.Web.Api.csproj", "{CA7C2776-D362-4AD6-BA88-2C0B38D71C42}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KITT.Proposals.Web.Models", "src\KITT.Proposals.Web.Models\KITT.Proposals.Web.Models.csproj", "{B7D3A829-1BFD-455B-8E7B-074DC16EE85D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KITT.Proposals.Web.App", "src\KITT.Proposals.Web.App\KITT.Proposals.Web.App.csproj", "{B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KITT.Cms.Web.Api.Test", "tests\KITT.Cms.Web.Api.Test\KITT.Cms.Web.Api.Test.csproj", "{A29853FF-53A7-499F-B309-7154A51D9908}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KITT.Web.Testing", "tests\KITT.Web.Testing\KITT.Web.Testing.csproj", "{AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Support", "Support", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KITT.Support.Seeder", "src\KITT.Support.Seeder\KITT.Support.Seeder.csproj", "{92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KITT.Services", "src\KITT.Services\KITT.Services.csproj", "{C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -85,18 +113,6 @@ Global {D2F9C1BB-F01E-459E-8C7E-F225F83A47B1}.Release|x64.Build.0 = Release|Any CPU {D2F9C1BB-F01E-459E-8C7E-F225F83A47B1}.Release|x86.ActiveCfg = Release|Any CPU {D2F9C1BB-F01E-459E-8C7E-F225F83A47B1}.Release|x86.Build.0 = Release|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Debug|x64.ActiveCfg = Debug|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Debug|x64.Build.0 = Debug|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Debug|x86.ActiveCfg = Debug|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Debug|x86.Build.0 = Debug|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Release|Any CPU.Build.0 = Release|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Release|x64.ActiveCfg = Release|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Release|x64.Build.0 = Release|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Release|x86.ActiveCfg = Release|Any CPU - {D2265674-E203-4073-9910-C9412A220B80}.Release|x86.Build.0 = Release|Any CPU {A9018405-BC9E-41FD-955A-5A3B4AED27A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A9018405-BC9E-41FD-955A-5A3B4AED27A4}.Debug|Any CPU.Build.0 = Debug|Any CPU {A9018405-BC9E-41FD-955A-5A3B4AED27A4}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -133,42 +149,6 @@ Global {AB7A75D3-84BA-4AFD-BC49-D0C7545FB235}.Release|x64.Build.0 = Release|Any CPU {AB7A75D3-84BA-4AFD-BC49-D0C7545FB235}.Release|x86.ActiveCfg = Release|Any CPU {AB7A75D3-84BA-4AFD-BC49-D0C7545FB235}.Release|x86.Build.0 = Release|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Debug|x64.ActiveCfg = Debug|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Debug|x64.Build.0 = Debug|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Debug|x86.ActiveCfg = Debug|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Debug|x86.Build.0 = Debug|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Release|Any CPU.Build.0 = Release|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Release|x64.ActiveCfg = Release|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Release|x64.Build.0 = Release|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Release|x86.ActiveCfg = Release|Any CPU - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7}.Release|x86.Build.0 = Release|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Debug|x64.ActiveCfg = Debug|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Debug|x64.Build.0 = Debug|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Debug|x86.ActiveCfg = Debug|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Debug|x86.Build.0 = Debug|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Release|Any CPU.Build.0 = Release|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Release|x64.ActiveCfg = Release|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Release|x64.Build.0 = Release|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Release|x86.ActiveCfg = Release|Any CPU - {C981DD09-2572-47B7-B336-A31EC6E3B7E7}.Release|x86.Build.0 = Release|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Debug|x64.ActiveCfg = Debug|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Debug|x64.Build.0 = Debug|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Debug|x86.ActiveCfg = Debug|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Debug|x86.Build.0 = Debug|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Release|Any CPU.Build.0 = Release|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Release|x64.ActiveCfg = Release|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Release|x64.Build.0 = Release|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Release|x86.ActiveCfg = Release|Any CPU - {78414030-A691-4D48-84FA-A42E4ED034E7}.Release|x86.Build.0 = Release|Any CPU {2B84BEBB-A29E-4BDE-893F-BBCCE979A0BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2B84BEBB-A29E-4BDE-893F-BBCCE979A0BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B84BEBB-A29E-4BDE-893F-BBCCE979A0BE}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -229,6 +209,186 @@ Global {92122C69-6CEC-4FEA-BAC5-AC7F8197C6FE}.Release|x64.Build.0 = Release|Any CPU {92122C69-6CEC-4FEA-BAC5-AC7F8197C6FE}.Release|x86.ActiveCfg = Release|Any CPU {92122C69-6CEC-4FEA-BAC5-AC7F8197C6FE}.Release|x86.Build.0 = Release|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Debug|x64.ActiveCfg = Debug|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Debug|x64.Build.0 = Debug|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Debug|x86.ActiveCfg = Debug|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Debug|x86.Build.0 = Debug|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Release|Any CPU.Build.0 = Release|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Release|x64.ActiveCfg = Release|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Release|x64.Build.0 = Release|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Release|x86.ActiveCfg = Release|Any CPU + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF}.Release|x86.Build.0 = Release|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Debug|x64.ActiveCfg = Debug|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Debug|x64.Build.0 = Debug|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Debug|x86.ActiveCfg = Debug|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Debug|x86.Build.0 = Debug|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Release|Any CPU.Build.0 = Release|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Release|x64.ActiveCfg = Release|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Release|x64.Build.0 = Release|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Release|x86.ActiveCfg = Release|Any CPU + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29}.Release|x86.Build.0 = Release|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Debug|x64.ActiveCfg = Debug|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Debug|x64.Build.0 = Debug|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Debug|x86.ActiveCfg = Debug|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Debug|x86.Build.0 = Debug|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Release|Any CPU.Build.0 = Release|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Release|x64.ActiveCfg = Release|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Release|x64.Build.0 = Release|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Release|x86.ActiveCfg = Release|Any CPU + {A701C3B3-E70B-4386-BBD4-39B9376966EE}.Release|x86.Build.0 = Release|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Debug|x64.Build.0 = Debug|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Debug|x86.Build.0 = Debug|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Release|Any CPU.Build.0 = Release|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Release|x64.ActiveCfg = Release|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Release|x64.Build.0 = Release|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Release|x86.ActiveCfg = Release|Any CPU + {7C417964-199A-40D7-99AD-77A356F6B8D0}.Release|x86.Build.0 = Release|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Debug|x64.ActiveCfg = Debug|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Debug|x64.Build.0 = Debug|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Debug|x86.ActiveCfg = Debug|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Debug|x86.Build.0 = Debug|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Release|Any CPU.Build.0 = Release|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Release|x64.ActiveCfg = Release|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Release|x64.Build.0 = Release|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Release|x86.ActiveCfg = Release|Any CPU + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93}.Release|x86.Build.0 = Release|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Debug|x64.ActiveCfg = Debug|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Debug|x64.Build.0 = Debug|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Debug|x86.ActiveCfg = Debug|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Debug|x86.Build.0 = Debug|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Release|Any CPU.Build.0 = Release|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Release|x64.ActiveCfg = Release|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Release|x64.Build.0 = Release|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Release|x86.ActiveCfg = Release|Any CPU + {4CBD2E55-3636-445A-93BB-84D549D64501}.Release|x86.Build.0 = Release|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Debug|x64.ActiveCfg = Debug|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Debug|x64.Build.0 = Debug|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Debug|x86.ActiveCfg = Debug|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Debug|x86.Build.0 = Debug|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Release|Any CPU.Build.0 = Release|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Release|x64.ActiveCfg = Release|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Release|x64.Build.0 = Release|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Release|x86.ActiveCfg = Release|Any CPU + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4}.Release|x86.Build.0 = Release|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Debug|x64.ActiveCfg = Debug|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Debug|x64.Build.0 = Debug|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Debug|x86.ActiveCfg = Debug|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Debug|x86.Build.0 = Debug|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Release|Any CPU.Build.0 = Release|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Release|x64.ActiveCfg = Release|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Release|x64.Build.0 = Release|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Release|x86.ActiveCfg = Release|Any CPU + {E51D737B-4337-47BD-84AF-97086FDDA158}.Release|x86.Build.0 = Release|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Debug|x64.ActiveCfg = Debug|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Debug|x64.Build.0 = Debug|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Debug|x86.Build.0 = Debug|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Release|Any CPU.Build.0 = Release|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Release|x64.ActiveCfg = Release|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Release|x64.Build.0 = Release|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Release|x86.ActiveCfg = Release|Any CPU + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42}.Release|x86.Build.0 = Release|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Debug|x64.Build.0 = Debug|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Debug|x86.Build.0 = Debug|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Release|Any CPU.Build.0 = Release|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Release|x64.ActiveCfg = Release|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Release|x64.Build.0 = Release|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Release|x86.ActiveCfg = Release|Any CPU + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D}.Release|x86.Build.0 = Release|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Debug|x64.ActiveCfg = Debug|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Debug|x64.Build.0 = Debug|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Debug|x86.ActiveCfg = Debug|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Debug|x86.Build.0 = Debug|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Release|Any CPU.Build.0 = Release|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Release|x64.ActiveCfg = Release|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Release|x64.Build.0 = Release|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Release|x86.ActiveCfg = Release|Any CPU + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD}.Release|x86.Build.0 = Release|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Debug|x64.ActiveCfg = Debug|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Debug|x64.Build.0 = Debug|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Debug|x86.ActiveCfg = Debug|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Debug|x86.Build.0 = Debug|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Release|Any CPU.Build.0 = Release|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Release|x64.ActiveCfg = Release|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Release|x64.Build.0 = Release|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Release|x86.ActiveCfg = Release|Any CPU + {A29853FF-53A7-499F-B309-7154A51D9908}.Release|x86.Build.0 = Release|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Debug|x64.ActiveCfg = Debug|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Debug|x64.Build.0 = Debug|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Debug|x86.ActiveCfg = Debug|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Debug|x86.Build.0 = Debug|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Release|Any CPU.Build.0 = Release|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Release|x64.ActiveCfg = Release|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Release|x64.Build.0 = Release|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Release|x86.ActiveCfg = Release|Any CPU + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9}.Release|x86.Build.0 = Release|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Debug|x64.ActiveCfg = Debug|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Debug|x64.Build.0 = Debug|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Debug|x86.ActiveCfg = Debug|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Debug|x86.Build.0 = Debug|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Release|Any CPU.Build.0 = Release|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Release|x64.ActiveCfg = Release|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Release|x64.Build.0 = Release|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Release|x86.ActiveCfg = Release|Any CPU + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98}.Release|x86.Build.0 = Release|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Debug|x64.ActiveCfg = Debug|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Debug|x64.Build.0 = Debug|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Debug|x86.ActiveCfg = Debug|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Debug|x86.Build.0 = Debug|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Release|Any CPU.Build.0 = Release|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Release|x64.ActiveCfg = Release|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Release|x64.Build.0 = Release|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Release|x86.ActiveCfg = Release|Any CPU + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -237,19 +397,33 @@ Global {C56ABDA1-E83F-4C97-B3C2-3A248526E4FC} = {F47298C6-FC9C-4B30-88D6-3DDAAD1B7B65} {0017D050-5521-4C55-B1CD-2DDB228CEC42} = {F47298C6-FC9C-4B30-88D6-3DDAAD1B7B65} {D2F9C1BB-F01E-459E-8C7E-F225F83A47B1} = {F47298C6-FC9C-4B30-88D6-3DDAAD1B7B65} - {D2265674-E203-4073-9910-C9412A220B80} = {F47298C6-FC9C-4B30-88D6-3DDAAD1B7B65} {53F76412-0B22-4C62-8519-834A82747AD4} = {F47298C6-FC9C-4B30-88D6-3DDAAD1B7B65} {A9018405-BC9E-41FD-955A-5A3B4AED27A4} = {53F76412-0B22-4C62-8519-834A82747AD4} {66F99049-7BCF-47D4-81FF-8E0254FE82EE} = {53F76412-0B22-4C62-8519-834A82747AD4} {AB7A75D3-84BA-4AFD-BC49-D0C7545FB235} = {B32B1475-75B8-41DD-BB71-D6106152C656} - {9E51F921-9BF4-47C2-92C7-A958F5E2F4D7} = {53F76412-0B22-4C62-8519-834A82747AD4} - {C981DD09-2572-47B7-B336-A31EC6E3B7E7} = {B32B1475-75B8-41DD-BB71-D6106152C656} - {78414030-A691-4D48-84FA-A42E4ED034E7} = {B32B1475-75B8-41DD-BB71-D6106152C656} {2B84BEBB-A29E-4BDE-893F-BBCCE979A0BE} = {53F76412-0B22-4C62-8519-834A82747AD4} {EB1A10AF-8561-42E1-9348-6C8669F09176} = {53F76412-0B22-4C62-8519-834A82747AD4} {6E87C525-EB68-4DAD-9F63-39E426D01668} = {53F76412-0B22-4C62-8519-834A82747AD4} {A6A4722A-4AA2-4B9C-A2FB-A7C171E03DF4} = {53F76412-0B22-4C62-8519-834A82747AD4} {92122C69-6CEC-4FEA-BAC5-AC7F8197C6FE} = {B32B1475-75B8-41DD-BB71-D6106152C656} + {406A74C4-AF3B-43C0-9D54-111B7CE24EBF} = {90F26E81-71A2-4438-ACD9-64D3AC5636AF} + {7E0161FF-5AB4-451E-A95A-45D15BFC8C29} = {90F26E81-71A2-4438-ACD9-64D3AC5636AF} + {A701C3B3-E70B-4386-BBD4-39B9376966EE} = {53F76412-0B22-4C62-8519-834A82747AD4} + {7C417964-199A-40D7-99AD-77A356F6B8D0} = {F47298C6-FC9C-4B30-88D6-3DDAAD1B7B65} + {F1D748E1-2241-446F-88D0-CEFCCCEB7A93} = {F47298C6-FC9C-4B30-88D6-3DDAAD1B7B65} + {4CBD2E55-3636-445A-93BB-84D549D64501} = {53F76412-0B22-4C62-8519-834A82747AD4} + {C4ED93D6-3B6F-4EF3-84D4-3B76FE7875F4} = {53F76412-0B22-4C62-8519-834A82747AD4} + {90F26E81-71A2-4438-ACD9-64D3AC5636AF} = {53F76412-0B22-4C62-8519-834A82747AD4} + {E51D737B-4337-47BD-84AF-97086FDDA158} = {90F26E81-71A2-4438-ACD9-64D3AC5636AF} + {5C9DDC07-E0A9-48AA-8D75-0188C65B2D5E} = {53F76412-0B22-4C62-8519-834A82747AD4} + {CA7C2776-D362-4AD6-BA88-2C0B38D71C42} = {5C9DDC07-E0A9-48AA-8D75-0188C65B2D5E} + {B7D3A829-1BFD-455B-8E7B-074DC16EE85D} = {5C9DDC07-E0A9-48AA-8D75-0188C65B2D5E} + {B5656CFD-8ED7-4CC8-9AE4-CD0BE14B98AD} = {5C9DDC07-E0A9-48AA-8D75-0188C65B2D5E} + {A29853FF-53A7-499F-B309-7154A51D9908} = {B32B1475-75B8-41DD-BB71-D6106152C656} + {AA34DAE8-59AE-4500-8D60-6CA2B899DCD9} = {B32B1475-75B8-41DD-BB71-D6106152C656} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {53F76412-0B22-4C62-8519-834A82747AD4} + {92B53BDC-3A3B-D5E5-70AE-56E877F1BB98} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {C4747EBF-4249-4D6A-9DD0-73C35E0C9AA1} = {F47298C6-FC9C-4B30-88D6-3DDAAD1B7B65} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08491B30-AD3E-4AEA-AAB9-40A83A96F393} diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000..ad48b25 --- /dev/null +++ b/azure.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: kitt +services: + app: + language: dotnet + project: ./src/KITT.AppHost/KITT.AppHost.csproj + host: containerapp diff --git a/docs/deploy-instructions.md b/docs/deploy-instructions.md new file mode 100644 index 0000000..5f8e49d --- /dev/null +++ b/docs/deploy-instructions.md @@ -0,0 +1,600 @@ +# Deploy to Azure con .NET Container Support + +## Richiesta Iniziale +L'utente ha richiesto una GitHub Action che: +- Richiami i file Bicep presenti nella cartella [`infra`](infra ) +- Crei le immagini Docker dei progetti +- Le pushi sulle istanze di Azure Container Registry definite nei Bicep + +## Prima Soluzione Proposta +Ho inizialmente suggerito una GitHub Action completa con Dockerfile tradizionali, includendo: + +### GitHub Action completa +```yaml +name: Deploy to Azure + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +env: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + RESOURCE_GROUP: "kitt-rg" + LOCATION: "Italy North" + +jobs: + deploy-infrastructure: + runs-on: ubuntu-latest + outputs: + container-registry-name: ${{ steps.deploy.outputs.kitt_env_AZURE_CONTAINER_REGISTRY_NAME }} + container-registry-endpoint: ${{ steps.deploy.outputs.kitt_env_AZURE_CONTAINER_REGISTRY_ENDPOINT }} + container-registry-managed-identity-id: ${{ steps.deploy.outputs.kitt_env_AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} + container-apps-environment-id: ${{ steps.deploy.outputs.kitt_env_AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} + container-apps-environment-default-domain: ${{ steps.deploy.outputs.kitt_env_AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} + steps: + - uses: actions/checkout@v4 + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Deploy Bicep Infrastructure + id: deploy + uses: azure/arm-deploy@v1 + with: + subscriptionId: ${{ env.AZURE_SUBSCRIPTION_ID }} + resourceGroupName: ${{ env.RESOURCE_GROUP }} + template: ./infra/main.bicep + parameters: > + resourceGroupName=${{ env.RESOURCE_GROUP }} + location="${{ env.LOCATION }}" + deploymentMode: Incremental + + build-and-push-images: + needs: deploy-infrastructure + runs-on: ubuntu-latest + strategy: + matrix: + project: + - name: "kitt-webapp" + path: "./src/KITT.Web.App" + csproj: "KITT.Web.App.csproj" + port: "8080" + - name: "kitt-cms-api" + path: "./src/KITT.Cms.Web.Api" + csproj: "KITT.Cms.Web.Api.csproj" + port: "8080" + - name: "kitt-proposals-api" + path: "./src/KITT.Proposals.Web.Api" + csproj: "KITT.Proposals.Web.Api.csproj" + port: "8080" + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Login to Azure Container Registry + run: | + az acr login --name ${{ needs.deploy-infrastructure.outputs.container-registry-name }} + + - name: Build and Push Container using .NET SDK + run: | + REGISTRY_ENDPOINT="${{ needs.deploy-infrastructure.outputs.container-registry-endpoint }}" + IMAGE_TAG="${REGISTRY_ENDPOINT}/${{ matrix.project.name }}:${{ github.sha }}" + + cd ${{ matrix.project.path }} + dotnet publish ${{ matrix.project.csproj }} \ + --os linux \ + --arch x64 \ + -p:PublishProfile=DefaultContainer \ + -p:ContainerImageName=${{ matrix.project.name }} \ + -p:ContainerImageTag=${{ github.sha }} \ + -p:ContainerRegistry=${REGISTRY_ENDPOINT} + + deploy-container-apps: + needs: [deploy-infrastructure, build-and-push-images] + runs-on: ubuntu-latest + strategy: + matrix: + app: + - name: "kitt-webapp" + bicep: "./infra/kitt-webapp/kitt-webapp.bicep" + port: "8080" + - name: "kitt-cms-api" + bicep: "./infra/kitt-cms-api/kitt-cms-api.bicep" + port: "8080" + - name: "kitt-proposals-api" + bicep: "./infra/kitt-proposals-api/kitt-proposals-api.bicep" + port: "8080" + + steps: + - uses: actions/checkout@v4 + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Deploy Container App + uses: azure/arm-deploy@v1 + with: + subscriptionId: ${{ env.AZURE_SUBSCRIPTION_ID }} + resourceGroupName: ${{ env.RESOURCE_GROUP }} + template: ${{ matrix.app.bicep }} + parameters: > + location="${{ env.LOCATION }}" + ${{ matrix.app.name }}_containerimage="${{ needs.deploy-infrastructure.outputs.container-registry-endpoint }}/${{ matrix.app.name }}:${{ github.sha }}" + ${{ matrix.app.name }}_containerport="${{ matrix.app.port }}" + kitt_env_outputs_azure_container_apps_environment_id="${{ needs.deploy-infrastructure.outputs.container-apps-environment-id }}" + kitt_env_outputs_azure_container_registry_endpoint="${{ needs.deploy-infrastructure.outputs.container-registry-endpoint }}" + kitt_env_outputs_azure_container_registry_managed_identity_id="${{ needs.deploy-infrastructure.outputs.container-registry-managed-identity-id }}" + kitt_env_outputs_azure_container_apps_environment_default_domain="${{ needs.deploy-infrastructure.outputs.container-apps-environment-default-domain }}" + kittdatabase_connectionstring="${{ secrets.KITT_DATABASE_CONNECTION_STRING }}" + entraidtenantid_value="${{ secrets.ENTRA_ID_TENANT_ID }}" + entraiddomainname_value="${{ secrets.ENTRA_ID_DOMAIN_NAME }}" + webappid_value="${{ secrets.WEB_APP_ID }}" + webappsecret_value="${{ secrets.WEB_APP_SECRET }}" + cmsapiappid_value="${{ secrets.CMS_API_APP_ID }}" + proposalsapiappid_value="${{ secrets.PROPOSALS_API_APP_ID }}" + deploymentMode: Incremental +``` + +### Secrets richiesti +``` +AZURE_CREDENTIALS +AZURE_SUBSCRIPTION_ID +AZURE_TENANT_ID +AZURE_CLIENT_ID +AZURE_CLIENT_SECRET +AZURE_PRINCIPAL_ID +KITT_DATABASE_CONNECTION_STRING +ENTRA_ID_TENANT_ID +ENTRA_ID_DOMAIN_NAME +WEB_APP_ID +WEB_APP_SECRET +CMS_API_APP_ID +PROPOSALS_API_APP_ID +``` + +## Configurazione dei Secrets Azure + +### 1. Creazione Service Principal per GitHub Actions + +Per configurare l'autenticazione automatica con Azure, devi creare una Service Principal. Esegui il seguente comando con Azure CLI: + +```bash +# Sostituisci con il tuo ID subscription +az ad sp create-for-rbac \ + --name "GitHub-Actions-KITT" \ + --role contributor \ + --scopes /subscriptions/ \ + --sdk-auth +``` + +Questo comando restituirà un JSON simile a questo: +```json +{ + "clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "clientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "subscriptionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "activeDirectoryEndpointUrl": "https://login.microsoftonline.com", + "resourceManagerEndpointUrl": "https://management.azure.com/", + "activeDirectoryGraphResourceId": "https://graph.windows.net/", + "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/", + "galleryEndpointUrl": "https://gallery.azure.com/", + "managementEndpointUrl": "https://management.core.windows.net/" +} +``` + +### 2. Configurazione GitHub Secrets + +Aggiungi i seguenti secrets nel tuo repository GitHub (Settings → Secrets and variables → Actions): + +#### Secrets Azure Infrastructure +- **AZURE_CREDENTIALS**: L'intero JSON restituito dal comando `az ad sp create-for-rbap` +- **AZURE_SUBSCRIPTION_ID**: Il valore di `subscriptionId` dal JSON +- **AZURE_TENANT_ID**: Il valore di `tenantId` dal JSON +- **AZURE_CLIENT_ID**: Il valore di `clientId` dal JSON +- **AZURE_CLIENT_SECRET**: Il valore di `clientSecret` dal JSON +- **AZURE_PRINCIPAL_ID**: Il valore di `clientId` dal JSON (stesso valore di AZURE_CLIENT_ID) + +#### Secrets Database +- **KITT_DATABASE_CONNECTION_STRING**: Stringa di connessione completa al database SQL Azure + +### 3. Configurazione Entra ID (Azure AD) + +Per i secrets di autenticazione delle applicazioni web, devi registrare le applicazioni in Entra ID: + +#### 3.1 Ottenere informazioni del tenant +```bash +# Ottieni Tenant ID +az account show --query tenantId -o tsv + +# Ottieni Domain Name +az rest --method GET --url "https://graph.microsoft.com/v1.0/domains" --query "value[?isDefault].id" -o tsv +``` + +#### 3.2 Registrare le applicazioni Web +```bash +# Registra Web App principale +az ad app create \ + --display-name "KITT-WebApp" \ + --web-redirect-uris "https://your-webapp-url/signin-oidc" \ + --required-resource-accesses @webapp-manifest.json + +# Registra CMS API +az ad app create \ + --display-name "KITT-CMS-API" \ + --identifier-uris "api://kitt-cms-api" + +# Registra Proposals API +az ad app create \ + --display-name "KITT-Proposals-API" \ + --identifier-uris "api://kitt-proposals-api" +``` + +#### 3.3 Creare client secrets per le applicazioni +```bash +# Per ogni app registrata, crea un client secret +az ad app credential reset --id --display-name "GitHub-Actions-Secret" +``` + +#### Secrets Entra ID da configurare +- **ENTRA_ID_TENANT_ID**: Tenant ID di Entra ID (stesso di AZURE_TENANT_ID) +- **ENTRA_ID_DOMAIN_NAME**: Nome del dominio del tenant (es. contoso.onmicrosoft.com) +- **WEB_APP_ID**: Application ID della Web App principale +- **WEB_APP_SECRET**: Client secret della Web App principale +- **CMS_API_APP_ID**: Application ID della CMS API +- **PROPOSALS_API_APP_ID**: Application ID della Proposals API + +### 4. Verifica configurazione + +Per verificare che tutto sia configurato correttamente: + +```bash +# Test login con Service Principal +az login --service-principal \ + --username \ + --password \ + --tenant + +# Verifica permessi +az account show +az group list +``` + +### Documentazione Microsoft di riferimento + +- [**Autenticazione Service Principal**](https://learn.microsoft.com/en-us/dotnet/azure/sdk/authentication/local-development-service-principal): Guida completa per configurare Service Principal per applicazioni .NET +- [**Variabili d'ambiente Azure**](https://learn.microsoft.com/en-us/dotnet/azure/sdk/authentication/on-premises-apps#set-the-app-environment-variables): Come impostare le credenziali Azure nelle variabili d'ambiente +- [**GitHub Actions con Azure**](https://learn.microsoft.com/en-us/visualstudio/azure/azure-deployment-using-github-actions): Deploy su Azure Container Apps usando GitHub Actions +- [**Registrazione app Entra ID**](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app): Registrare applicazioni in Entra ID (Azure AD) +- [**Azure CLI Service Principal**](https://learn.microsoft.com/en-us/cli/azure/ad/sp): Documentazione completa sui comandi `az ad sp` +- [**Container Apps autenticazione**](https://learn.microsoft.com/en-us/azure/container-apps/authentication): Configurare l'autenticazione per Azure Container Apps + +## Richiesta di Modifica +L'utente ha richiesto di evitare i Dockerfile e utilizzare le funzionalità native di .NET per la containerizzazione. + +## Soluzione Finale con .NET Container Support + +### 1. Modifiche ai file .csproj + +#### KITT.Web.App.csproj +```xml + + + + net9.0 + enable + enable + + + true + kitt-webapp + latest + mcr.microsoft.com/dotnet/aspnet:9.0 + /app + + + + +``` + +#### KITT.Cms.Web.Api.csproj +```xml + + + + net9.0 + enable + enable + + + true + kitt-cms-api + latest + mcr.microsoft.com/dotnet/aspnet:9.0 + /app + + + + +``` + +#### KITT.Proposals.Web.Api.csproj +```xml + + + + net9.0 + enable + enable + + + true + kitt-proposals-api + latest + mcr.microsoft.com/dotnet/aspnet:9.0 + /app + + + + +``` + +### 2. GitHub Action aggiornata + +```yaml +name: Deploy to Azure + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +env: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + RESOURCE_GROUP: "kitt-rg" + LOCATION: "Italy North" + +jobs: + deploy-infrastructure: + runs-on: ubuntu-latest + outputs: + container-registry-name: ${{ steps.deploy.outputs.kitt_env_AZURE_CONTAINER_REGISTRY_NAME }} + container-registry-endpoint: ${{ steps.deploy.outputs.kitt_env_AZURE_CONTAINER_REGISTRY_ENDPOINT }} + container-registry-managed-identity-id: ${{ steps.deploy.outputs.kitt_env_AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} + container-apps-environment-id: ${{ steps.deploy.outputs.kitt_env_AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} + container-apps-environment-default-domain: ${{ steps.deploy.outputs.kitt_env_AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} + steps: + - uses: actions/checkout@v4 + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Deploy Bicep Infrastructure + id: deploy + uses: azure/arm-deploy@v1 + with: + subscriptionId: ${{ env.AZURE_SUBSCRIPTION_ID }} + resourceGroupName: ${{ env.RESOURCE_GROUP }} + template: ./infra/main.bicep + parameters: > + resourceGroupName=${{ env.RESOURCE_GROUP }} + location="${{ env.LOCATION }}" + deploymentMode: Incremental + + build-and-push-images: + needs: deploy-infrastructure + runs-on: ubuntu-latest + strategy: + matrix: + project: + - name: "kitt-webapp" + path: "./src/KITT.Web.App" + csproj: "KITT.Web.App.csproj" + - name: "kitt-cms-api" + path: "./src/KITT.Cms.Web.Api" + csproj: "KITT.Cms.Web.Api.csproj" + - name: "kitt-proposals-api" + path: "./src/KITT.Proposals.Web.Api" + csproj: "KITT.Proposals.Web.Api.csproj" + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Login to Azure Container Registry + run: | + az acr login --name ${{ needs.deploy-infrastructure.outputs.container-registry-name }} + + - name: Build and Push Container using .NET SDK + run: | + REGISTRY_ENDPOINT="${{ needs.deploy-infrastructure.outputs.container-registry-endpoint }}" + IMAGE_TAG="${REGISTRY_ENDPOINT}/${{ matrix.project.name }}:${{ github.sha }}" + + cd ${{ matrix.project.path }} + dotnet publish ${{ matrix.project.csproj }} \ + --os linux \ + --arch x64 \ + -p:PublishProfile=DefaultContainer \ + -p:ContainerImageName=${{ matrix.project.name }} \ + -p:ContainerImageTag=${{ github.sha }} \ + -p:ContainerRegistry=${REGISTRY_ENDPOINT} + + deploy-container-apps: + needs: [deploy-infrastructure, build-and-push-images] + runs-on: ubuntu-latest + strategy: + matrix: + app: + - name: "kitt-webapp" + bicep: "./infra/kitt-webapp/kitt-webapp.bicep" + port: "8080" + - name: "kitt-cms-api" + bicep: "./infra/kitt-cms-api/kitt-cms-api.bicep" + port: "8080" + - name: "kitt-proposals-api" + bicep: "./infra/kitt-proposals-api/kitt-proposals-api.bicep" + port: "8080" + + steps: + - uses: actions/checkout@v4 + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Deploy Container App + uses: azure/arm-deploy@v1 + with: + subscriptionId: ${{ env.AZURE_SUBSCRIPTION_ID }} + resourceGroupName: ${{ env.RESOURCE_GROUP }} + template: ${{ matrix.app.bicep }} + parameters: > + location="${{ env.LOCATION }}" + ${{ matrix.app.name }}_containerimage="${{ needs.deploy-infrastructure.outputs.container-registry-endpoint }}/${{ matrix.app.name }}:${{ github.sha }}" + ${{ matrix.app.name }}_containerport="${{ matrix.app.port }}" + kitt_env_outputs_azure_container_apps_environment_id="${{ needs.deploy-infrastructure.outputs.container-apps-environment-id }}" + kitt_env_outputs_azure_container_registry_endpoint="${{ needs.deploy-infrastructure.outputs.container-registry-endpoint }}" + kitt_env_outputs_azure_container_registry_managed_identity_id="${{ needs.deploy-infrastructure.outputs.container-registry-managed-identity-id }}" + kitt_env_outputs_azure_container_apps_environment_default_domain="${{ needs.deploy-infrastructure.outputs.container-apps-environment-default-domain }}" + kittdatabase_connectionstring="${{ secrets.KITT_DATABASE_CONNECTION_STRING }}" + entraidtenantid_value="${{ secrets.ENTRA_ID_TENANT_ID }}" + entraiddomainname_value="${{ secrets.ENTRA_ID_DOMAIN_NAME }}" + webappid_value="${{ secrets.WEB_APP_ID }}" + webappsecret_value="${{ secrets.WEB_APP_SECRET }}" + cmsapiappid_value="${{ secrets.CMS_API_APP_ID }}" + proposalsapiappid_value="${{ secrets.PROPOSALS_API_APP_ID }}" + deploymentMode: Incremental +``` + +### 3. Package NuGet da aggiungere +```xml + +``` + +### 4. Script PowerShell per test locale +```powershell +# build-containers.ps1 +Write-Host "Building KITT Web App container..." +dotnet publish src/KITT.Web.App/KITT.Web.App.csproj ` + --os linux --arch x64 ` + -p:PublishProfile=DefaultContainer ` + -p:ContainerImageName=kitt-webapp ` + -p:ContainerImageTag=local + +Write-Host "Building KITT CMS API container..." +dotnet publish src/KITT.Cms.Web.Api/KITT.Cms.Web.Api.csproj ` + --os linux --arch x64 ` + -p:PublishProfile=DefaultContainer ` + -p:ContainerImageName=kitt-cms-api ` + -p:ContainerImageTag=local + +Write-Host "Building KITT Proposals API container..." +dotnet publish src/KITT.Proposals.Web.Api/KITT.Proposals.Web.Api.csproj ` + --os linux --arch x64 ` + -p:PublishProfile=DefaultContainer ` + -p:ContainerImageName=kitt-proposals-api ` + -p:ContainerImageTag=local + +Write-Host "All containers built successfully!" +``` + +## Architettura dell'infrastruttura + +L'infrastruttura Azure è organizzata in moduli Bicep separati: + +### 1. `main.bicep` (Entry Point) +- **Scope**: Subscription-level deployment +- **Ruolo**: Crea il resource group e richiama il modulo `kitt-env` +- **Output**: Espone tutti gli output del modulo environment per gli altri deployment + +### 2. `kitt-env/kitt-env.bicep` (Shared Environment) +- **Azure Container Registry** (Basic SKU) con nome univoco +- **User-Assigned Managed Identity** per l'accesso al registry +- **Role Assignment** AcrPull per la managed identity +- **Log Analytics Workspace** per i log delle container apps +- **Container Apps Environment** con configurazione log analytics +- **Aspire Dashboard** component per observability + +### 3. `kitt-webapp/kitt-webapp.bicep` (Web Application) +- **External ingress** su porta 8080 +- **Secrets** per database e identità Entra ID +- **Service discovery** per CMS e Proposals API +- **Aspire telemetry** configurato + +### 4. `kitt-cms-api/kitt-cms-api.bicep` (CMS API) +- **Internal ingress** (non esposta esternamente) +- **Secrets** per database e identità CMS +- **Aspire telemetry** configurato + +### 5. `kitt-proposals-api/kitt-proposals-api.bicep` (Proposals API) +- **Internal ingress** (non esposta esternamente) +- **Secrets** per database e identità Proposals +- **Aspire telemetry** configurato + +## Configurazione .NET Container Support + +### Proprietà csproj necessarie +Aggiungi ai file `.csproj` delle applicazioni web: + +```xml + + + true + kitt-webapp + latest + mcr.microsoft.com/dotnet/aspnet:9.0 + /app + +``` + +### Package NuGet richiesto +```xml + +``` + +## Vantaggi della soluzione finale + +1. **No Dockerfile**: .NET genera automaticamente l'immagine container +2. **Ottimizzazione automatica**: .NET applica le migliori pratiche per le immagini +3. **Consistenza**: Stesso processo per tutti i progetti +4. **Manutenzione ridotta**: Non devi mantenere Dockerfile separati +5. **Sicurezza**: Usa immagini base Microsoft ottimizzate + +Il processo è completamente automatizzato: ogni push su main triggera la build dell'infrastruttura, la creazione delle immagini container e il deployment su Azure Container Apps. diff --git a/next-steps.md b/next-steps.md new file mode 100644 index 0000000..21e6517 --- /dev/null +++ b/next-steps.md @@ -0,0 +1,73 @@ +# Next Steps after `azd init` + +## Table of Contents + +1. [Next Steps](#next-steps) +2. [What was added](#what-was-added) +3. [Billing](#billing) +4. [Troubleshooting](#troubleshooting) + +## Next Steps + +### Provision infrastructure and deploy application code + +Run `azd up` to provision your infrastructure and deploy to Azure in one step (or run `azd provision` then `azd deploy` to accomplish the tasks separately). Visit the service endpoints listed to see your application up-and-running! + +To troubleshoot any issues, see [troubleshooting](#troubleshooting). + +### Configure CI/CD pipeline + +Run `azd pipeline config -e ` to configure the deployment pipeline to connect securely to Azure. An environment name is specified here to configure the pipeline with a different environment for isolation purposes. Run `azd env list` and `azd env set` to reselect the default environment after this step. + +- Deploying with `GitHub Actions`: Select `GitHub` when prompted for a provider. If your project lacks the `azure-dev.yml` file, accept the prompt to add it and proceed with pipeline configuration. + +- Deploying with `Azure DevOps Pipeline`: Select `Azure DevOps` when prompted for a provider. If your project lacks the `azure-dev.yml` file, accept the prompt to add it and proceed with pipeline configuration. + +## What was added + +### Infrastructure configuration + +To describe the infrastructure and application, an `azure.yaml` was added with the following directory structure: + +```yaml +- azure.yaml # azd project configuration +``` + +This file contains a single service, which references your project's App Host. When needed, `azd` generates the required infrastructure as code in memory and uses it. + +If you would like to see or modify the infrastructure that `azd` uses, run `azd infra gen` to generate it to disk. + +If you do this, some additional directories will be created: + +```yaml +- infra/ # Infrastructure as Code (bicep) files + - main.bicep # main deployment module + - resources.bicep # resources shared across your application's services +``` + +In addition, for each project resource referenced by your app host, a `containerApp.tmpl.yaml` file will be created in a directory named `manifests` next the project file. This file contains the infrastructure as code for running the project on Azure Container Apps. + +*Note*: Once you have generated your infrastructure to disk, those files are the source of truth for azd. Any changes made to `azure.yaml` or your App Host will not be reflected in the infrastructure until you regenerate it with `azd infra gen` again. It will prompt you before overwriting files. You can pass `--force` to force `azd infra gen` to overwrite the files without prompting. + +## Billing + +Visit the *Cost Management + Billing* page in Azure Portal to track current spend. For more information about how you're billed, and how you can monitor the costs incurred in your Azure subscriptions, visit [billing overview](https://learn.microsoft.com/azure/developer/intro/azure-developer-billing). + +## Troubleshooting + +Q: I visited the service endpoint listed, and I'm seeing a blank page, a generic welcome page, or an error page. + +A: Your service may have failed to start, or it may be missing some configuration settings. To investigate further: + +1. Run `azd show`. Click on the link under "View in Azure Portal" to open the resource group in Azure Portal. +2. Navigate to the specific Container App service that is failing to deploy. +3. Click on the failing revision under "Revisions with Issues". +4. Review "Status details" for more information about the type of failure. +5. Observe the log outputs from Console log stream and System log stream to identify any errors. +6. If logs are written to disk, use *Console* in the navigation to connect to a shell within the running container. + +For more troubleshooting information, visit [Container Apps troubleshooting](https://learn.microsoft.com/azure/container-apps/troubleshooting). + +### Additional information + +For additional information about setting up your `azd` project, visit our official [docs](https://learn.microsoft.com/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-convert). diff --git a/src/KITT.AppHost/DistributedApplicationBuilderExtensions.cs b/src/KITT.AppHost/DistributedApplicationBuilderExtensions.cs new file mode 100644 index 0000000..173940e --- /dev/null +++ b/src/KITT.AppHost/DistributedApplicationBuilderExtensions.cs @@ -0,0 +1,26 @@ +using KITT.Services; +using Microsoft.Extensions.Hosting; + +namespace KITT.AppHost; + +public static class DistributedApplicationBuilderExtensions +{ + public static IResourceBuilder AddKittDatabase(this IDistributedApplicationBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + if (!builder.ExecutionContext.IsPublishMode) + { + var kittSql = builder.AddSqlServer(ServiceNames.Sql) + .WithContainerName("sqlserver-local") + .WithLifetime(ContainerLifetime.Persistent) + .WithDataVolume("kitt-data"); + + return kittSql.AddDatabase(ServiceNames.Database, databaseName: "KITT"); + } + else + { + return builder.AddConnectionString(ServiceNames.Database); + } + } +} diff --git a/src/KITT.AppHost/KITT.AppHost.csproj b/src/KITT.AppHost/KITT.AppHost.csproj new file mode 100644 index 0000000..38ad8e2 --- /dev/null +++ b/src/KITT.AppHost/KITT.AppHost.csproj @@ -0,0 +1,30 @@ + + + + + + Exe + net9.0 + enable + enable + 9088a778-e43f-44ea-8c00-27313c124862 + + + + + + + + + + + + + + + + + + + + diff --git a/src/KITT.AppHost/Program.cs b/src/KITT.AppHost/Program.cs new file mode 100644 index 0000000..b37dadc --- /dev/null +++ b/src/KITT.AppHost/Program.cs @@ -0,0 +1,65 @@ +using Microsoft.Extensions.Hosting; +using KITT.Services; +using KITT.AppHost; + +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddAzureContainerAppEnvironment("kitt-env"); + +#region EntraID configuration +var tenantId = builder.AddParameter("EntraIdTenantId", secret: true); +var domainName = builder.AddParameter("EntraIdDomainName", secret: true); + +var cmsApiAppId = builder.AddParameter("CmsApiAppId", secret: true); + +var proposalsApiAppId = builder.AddParameter("ProposalsApiAppId", secret: true); + +var webAppId = builder.AddParameter("WebAppId", secret: true); +var webAppSecret = builder.AddParameter("WebAppSecret", secret: true); +#endregion + +#region Database +var kittDb = builder.AddKittDatabase(); +#endregion + +#region CMS +var cmsApi = builder.AddProject(ServiceNames.CmsApi) + .WithReference(kittDb) + .WaitFor(kittDb) + .WithEnvironment("Identity__TenantId", tenantId) + .WithEnvironment("Identity__Cms__AppId", cmsApiAppId); +#endregion + +#region Proposals +var proposalsApi = builder.AddProject(ServiceNames.ProposalsApi) + .WithReference(kittDb) + .WaitFor(kittDb) + .WithEnvironment("Identity__TenantId", tenantId) + .WithEnvironment("Identity__Proposals__AppId", proposalsApiAppId); +#endregion + +var webApp = builder.AddProject(ServiceNames.WebApp) + .WithReference(cmsApi) + .WaitFor(cmsApi) + .WithReference(proposalsApi) + .WaitFor(proposalsApi) + .WithReference(kittDb) + .WaitFor(kittDb) + .WithEnvironment("Identity__TenantId", tenantId) + .WithEnvironment("Identity__DomainName", domainName) + .WithEnvironment("Identity__WebApp__AppId", webAppId) + .WithEnvironment("Identity__WebApp__AppSecret", webAppSecret) + .WithEnvironment("Identity__Cms__AppId", cmsApiAppId) + .WithEnvironment("Identity__Proposals__AppId", proposalsApiAppId) + .WithExternalHttpEndpoints(); + + +if (!builder.ExecutionContext.IsPublishMode) +{ + var seeder = builder.AddProject(ServiceNames.Seeder) + .WithReference(kittDb) + .WaitFor(kittDb); +} + + +builder.Build().Run(); diff --git a/src/KITT.AppHost/Properties/launchSettings.json b/src/KITT.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..722327b --- /dev/null +++ b/src/KITT.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17294;http://localhost:15096", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21093", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22230" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15096", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19005", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20228" + } + } + } +} diff --git a/src/KITT.AppHost/appsettings.json b/src/KITT.AppHost/appsettings.json new file mode 100644 index 0000000..4b3b0a8 --- /dev/null +++ b/src/KITT.AppHost/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + }, + "Azure": { + "SubscriptionId": "", + "ResourceGroup": "", + "Location": "", + "CredentialSource": "AzureDeveloperCli" + } +} diff --git a/src/KITT.Bot.Functions/KITT.Bot.Functions.csproj b/src/KITT.Bot.Functions/KITT.Bot.Functions.csproj index 3db89f5..7f76076 100644 --- a/src/KITT.Bot.Functions/KITT.Bot.Functions.csproj +++ b/src/KITT.Bot.Functions/KITT.Bot.Functions.csproj @@ -4,8 +4,10 @@ v4 - - + + + + diff --git a/src/LemonBot.Web/Endpoints/Services/SettingsEndpointsServices.cs b/src/KITT.Cms.Web.Api/Endpoints/Services/SettingsEndpointsServices.cs similarity index 93% rename from src/LemonBot.Web/Endpoints/Services/SettingsEndpointsServices.cs rename to src/KITT.Cms.Web.Api/Endpoints/Services/SettingsEndpointsServices.cs index 1ee911d..f9076b4 100644 --- a/src/LemonBot.Web/Endpoints/Services/SettingsEndpointsServices.cs +++ b/src/KITT.Cms.Web.Api/Endpoints/Services/SettingsEndpointsServices.cs @@ -1,9 +1,9 @@ -using KITT.Core.Commands; +using KITT.Cms.Web.Models.Settings; +using KITT.Core.Commands; using KITT.Core.ReadModels; -using KITT.Web.Models.Settings; using Microsoft.EntityFrameworkCore; -namespace LemonBot.Web.Endpoints.Services; +namespace KITT.Cms.Web.Api.Endpoints.Services; public class SettingsEndpointsServices { diff --git a/src/LemonBot.Web/Endpoints/Services/StreamingsEndpointsServices.cs b/src/KITT.Cms.Web.Api/Endpoints/Services/StreamingsEndpointsServices.cs similarity index 86% rename from src/LemonBot.Web/Endpoints/Services/StreamingsEndpointsServices.cs rename to src/KITT.Cms.Web.Api/Endpoints/Services/StreamingsEndpointsServices.cs index 30dca69..3c02c7d 100644 --- a/src/LemonBot.Web/Endpoints/Services/StreamingsEndpointsServices.cs +++ b/src/KITT.Cms.Web.Api/Endpoints/Services/StreamingsEndpointsServices.cs @@ -1,9 +1,9 @@ -using KITT.Core.Commands; +using KITT.Cms.Web.Models.Streamings; +using KITT.Core.Commands; using KITT.Core.ReadModels; -using KITT.Web.Models.Streamings; using Microsoft.EntityFrameworkCore; -namespace LemonBot.Web.Endpoints.Services; +namespace KITT.Cms.Web.Api.Endpoints.Services; public class StreamingsEndpointsServices { @@ -81,7 +81,7 @@ public Task ImportStreamingAsync(ImportStreamingModel model, string userId throw new InvalidOperationException("No settings configured"); } - var seo = new KITT.Core.Models.Content.SeoData + var seo = new Core.Models.Content.SeoData { Title = model.Seo.Title, Description = model.Seo.Description, @@ -113,7 +113,7 @@ public Task ScheduleStreamingAsync(ScheduleStreamingModel model, string us throw new InvalidOperationException("No settings configured"); } - var seo = new KITT.Core.Models.Content.SeoData + var seo = new Core.Models.Content.SeoData { Title = model.Seo.Title, Description = model.Seo.Description, @@ -135,7 +135,7 @@ public Task ScheduleStreamingAsync(ScheduleStreamingModel model, string us public Task UpdateStreamingAsync(Guid streamingId, StreamingDetailModel model) { - var seo = new KITT.Core.Models.Content.SeoData + var seo = new Core.Models.Content.SeoData { Title = model.Seo.Title, Description = model.Seo.Description, @@ -155,4 +155,16 @@ public Task UpdateStreamingAsync(Guid streamingId, StreamingDetailModel model) } public Task DeleteStreamingAsync(Guid streamingId) => Commands.DeleteStreamingAsync(streamingId); + + public async Task GetStreamingStatsAsync(string userId) + { + var streamingsQuery = Database.Streamings + .ByUserId(userId) + .OrderedBySchedule(); + + var deliveredStreamingsNumber = await streamingsQuery.DeliveredOnly().CountAsync(); + var scheduledStreamingsNumber = await streamingsQuery.Scheduled().CountAsync(); + + return new StreamingStatsModel(deliveredStreamingsNumber, scheduledStreamingsNumber); + } } diff --git a/src/LemonBot.Web/Endpoints/SettingsEndpoints.cs b/src/KITT.Cms.Web.Api/Endpoints/SettingsEndpoints.cs similarity index 87% rename from src/LemonBot.Web/Endpoints/SettingsEndpoints.cs rename to src/KITT.Cms.Web.Api/Endpoints/SettingsEndpoints.cs index 925ceeb..dc049ef 100644 --- a/src/LemonBot.Web/Endpoints/SettingsEndpoints.cs +++ b/src/KITT.Cms.Web.Api/Endpoints/SettingsEndpoints.cs @@ -1,17 +1,18 @@ -using KITT.Web.Models.Settings; -using LemonBot.Web.Endpoints.Services; +using KITT.Cms.Web.Api.Endpoints.Services; +using KITT.Cms.Web.Models.Settings; +using KITT.Web.Shared.Security; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; -namespace LemonBot.Web.Endpoints; +namespace KITT.Cms.Web.Api.Endpoints; public static class SettingsEndpoints { public static IEndpointRouteBuilder MapSettingsEndpoints(this IEndpointRouteBuilder builder) { var settingsGroup = builder - .MapGroup("api/console/settings") + .MapGroup("api/settings") .RequireAuthorization() .WithParameterValidation(); @@ -34,8 +35,8 @@ public static IEndpointRouteBuilder MapSettingsEndpoints(this IEndpointRouteBuil } private static async Task> GetAllSettings( - SettingsEndpointsServices services, - ClaimsPrincipal user) + SettingsEndpointsServices services, + ClaimsPrincipal user) { var model = await services.GetAllSettingsAsync(user.GetUserId()); return TypedResults.Ok(model); diff --git a/src/LemonBot.Web/Endpoints/StreamingsEndpoints.cs b/src/KITT.Cms.Web.Api/Endpoints/StreamingsEndpoints.cs similarity index 85% rename from src/LemonBot.Web/Endpoints/StreamingsEndpoints.cs rename to src/KITT.Cms.Web.Api/Endpoints/StreamingsEndpoints.cs index 06bd396..98a384e 100644 --- a/src/LemonBot.Web/Endpoints/StreamingsEndpoints.cs +++ b/src/KITT.Cms.Web.Api/Endpoints/StreamingsEndpoints.cs @@ -1,17 +1,18 @@ -using KITT.Web.Models.Streamings; -using LemonBot.Web.Endpoints.Services; +using KITT.Cms.Web.Api.Endpoints.Services; +using KITT.Cms.Web.Models.Streamings; +using KITT.Web.Shared.Security; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; -namespace LemonBot.Web.Endpoints; +namespace KITT.Cms.Web.Api.Endpoints; public static class StreamingsEndpoints { public static IEndpointRouteBuilder MapStreamingsEndpoints(this IEndpointRouteBuilder builder) { var streamingsGroup = builder - .MapGroup("api/console/streamings") + .MapGroup("api/streamings") .RequireAuthorization() .WithParameterValidation(); @@ -45,6 +46,11 @@ public static IEndpointRouteBuilder MapStreamingsEndpoints(this IEndpointRouteBu .WithName(nameof(DeleteStreaming)) .WithOpenApi(); + streamingsGroup + .MapGet("stats", GetStreamingStats) + .WithName(nameof(GetStreamingStats)) + .WithOpenApi(); + return builder; } @@ -131,4 +137,14 @@ private static async Task> GetStreamingStats( + StreamingsEndpointsServices services, + ClaimsPrincipal user) + { + var userId = user.GetUserId(); + var streamingStats = await services.GetStreamingStatsAsync(userId); + + return TypedResults.Ok(streamingStats); + } } diff --git a/src/KITT.Cms.Web.Api/KITT.Cms.Web.Api.csproj b/src/KITT.Cms.Web.Api/KITT.Cms.Web.Api.csproj new file mode 100644 index 0000000..b2550b7 --- /dev/null +++ b/src/KITT.Cms.Web.Api/KITT.Cms.Web.Api.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + diff --git a/src/KITT.Cms.Web.Api/KITT.Cms.Web.Api.http b/src/KITT.Cms.Web.Api/KITT.Cms.Web.Api.http new file mode 100644 index 0000000..d683e9e --- /dev/null +++ b/src/KITT.Cms.Web.Api/KITT.Cms.Web.Api.http @@ -0,0 +1,6 @@ +@KITT.Cms.Web.Api_HostAddress = http://localhost:5195 + +GET {{KITT.Cms.Web.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/LemonBot.Web/LocalMessageBus.cs b/src/KITT.Cms.Web.Api/LocalMessageBus.cs similarity index 88% rename from src/LemonBot.Web/LocalMessageBus.cs rename to src/KITT.Cms.Web.Api/LocalMessageBus.cs index 62b2521..487f83f 100644 --- a/src/LemonBot.Web/LocalMessageBus.cs +++ b/src/KITT.Cms.Web.Api/LocalMessageBus.cs @@ -1,6 +1,6 @@ using KITT.Telegram.Messages; -namespace LemonBot.Web; +namespace KITT.Cms.Web.Api; internal class LocalMessageBus : IMessageBus { diff --git a/src/KITT.Cms.Web.Api/Program.cs b/src/KITT.Cms.Web.Api/Program.cs new file mode 100644 index 0000000..956723c --- /dev/null +++ b/src/KITT.Cms.Web.Api/Program.cs @@ -0,0 +1,71 @@ +using KITT.Cms.Web.Api.Endpoints; +using KITT.Core.Persistence; +using KITT.Core.DependencyInjection; +using KITT.Cms.Web.Api.Endpoints.Services; +using KITT.Cms.Web.Api; +using KITT.Telegram.Messages; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using KITT.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddSqlServerDbContext(ServiceNames.Database); + +builder.Services.AddAuthentication() + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => + { + options.Authority = $"https://sts.windows.net/{builder.Configuration["Identity:TenantId"]}/"; + options.Audience = $"api://{builder.Configuration["Identity:Cms:AppId"]}"; + }); + +builder.Services.AddAuthorization(); + +builder.Services.AddOpenApi(); + +builder.Services.AddKittCore(); + +builder.Services + .AddScoped() + .AddScoped(); + +builder.Services.AddProblemDetails(); + +builder.Services.Configure( + options => options.ConnectionString = builder.Configuration["QueueClientOptions:ConnectionString"]!); + +//TOFIX disable the message bus for now, it will be fixed in future to use the correct service +builder.Services.AddSingleton(); +//if (builder.Environment.IsDevelopment()) +//{ +// builder.Services.AddSingleton(); +//} +//else +//{ +// builder.Services.AddSingleton(); +//} + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseExceptionHandler(); +app.UseStatusCodePages(); + +app.MapDefaultEndpoints(); + +app + .MapStreamingsEndpoints() + .MapSettingsEndpoints(); + +app.Run(); + +#region Testing workaround +public partial class Program { } // This is a workaround for testing purposes, to allow the test project to reference the Program class. +#endregion \ No newline at end of file diff --git a/src/KITT.Cms.Web.Api/Properties/launchSettings.json b/src/KITT.Cms.Web.Api/Properties/launchSettings.json new file mode 100644 index 0000000..94f3eb4 --- /dev/null +++ b/src/KITT.Cms.Web.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:39273", + "sslPort": 44308 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5195", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7280;http://localhost:5195", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/KITT.Cms.Web.Api/appsettings.json b/src/KITT.Cms.Web.Api/appsettings.json new file mode 100644 index 0000000..37e04d8 --- /dev/null +++ b/src/KITT.Cms.Web.Api/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "QueueClientOptions": { + "ConnectionString": "UseDevelopmentStorage=true" + } +} diff --git a/src/KITT.Cms.Web.App/Clients/ClientsServiceCollectionExtensions.cs b/src/KITT.Cms.Web.App/Clients/ClientsServiceCollectionExtensions.cs new file mode 100644 index 0000000..36a6f68 --- /dev/null +++ b/src/KITT.Cms.Web.App/Clients/ClientsServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using KITT.Cms.Web.App.Clients.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace KITT.Cms.Web.App.Clients; + +public static class ClientsServiceCollectionExtensions +{ + public static IServiceCollection AddCmsClients( + this IServiceCollection services, + string apiBaseUrl) + { + services + .AddHttpClient(client => client.BaseAddress = new(apiBaseUrl)); + + return services; + } +} diff --git a/src/KITT.Web.App/Clients/Http/StreamingsHttpClient.cs b/src/KITT.Cms.Web.App/Clients/Http/StreamingsHttpClient.cs similarity index 85% rename from src/KITT.Web.App/Clients/Http/StreamingsHttpClient.cs rename to src/KITT.Cms.Web.App/Clients/Http/StreamingsHttpClient.cs index 5e00785..20600c5 100644 --- a/src/KITT.Web.App/Clients/Http/StreamingsHttpClient.cs +++ b/src/KITT.Cms.Web.App/Clients/Http/StreamingsHttpClient.cs @@ -1,13 +1,13 @@ -using KITT.Web.Models.Streamings; +using KITT.Cms.Web.Models.Streamings; using System.Net.Http.Json; -namespace KITT.Web.App.Clients.Http; +namespace KITT.Cms.Web.App.Clients.Http; public class StreamingsHttpClient : IStreamingsClient { public HttpClient Client { get; } - public string ApiResource { get; } = "/api/console/streamings"; + public string ApiResource { get; } = "/api/cms/streamings"; public StreamingsHttpClient(HttpClient client) { @@ -75,4 +75,10 @@ public async Task ImportStreamingAsync(ImportStreamingModel model) throw new ApplicationException("Error importing streaming"); } } + + public async Task GetStreamingStatsAsync() + { + var streamingStats = await Client.GetFromJsonAsync($"{ApiResource}/stats"); + return streamingStats ?? new(0, 0); + } } diff --git a/src/KITT.Web.App/Clients/IStreamingsClient.cs b/src/KITT.Cms.Web.App/Clients/IStreamingsClient.cs similarity index 76% rename from src/KITT.Web.App/Clients/IStreamingsClient.cs rename to src/KITT.Cms.Web.App/Clients/IStreamingsClient.cs index e5ea5ea..f31f9d1 100644 --- a/src/KITT.Web.App/Clients/IStreamingsClient.cs +++ b/src/KITT.Cms.Web.App/Clients/IStreamingsClient.cs @@ -1,6 +1,6 @@ -using KITT.Web.Models.Streamings; +using KITT.Cms.Web.Models.Streamings; -namespace KITT.Web.App.Clients; +namespace KITT.Cms.Web.App.Clients; public interface IStreamingsClient { @@ -15,4 +15,6 @@ public interface IStreamingsClient Task DeleteStreamingAsync(Guid streamingId); Task ImportStreamingAsync(ImportStreamingModel model); + + Task GetStreamingStatsAsync(); } diff --git a/src/KITT.Web.App/Components/ContentForm.razor b/src/KITT.Cms.Web.App/Components/ContentForm.razor similarity index 89% rename from src/KITT.Web.App/Components/ContentForm.razor rename to src/KITT.Cms.Web.App/Components/ContentForm.razor index a68b2f9..e7bf183 100644 --- a/src/KITT.Web.App/Components/ContentForm.razor +++ b/src/KITT.Cms.Web.App/Components/ContentForm.razor @@ -1,6 +1,6 @@ -@typeparam TContent where TContent : KITT.Web.App.Model.ContentViewModel +@typeparam TContent where TContent : KITT.Cms.Web.Models.ContentViewModel -@inject IStringLocalizer Localizer +@inject IStringLocalizer Localizer @@ -53,12 +53,12 @@ Loading="saving" OnClick="SubmitAsync" Id="saveContentButton"> - @Localizer[nameof(UI.Resources.Common.Save)] + @Localizer[nameof(KITT.Web.App.UI.Resources.Common.Save)] - @Localizer[nameof(UI.Resources.Common.Cancel)] + @Localizer[nameof(KITT.Web.App.UI.Resources.Common.Cancel)] } diff --git a/src/KITT.Web.App/Components/ContentForm.razor.cs b/src/KITT.Cms.Web.App/Components/ContentForm.razor.cs similarity index 78% rename from src/KITT.Web.App/Components/ContentForm.razor.cs rename to src/KITT.Cms.Web.App/Components/ContentForm.razor.cs index 0220685..32c1329 100644 --- a/src/KITT.Web.App/Components/ContentForm.razor.cs +++ b/src/KITT.Cms.Web.App/Components/ContentForm.razor.cs @@ -1,9 +1,9 @@ -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; -namespace KITT.Web.App.Components; +namespace KITT.Cms.Web.App.Components; public partial class ContentForm { @@ -62,13 +62,13 @@ private async Task HandleNavigationAsync(LocationChangingContext context) { if ((editContext?.IsModified() ?? false) && !saving) { - string confirmText = Localizer[nameof(UI.Resources.Common.UnsavedChangesDialogContent)]; + string confirmText = Localizer[nameof(KITT.Web.App.UI.Resources.Common.UnsavedChangesDialogContent)]; var confirmDialog = await DialogService.ShowConfirmationAsync( confirmText, - primaryText: Localizer[nameof(UI.Resources.Common.Confirm)], - secondaryText: Localizer[nameof(UI.Resources.Common.Cancel)], - title: Localizer[nameof(UI.Resources.Common.UnsavedChangesDialogTitle)]); + primaryText: Localizer[nameof(KITT.Web.App.UI.Resources.Common.Confirm)], + secondaryText: Localizer[nameof(KITT.Web.App.UI.Resources.Common.Cancel)], + title: Localizer[nameof(KITT.Web.App.UI.Resources.Common.UnsavedChangesDialogTitle)]); var confirmResult = await confirmDialog.Result; @@ -79,3 +79,4 @@ private async Task HandleNavigationAsync(LocationChangingContext context) } } } + diff --git a/src/KITT.Web.App/Components/ScheduleForm.razor b/src/KITT.Cms.Web.App/Components/ScheduleForm.razor similarity index 88% rename from src/KITT.Web.App/Components/ScheduleForm.razor rename to src/KITT.Cms.Web.App/Components/ScheduleForm.razor index 61a57ae..76741b0 100644 --- a/src/KITT.Web.App/Components/ScheduleForm.razor +++ b/src/KITT.Cms.Web.App/Components/ScheduleForm.razor @@ -1,7 +1,6 @@ -@inject IStringLocalizer CommonLocalizer -@inject IStringLocalizer Localizer +@inject IStringLocalizer Localizer - + Id="streamingTitle" /> @@ -26,7 +25,7 @@ Appearance="FluentInputAppearance.Filled" Style="width:100%;" Required="true" - Id="streamingSlug"/> + Id="streamingSlug" /> @@ -49,7 +48,7 @@ Appearance="FluentInputAppearance.Filled" Style="width:100%;" Required="true" - Id="streamingScheduleDate"/> + Id="streamingScheduleDate" /> @@ -58,8 +57,8 @@ Appearance="FluentInputAppearance.Filled" Style="width:100%;" Required="true" - Id="streamingStartTime"/> - + Id="streamingStartTime" /> + + Id="streamingEndTime" /> @@ -79,7 +78,7 @@ Appearance="FluentInputAppearance.Filled" Rows="10" Style="width:100%;" - Id="streamingAbstract"/> + Id="streamingAbstract" /> diff --git a/src/KITT.Web.App/Components/ScheduleForm.razor.cs b/src/KITT.Cms.Web.App/Components/ScheduleForm.razor.cs similarity index 94% rename from src/KITT.Web.App/Components/ScheduleForm.razor.cs rename to src/KITT.Cms.Web.App/Components/ScheduleForm.razor.cs index 950f0e7..6804a46 100644 --- a/src/KITT.Web.App/Components/ScheduleForm.razor.cs +++ b/src/KITT.Cms.Web.App/Components/ScheduleForm.razor.cs @@ -1,8 +1,8 @@ -using KITT.Web.App.Model; +using KITT.Cms.Web.Models; using Microsoft.AspNetCore.Components; using System.ComponentModel.DataAnnotations; -namespace KITT.Web.App.Components; +namespace KITT.Cms.Web.App.Components; public partial class ScheduleForm { @@ -46,3 +46,4 @@ public class ViewModel : ContentViewModel public string? StreamingAbstract { get; set; } } } + diff --git a/src/KITT.Cms.Web.App/Components/StreamingStats.razor b/src/KITT.Cms.Web.App/Components/StreamingStats.razor new file mode 100644 index 0000000..b59fca4 --- /dev/null +++ b/src/KITT.Cms.Web.App/Components/StreamingStats.razor @@ -0,0 +1,43 @@ +@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false)) + +@inject KITT.Cms.Web.App.Clients.IStreamingsClient Client +@inject IStringLocalizer Localizer + + + + + + @model.NumberOfStreamingsScheduled + @Localizer[nameof(Resources.Components.StreamingStats.LivesScheduledLabel)] + + + + @model.NumberOfStreamingsDelivered + @Localizer[nameof(Resources.Components.StreamingStats.LivesDeliveredLabel)] + + + + @Localizer[nameof(Resources.Components.StreamingStats.ViewStreamingsButtonText)] + + + + + +@code { + private StreamingStatsModel model = default!; + + private bool loading = true; + + protected override async Task OnInitializedAsync() + { + try + { + model = await Client.GetStreamingStatsAsync(); + } + finally + { + loading = false; + } + } +} diff --git a/src/KITT.Cms.Web.App/KITT.Cms.Web.App.csproj b/src/KITT.Cms.Web.App/KITT.Cms.Web.App.csproj new file mode 100644 index 0000000..37fec1e --- /dev/null +++ b/src/KITT.Cms.Web.App/KITT.Cms.Web.App.csproj @@ -0,0 +1,89 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + True + True + ScheduleForm.resx + + + True + True + StreamingStats.resx + + + True + True + Import.resx + + + True + True + Index.resx + + + True + True + Schedule.resx + + + True + True + StreamingDetail.resx + + + + + + PublicResXFileCodeGenerator + + + ResXFileCodeGenerator + ScheduleForm.Designer.cs + + + ResXFileCodeGenerator + + + ResXFileCodeGenerator + StreamingStats.Designer.cs + + + ResXFileCodeGenerator + Import.Designer.cs + + + ResXFileCodeGenerator + Index.Designer.cs + + + ResXFileCodeGenerator + Schedule.Designer.cs + + + ResXFileCodeGenerator + StreamingDetail.Designer.cs + + + + diff --git a/src/KITT.Web.App/Pages/Streamings/Import.razor b/src/KITT.Cms.Web.App/Pages/Streamings/Import.razor similarity index 91% rename from src/KITT.Web.App/Pages/Streamings/Import.razor rename to src/KITT.Cms.Web.App/Pages/Streamings/Import.razor index ef4beac..c7cf0f7 100644 --- a/src/KITT.Web.App/Pages/Streamings/Import.razor +++ b/src/KITT.Cms.Web.App/Pages/Streamings/Import.razor @@ -1,10 +1,8 @@ -@page "/streamings/import" +@page "/streamings/import" @inject IStringLocalizer CommonLocalizer @inject IStringLocalizer Localizer -@attribute [Authorize] - KITT - @Localizer[nameof(Resources.Pages.Streamings.Import.PageTitle)] @@ -29,7 +27,7 @@ @bind-Value="streaming.Title" Appearance="FluentInputAppearance.Filled" Style="width:100%;" - Required="true"/> + Required="true" /> @@ -37,8 +35,8 @@ @bind-Value="streaming.Slug" Appearance="FluentInputAppearance.Filled" Style="width:100%;" - Required="true"/> - + Required="true" /> + @@ -57,7 +55,7 @@ + Style="width:100%;" /> @@ -67,7 +65,7 @@ @bind-Value="streaming.ScheduleDate" Appearance="FluentInputAppearance.Filled" Style="width:100%;" - Required="true"/> + Required="true" /> @@ -75,7 +73,7 @@ @bind-Value="streaming.StartingTime" Appearance="FluentInputAppearance.Filled" Style="width:100%;" - Required="true"/> + Required="true" /> @@ -83,7 +81,7 @@ @bind-Value="streaming.EndingTime" Appearance="FluentInputAppearance.Filled" Style="width:100%;" - Required="true"/> + Required="true" /> @@ -94,7 +92,7 @@ @bind-Value="streaming.StreamingAbstract" Appearance="FluentInputAppearance.Filled" Rows="10" - Style="width:100%;"/> + Style="width:100%;" /> diff --git a/src/KITT.Web.App/Pages/Streamings/Import.razor.cs b/src/KITT.Cms.Web.App/Pages/Streamings/Import.razor.cs similarity index 95% rename from src/KITT.Web.App/Pages/Streamings/Import.razor.cs rename to src/KITT.Cms.Web.App/Pages/Streamings/Import.razor.cs index 36f3199..70faf10 100644 --- a/src/KITT.Web.App/Pages/Streamings/Import.razor.cs +++ b/src/KITT.Cms.Web.App/Pages/Streamings/Import.razor.cs @@ -1,12 +1,12 @@ -using KITT.Web.App.Clients; -using KITT.Web.App.Model; +using KITT.Cms.Web.App.Clients; +using KITT.Cms.Web.Models; +using KITT.Cms.Web.Models.Streamings; using KITT.Web.App.UI; -using KITT.Web.Models.Streamings; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; using System.ComponentModel.DataAnnotations; -namespace KITT.Web.App.Pages.Streamings; +namespace KITT.Cms.Web.App.Pages.Streamings; public partial class Import { @@ -102,4 +102,4 @@ public ImportStreamingModel ToApiModel() }; } } -} \ No newline at end of file +} diff --git a/src/KITT.Web.App/Pages/Streamings/Index.razor b/src/KITT.Cms.Web.App/Pages/Streamings/Index.razor similarity index 91% rename from src/KITT.Web.App/Pages/Streamings/Index.razor rename to src/KITT.Cms.Web.App/Pages/Streamings/Index.razor index c328d3a..dcce14f 100644 --- a/src/KITT.Web.App/Pages/Streamings/Index.razor +++ b/src/KITT.Cms.Web.App/Pages/Streamings/Index.razor @@ -3,8 +3,6 @@ @inject IStringLocalizer CommonLocalizer @inject IStringLocalizer Localizer -@attribute [Authorize] - KITT - @Localizer[nameof(Resources.Pages.Streamings.Index.PageTitle)] @@ -88,13 +86,19 @@ - + @streaming.StartingTime.ToString("HH\\:mm") - @streaming.EndingTime.ToString("HH\\:mm") - + @streaming.HostingChannelUrl - + @if (string.IsNullOrWhiteSpace(streaming.YouTubeVideoUrl)) { - @@ -104,7 +108,8 @@ @streaming.YouTubeVideoUrl } - + diff --git a/src/KITT.Web.App/Pages/Streamings/Index.razor.cs b/src/KITT.Cms.Web.App/Pages/Streamings/Index.razor.cs similarity index 92% rename from src/KITT.Web.App/Pages/Streamings/Index.razor.cs rename to src/KITT.Cms.Web.App/Pages/Streamings/Index.razor.cs index 93dae95..0fa0a96 100644 --- a/src/KITT.Web.App/Pages/Streamings/Index.razor.cs +++ b/src/KITT.Cms.Web.App/Pages/Streamings/Index.razor.cs @@ -1,9 +1,9 @@ -using KITT.Web.App.Clients; -using KITT.Web.Models.Streamings; +using KITT.Cms.Web.App.Clients; +using KITT.Cms.Web.Models.Streamings; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; -namespace KITT.Web.App.Pages.Streamings; +namespace KITT.Cms.Web.App.Pages.Streamings; public partial class Index { @@ -86,8 +86,8 @@ private async Task DeleteStreaming(StreamingsListModel.StreamingListItemModel st var confirm = await DialogService.ShowConfirmationAsync( confirmText, - primaryText: CommonLocalizer[nameof(UI.Resources.Common.Confirm)], - secondaryText: CommonLocalizer[(nameof(UI.Resources.Common.Cancel))], + primaryText: CommonLocalizer[nameof(KITT.Web.App.UI.Resources.Common.Confirm)], + secondaryText: CommonLocalizer[(nameof(KITT.Web.App.UI.Resources.Common.Cancel))], title: Localizer[nameof(Resources.Pages.Streamings.Index.DeleteStreamingConfirmTitle), streamingTitle]); var result = await confirm.Result; @@ -117,3 +117,4 @@ private async Task ClearSearchAsync() await LoadStreamingsAsync(query); } } + diff --git a/src/KITT.Web.App/Pages/Streamings/Schedule.razor b/src/KITT.Cms.Web.App/Pages/Streamings/Schedule.razor similarity index 96% rename from src/KITT.Web.App/Pages/Streamings/Schedule.razor rename to src/KITT.Cms.Web.App/Pages/Streamings/Schedule.razor index cb6549e..e415870 100644 --- a/src/KITT.Web.App/Pages/Streamings/Schedule.razor +++ b/src/KITT.Cms.Web.App/Pages/Streamings/Schedule.razor @@ -3,8 +3,6 @@ @inject IStringLocalizer CommonLocalizer @inject IStringLocalizer Localizer -@attribute [Authorize] - KITT - @Localizer[nameof(Resources.Pages.Streamings.Schedule.PageTitle)] diff --git a/src/KITT.Web.App/Pages/Streamings/Schedule.razor.cs b/src/KITT.Cms.Web.App/Pages/Streamings/Schedule.razor.cs similarity index 93% rename from src/KITT.Web.App/Pages/Streamings/Schedule.razor.cs rename to src/KITT.Cms.Web.App/Pages/Streamings/Schedule.razor.cs index 584645b..54a9c24 100644 --- a/src/KITT.Web.App/Pages/Streamings/Schedule.razor.cs +++ b/src/KITT.Cms.Web.App/Pages/Streamings/Schedule.razor.cs @@ -1,11 +1,11 @@ -using KITT.Web.App.Clients; -using KITT.Web.App.Components; +using KITT.Cms.Web.App.Clients; +using KITT.Cms.Web.App.Components; +using KITT.Cms.Web.Models.Streamings; using KITT.Web.App.UI; -using KITT.Web.Models.Streamings; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; -namespace KITT.Web.App.Pages.Streamings; +namespace KITT.Cms.Web.App.Pages.Streamings; public partial class Schedule { @@ -66,3 +66,4 @@ private static ScheduleStreamingModel ConvertToApiModel(ScheduleForm.ViewModel m }; } } + diff --git a/src/KITT.Web.App/Pages/Streamings/StreamingDetail.razor b/src/KITT.Cms.Web.App/Pages/Streamings/StreamingDetail.razor similarity index 97% rename from src/KITT.Web.App/Pages/Streamings/StreamingDetail.razor rename to src/KITT.Cms.Web.App/Pages/Streamings/StreamingDetail.razor index bb63360..ac99e61 100644 --- a/src/KITT.Web.App/Pages/Streamings/StreamingDetail.razor +++ b/src/KITT.Cms.Web.App/Pages/Streamings/StreamingDetail.razor @@ -3,8 +3,6 @@ @inject IStringLocalizer CommonLocalizer @inject IStringLocalizer Localizer -@attribute [Authorize] - KITT - @pageTitle @@ -110,10 +108,10 @@ @bind-Value="streaming.StreamingAbstract" Appearance="FluentInputAppearance.Filled" Rows="10" - Style="width:100%;" - Disabled="@isReadOnly"/> + Style="width:100%;" + Disabled="@isReadOnly" /> - + \ No newline at end of file diff --git a/src/KITT.Web.App/Pages/Streamings/StreamingDetail.razor.cs b/src/KITT.Cms.Web.App/Pages/Streamings/StreamingDetail.razor.cs similarity index 96% rename from src/KITT.Web.App/Pages/Streamings/StreamingDetail.razor.cs rename to src/KITT.Cms.Web.App/Pages/Streamings/StreamingDetail.razor.cs index ebc195b..d370861 100644 --- a/src/KITT.Web.App/Pages/Streamings/StreamingDetail.razor.cs +++ b/src/KITT.Cms.Web.App/Pages/Streamings/StreamingDetail.razor.cs @@ -1,13 +1,13 @@ -using KITT.Web.App.Clients; -using KITT.Web.App.Model; +using KITT.Cms.Web.App.Clients; +using KITT.Cms.Web.Models; +using KITT.Cms.Web.Models.Streamings; using KITT.Web.App.UI; -using KITT.Web.Models.Streamings; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components.Extensions; using System.ComponentModel.DataAnnotations; -namespace KITT.Web.App.Pages.Streamings; +namespace KITT.Cms.Web.App.Pages.Streamings; public partial class StreamingDetail { @@ -147,3 +147,4 @@ public static ViewModel FromStreamingDetailModel(StreamingDetailModel model) } } } + diff --git a/src/KITT.Web.App/Resources/Components/ScheduleForm.Designer.cs b/src/KITT.Cms.Web.App/Resources/Components/ScheduleForm.Designer.cs similarity index 85% rename from src/KITT.Web.App/Resources/Components/ScheduleForm.Designer.cs rename to src/KITT.Cms.Web.App/Resources/Components/ScheduleForm.Designer.cs index 085c71b..68826f1 100644 --- a/src/KITT.Web.App/Resources/Components/ScheduleForm.Designer.cs +++ b/src/KITT.Cms.Web.App/Resources/Components/ScheduleForm.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Components { +namespace KITT.Cms.Web.App.Resources.Components { using System; @@ -22,7 +22,7 @@ namespace KITT.Web.App.Resources.Components { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class ScheduleForm { + internal class ScheduleForm { private static global::System.Resources.ResourceManager resourceMan; @@ -36,10 +36,10 @@ internal ScheduleForm() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { + internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Components.ScheduleForm", typeof(ScheduleForm).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Cms.Web.App.Resources.Components.ScheduleForm", typeof(ScheduleForm).Assembly); resourceMan = temp; } return resourceMan; @@ -51,7 +51,7 @@ internal ScheduleForm() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal ScheduleForm() { /// /// Looks up a localized string similar to Abstract. /// - public static string AbstractLabel { + internal static string AbstractLabel { get { return ResourceManager.GetString("AbstractLabel", resourceCulture); } @@ -72,7 +72,7 @@ public static string AbstractLabel { /// /// Looks up a localized string similar to Ending time. /// - public static string EndingTimeLabel { + internal static string EndingTimeLabel { get { return ResourceManager.GetString("EndingTimeLabel", resourceCulture); } @@ -81,7 +81,7 @@ public static string EndingTimeLabel { /// /// Looks up a localized string similar to Hosting channel. /// - public static string HostingChannelLabel { + internal static string HostingChannelLabel { get { return ResourceManager.GetString("HostingChannelLabel", resourceCulture); } @@ -90,7 +90,7 @@ public static string HostingChannelLabel { /// /// Looks up a localized string similar to Schedule date. /// - public static string ScheduleDateLabel { + internal static string ScheduleDateLabel { get { return ResourceManager.GetString("ScheduleDateLabel", resourceCulture); } @@ -99,7 +99,7 @@ public static string ScheduleDateLabel { /// /// Looks up a localized string similar to Slug. /// - public static string SlugLabel { + internal static string SlugLabel { get { return ResourceManager.GetString("SlugLabel", resourceCulture); } @@ -108,7 +108,7 @@ public static string SlugLabel { /// /// Looks up a localized string similar to Starting time. /// - public static string StartingTimeLabel { + internal static string StartingTimeLabel { get { return ResourceManager.GetString("StartingTimeLabel", resourceCulture); } @@ -117,7 +117,7 @@ public static string StartingTimeLabel { /// /// Looks up a localized string similar to Streaming. /// - public static string StreamingTabTitle { + internal static string StreamingTabTitle { get { return ResourceManager.GetString("StreamingTabTitle", resourceCulture); } @@ -126,7 +126,7 @@ public static string StreamingTabTitle { /// /// Looks up a localized string similar to Title. /// - public static string TitleLabel { + internal static string TitleLabel { get { return ResourceManager.GetString("TitleLabel", resourceCulture); } diff --git a/src/KITT.Web.App/Resources/Components/ScheduleForm.it.resx b/src/KITT.Cms.Web.App/Resources/Components/ScheduleForm.it.resx similarity index 99% rename from src/KITT.Web.App/Resources/Components/ScheduleForm.it.resx rename to src/KITT.Cms.Web.App/Resources/Components/ScheduleForm.it.resx index 640ad3c..cd22a85 100644 --- a/src/KITT.Web.App/Resources/Components/ScheduleForm.it.resx +++ b/src/KITT.Cms.Web.App/Resources/Components/ScheduleForm.it.resx @@ -141,4 +141,4 @@ Titolo - \ No newline at end of file + diff --git a/src/KITT.Web.App/Resources/Components/ScheduleForm.resx b/src/KITT.Cms.Web.App/Resources/Components/ScheduleForm.resx similarity index 100% rename from src/KITT.Web.App/Resources/Components/ScheduleForm.resx rename to src/KITT.Cms.Web.App/Resources/Components/ScheduleForm.resx diff --git a/src/KITT.Web.App/Resources/Pages/Index.Designer.cs b/src/KITT.Cms.Web.App/Resources/Components/StreamingStats.Designer.cs similarity index 68% rename from src/KITT.Web.App/Resources/Pages/Index.Designer.cs rename to src/KITT.Cms.Web.App/Resources/Components/StreamingStats.Designer.cs index bd75894..cad1669 100644 --- a/src/KITT.Web.App/Resources/Pages/Index.Designer.cs +++ b/src/KITT.Cms.Web.App/Resources/Components/StreamingStats.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Pages { +namespace KITT.Cms.Web.App.Resources.Components { using System; @@ -22,24 +22,24 @@ namespace KITT.Web.App.Resources.Pages { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Index { + internal class StreamingStats { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Index() { + internal StreamingStats() { } /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { + internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Pages.Index", typeof(Index).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Cms.Web.App.Resources.Components.StreamingStats", typeof(StreamingStats).Assembly); resourceMan = temp; } return resourceMan; @@ -51,7 +51,7 @@ internal Index() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal Index() { /// /// Looks up a localized string similar to lives delivered. /// - public static string LivesDeliveredLabel { + internal static string LivesDeliveredLabel { get { return ResourceManager.GetString("LivesDeliveredLabel", resourceCulture); } @@ -72,43 +72,16 @@ public static string LivesDeliveredLabel { /// /// Looks up a localized string similar to lives scheduled. /// - public static string LivesScheduledLabel { + internal static string LivesScheduledLabel { get { return ResourceManager.GetString("LivesScheduledLabel", resourceCulture); } } - /// - /// Looks up a localized string similar to proposals accepted. - /// - public static string ProposalsAcceptedLabel { - get { - return ResourceManager.GetString("ProposalsAcceptedLabel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to proposals. - /// - public static string ProposalsLabel { - get { - return ResourceManager.GetString("ProposalsLabel", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to View all the proposals. - /// - public static string ViewProposalsButtonText { - get { - return ResourceManager.GetString("ViewProposalsButtonText", resourceCulture); - } - } - /// /// Looks up a localized string similar to View all my streamings. /// - public static string ViewStreamingsButtonText { + internal static string ViewStreamingsButtonText { get { return ResourceManager.GetString("ViewStreamingsButtonText", resourceCulture); } diff --git a/src/KITT.Web.App/Resources/Pages/Index.it.resx b/src/KITT.Cms.Web.App/Resources/Components/StreamingStats.it.resx similarity index 94% rename from src/KITT.Web.App/Resources/Pages/Index.it.resx rename to src/KITT.Cms.Web.App/Resources/Components/StreamingStats.it.resx index 4445d22..1f122d3 100644 --- a/src/KITT.Web.App/Resources/Pages/Index.it.resx +++ b/src/KITT.Cms.Web.App/Resources/Components/StreamingStats.it.resx @@ -123,16 +123,7 @@ dirette pianificate - - proposte accettate - - - proposte - - - Guarda tutte le proposte - Vai all'elenco delle dirette - \ No newline at end of file + diff --git a/src/KITT.Web.App/Resources/Pages/Index.resx b/src/KITT.Cms.Web.App/Resources/Components/StreamingStats.resx similarity index 95% rename from src/KITT.Web.App/Resources/Pages/Index.resx rename to src/KITT.Cms.Web.App/Resources/Components/StreamingStats.resx index 15b0308..caeebb0 100644 --- a/src/KITT.Web.App/Resources/Pages/Index.resx +++ b/src/KITT.Cms.Web.App/Resources/Components/StreamingStats.resx @@ -123,15 +123,6 @@ lives scheduled - - proposals accepted - - - proposals - - - View all the proposals - View all my streamings diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/Import.Designer.cs b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Import.Designer.cs similarity index 96% rename from src/KITT.Web.App/Resources/Pages/Streamings/Import.Designer.cs rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/Import.Designer.cs index 236ab6c..9005c04 100644 --- a/src/KITT.Web.App/Resources/Pages/Streamings/Import.Designer.cs +++ b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Import.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Pages.Streamings { +namespace KITT.Cms.Web.App.Resources.Pages.Streamings { using System; @@ -39,7 +39,7 @@ internal Import() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Pages.Streamings.Import", typeof(Import).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Cms.Web.App.Resources.Pages.Streamings.Import", typeof(Import).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/Import.it.resx b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Import.it.resx similarity index 100% rename from src/KITT.Web.App/Resources/Pages/Streamings/Import.it.resx rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/Import.it.resx diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/Import.resx b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Import.resx similarity index 100% rename from src/KITT.Web.App/Resources/Pages/Streamings/Import.resx rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/Import.resx diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/Index.Designer.cs b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Index.Designer.cs similarity index 84% rename from src/KITT.Web.App/Resources/Pages/Streamings/Index.Designer.cs rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/Index.Designer.cs index ef2df68..982935e 100644 --- a/src/KITT.Web.App/Resources/Pages/Streamings/Index.Designer.cs +++ b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Index.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Pages.Streamings { +namespace KITT.Cms.Web.App.Resources.Pages.Streamings { using System; @@ -22,7 +22,7 @@ namespace KITT.Web.App.Resources.Pages.Streamings { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Index { + internal class Index { private static global::System.Resources.ResourceManager resourceMan; @@ -36,10 +36,10 @@ internal Index() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { + internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Pages.Streamings.Index", typeof(Index).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Cms.Web.App.Resources.Pages.Streamings.Index", typeof(Index).Assembly); resourceMan = temp; } return resourceMan; @@ -51,7 +51,7 @@ internal Index() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal Index() { /// /// Looks up a localized string similar to Older. /// - public static string Ascending { + internal static string Ascending { get { return ResourceManager.GetString("Ascending", resourceCulture); } @@ -72,7 +72,7 @@ public static string Ascending { /// /// Looks up a localized string similar to Date. /// - public static string DateLabel { + internal static string DateLabel { get { return ResourceManager.GetString("DateLabel", resourceCulture); } @@ -81,7 +81,7 @@ public static string DateLabel { /// /// Looks up a localized string similar to You are going to delete the streaming {0}. Are you sure?. /// - public static string DeleteStreamingConfirmText { + internal static string DeleteStreamingConfirmText { get { return ResourceManager.GetString("DeleteStreamingConfirmText", resourceCulture); } @@ -90,7 +90,7 @@ public static string DeleteStreamingConfirmText { /// /// Looks up a localized string similar to Deleting {0}. /// - public static string DeleteStreamingConfirmTitle { + internal static string DeleteStreamingConfirmTitle { get { return ResourceManager.GetString("DeleteStreamingConfirmTitle", resourceCulture); } @@ -99,7 +99,7 @@ public static string DeleteStreamingConfirmTitle { /// /// Looks up a localized string similar to There was an error deleting streaming {0}. /// - public static string DeleteStreamingErrorMessage { + internal static string DeleteStreamingErrorMessage { get { return ResourceManager.GetString("DeleteStreamingErrorMessage", resourceCulture); } @@ -108,7 +108,7 @@ public static string DeleteStreamingErrorMessage { /// /// Looks up a localized string similar to Streaming {0} deleted successfully!. /// - public static string DeleteStreamingSuccessMessage { + internal static string DeleteStreamingSuccessMessage { get { return ResourceManager.GetString("DeleteStreamingSuccessMessage", resourceCulture); } @@ -117,7 +117,7 @@ public static string DeleteStreamingSuccessMessage { /// /// Looks up a localized string similar to Newer. /// - public static string Descending { + internal static string Descending { get { return ResourceManager.GetString("Descending", resourceCulture); } @@ -126,7 +126,7 @@ public static string Descending { /// /// Looks up a localized string similar to Duration. /// - public static string DurationLabel { + internal static string DurationLabel { get { return ResourceManager.GetString("DurationLabel", resourceCulture); } @@ -135,7 +135,7 @@ public static string DurationLabel { /// /// Looks up a localized string similar to Import streaming. /// - public static string ImportStreamingLabel { + internal static string ImportStreamingLabel { get { return ResourceManager.GetString("ImportStreamingLabel", resourceCulture); } @@ -144,7 +144,7 @@ public static string ImportStreamingLabel { /// /// Looks up a localized string similar to No streamings here. Try another search. /// - public static string NoStreamingsFoundLabel { + internal static string NoStreamingsFoundLabel { get { return ResourceManager.GetString("NoStreamingsFoundLabel", resourceCulture); } @@ -153,7 +153,7 @@ public static string NoStreamingsFoundLabel { /// /// Looks up a localized string similar to page {0} of {1}. /// - public static string PageNumberLabel { + internal static string PageNumberLabel { get { return ResourceManager.GetString("PageNumberLabel", resourceCulture); } @@ -162,7 +162,7 @@ public static string PageNumberLabel { /// /// Looks up a localized string similar to My streamings. /// - public static string PageTitle { + internal static string PageTitle { get { return ResourceManager.GetString("PageTitle", resourceCulture); } @@ -171,7 +171,7 @@ public static string PageTitle { /// /// Looks up a localized string similar to {0} streamings. /// - public static string PaginationItemsNumberText { + internal static string PaginationItemsNumberText { get { return ResourceManager.GetString("PaginationItemsNumberText", resourceCulture); } @@ -180,7 +180,7 @@ public static string PaginationItemsNumberText { /// /// Looks up a localized string similar to Schedule new streaming. /// - public static string ScheduleNewStreamingLabel { + internal static string ScheduleNewStreamingLabel { get { return ResourceManager.GetString("ScheduleNewStreamingLabel", resourceCulture); } @@ -189,7 +189,7 @@ public static string ScheduleNewStreamingLabel { /// /// Looks up a localized string similar to Search. /// - public static string SearchButtonLabel { + internal static string SearchButtonLabel { get { return ResourceManager.GetString("SearchButtonLabel", resourceCulture); } @@ -198,7 +198,7 @@ public static string SearchButtonLabel { /// /// Looks up a localized string similar to Order. /// - public static string StreamingQueryOrderLabel { + internal static string StreamingQueryOrderLabel { get { return ResourceManager.GetString("StreamingQueryOrderLabel", resourceCulture); } @@ -207,7 +207,7 @@ public static string StreamingQueryOrderLabel { /// /// Looks up a localized string similar to Nr. of elements. /// - public static string StreamingQuerySizeLabel { + internal static string StreamingQuerySizeLabel { get { return ResourceManager.GetString("StreamingQuerySizeLabel", resourceCulture); } @@ -216,7 +216,7 @@ public static string StreamingQuerySizeLabel { /// /// Looks up a localized string similar to Title. /// - public static string TitleLabel { + internal static string TitleLabel { get { return ResourceManager.GetString("TitleLabel", resourceCulture); } @@ -225,7 +225,7 @@ public static string TitleLabel { /// /// Looks up a localized string similar to Twitch link. /// - public static string TwitchLinkLabel { + internal static string TwitchLinkLabel { get { return ResourceManager.GetString("TwitchLinkLabel", resourceCulture); } @@ -234,7 +234,7 @@ public static string TwitchLinkLabel { /// /// Looks up a localized string similar to YouTube link. /// - public static string YoutubeVideoUrlLabel { + internal static string YoutubeVideoUrlLabel { get { return ResourceManager.GetString("YoutubeVideoUrlLabel", resourceCulture); } diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/Index.it.resx b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Index.it.resx similarity index 100% rename from src/KITT.Web.App/Resources/Pages/Streamings/Index.it.resx rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/Index.it.resx index 7acb082..60d4707 100644 --- a/src/KITT.Web.App/Resources/Pages/Streamings/Index.it.resx +++ b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Index.it.resx @@ -147,6 +147,9 @@ Nessuna diretta trovata. Prova a fare un'altra ricerca + + pagina {0} di {1} + Le mie dirette @@ -156,6 +159,9 @@ Pianifica una nuova diretta + + Cerca + Ordina per @@ -171,10 +177,4 @@ YouTube link - - pagina {0} di {1} - - - Cerca - \ No newline at end of file diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/Index.resx b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Index.resx similarity index 100% rename from src/KITT.Web.App/Resources/Pages/Streamings/Index.resx rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/Index.resx index 8b8db58..bd3d7f1 100644 --- a/src/KITT.Web.App/Resources/Pages/Streamings/Index.resx +++ b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Index.resx @@ -147,6 +147,9 @@ No streamings here. Try another search + + page {0} of {1} + My streamings @@ -156,6 +159,9 @@ Schedule new streaming + + Search + Order @@ -171,10 +177,4 @@ YouTube link - - page {0} of {1} - - - Search - \ No newline at end of file diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/Schedule.Designer.cs b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Schedule.Designer.cs similarity index 86% rename from src/KITT.Web.App/Resources/Pages/Streamings/Schedule.Designer.cs rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/Schedule.Designer.cs index a1eb15e..ec19e9a 100644 --- a/src/KITT.Web.App/Resources/Pages/Streamings/Schedule.Designer.cs +++ b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Schedule.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Pages.Streamings { +namespace KITT.Cms.Web.App.Resources.Pages.Streamings { using System; @@ -22,7 +22,7 @@ namespace KITT.Web.App.Resources.Pages.Streamings { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Schedule { + internal class Schedule { private static global::System.Resources.ResourceManager resourceMan; @@ -36,10 +36,10 @@ internal Schedule() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { + internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Pages.Streamings.Schedule", typeof(Schedule).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Cms.Web.App.Resources.Pages.Streamings.Schedule", typeof(Schedule).Assembly); resourceMan = temp; } return resourceMan; @@ -51,7 +51,7 @@ internal Schedule() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal Schedule() { /// /// Looks up a localized string similar to Schedule streaming. /// - public static string PageTitle { + internal static string PageTitle { get { return ResourceManager.GetString("PageTitle", resourceCulture); } @@ -72,7 +72,7 @@ public static string PageTitle { /// /// Looks up a localized string similar to The streaming {0} has been scheduled successfully!. /// - public static string StreamingScheduledSuccessfully { + internal static string StreamingScheduledSuccessfully { get { return ResourceManager.GetString("StreamingScheduledSuccessfully", resourceCulture); } diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/Schedule.it.resx b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Schedule.it.resx similarity index 100% rename from src/KITT.Web.App/Resources/Pages/Streamings/Schedule.it.resx rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/Schedule.it.resx diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/Schedule.resx b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/Schedule.resx similarity index 100% rename from src/KITT.Web.App/Resources/Pages/Streamings/Schedule.resx rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/Schedule.resx diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/StreamingDetail.Designer.cs b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/StreamingDetail.Designer.cs similarity index 96% rename from src/KITT.Web.App/Resources/Pages/Streamings/StreamingDetail.Designer.cs rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/StreamingDetail.Designer.cs index 50dcaec..b51e4aa 100644 --- a/src/KITT.Web.App/Resources/Pages/Streamings/StreamingDetail.Designer.cs +++ b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/StreamingDetail.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Pages.Streamings { +namespace KITT.Cms.Web.App.Resources.Pages.Streamings { using System; @@ -39,7 +39,7 @@ internal StreamingDetail() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Pages.Streamings.StreamingDetail", typeof(StreamingDetail).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Cms.Web.App.Resources.Pages.Streamings.StreamingDetail", typeof(StreamingDetail).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/StreamingDetail.it.resx b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/StreamingDetail.it.resx similarity index 100% rename from src/KITT.Web.App/Resources/Pages/Streamings/StreamingDetail.it.resx rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/StreamingDetail.it.resx diff --git a/src/KITT.Web.App/Resources/Pages/Streamings/StreamingDetail.resx b/src/KITT.Cms.Web.App/Resources/Pages/Streamings/StreamingDetail.resx similarity index 100% rename from src/KITT.Web.App/Resources/Pages/Streamings/StreamingDetail.resx rename to src/KITT.Cms.Web.App/Resources/Pages/Streamings/StreamingDetail.resx diff --git a/src/KITT.Cms.Web.App/_Imports.razor b/src/KITT.Cms.Web.App/_Imports.razor new file mode 100644 index 0000000..4dc0f34 --- /dev/null +++ b/src/KITT.Cms.Web.App/_Imports.razor @@ -0,0 +1,19 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Sections +@using Microsoft.Extensions.Localization +@using Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.JSInterop +@using KITT.Cms.Web.App.Components +@using KITT.Web.App.UI.Components +@using KITT.Web.App.UI +@using KITT.Cms.Web.Models.Streamings +@using UI = KITT.Web.App.UI +@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using Microsoft.AspNetCore.Authorization + +@attribute [Authorize] diff --git a/src/KITT.Web.App/Model/ContentViewModel.cs b/src/KITT.Cms.Web.Models/ContentViewModel.cs similarity index 58% rename from src/KITT.Web.App/Model/ContentViewModel.cs rename to src/KITT.Cms.Web.Models/ContentViewModel.cs index c97ff99..b835e9a 100644 --- a/src/KITT.Web.App/Model/ContentViewModel.cs +++ b/src/KITT.Cms.Web.Models/ContentViewModel.cs @@ -1,6 +1,4 @@ -using KITT.Web.Models; - -namespace KITT.Web.App.Model; +namespace KITT.Cms.Web.Models; public class ContentViewModel { diff --git a/src/KITT.Cms.Web.Models/KITT.Cms.Web.Models.csproj b/src/KITT.Cms.Web.Models/KITT.Cms.Web.Models.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/src/KITT.Cms.Web.Models/KITT.Cms.Web.Models.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/src/KITT.Cms.Web.Models/SeoData.cs b/src/KITT.Cms.Web.Models/SeoData.cs new file mode 100644 index 0000000..f9c26e3 --- /dev/null +++ b/src/KITT.Cms.Web.Models/SeoData.cs @@ -0,0 +1,10 @@ +namespace KITT.Cms.Web.Models; + +public record SeoData +{ + public string Title { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + + public string Keywords { get; set; } = string.Empty; +} diff --git a/src/KITT.Web.Models/Settings/CreateNewSettingsModel.cs b/src/KITT.Cms.Web.Models/Settings/CreateNewSettingsModel.cs similarity index 80% rename from src/KITT.Web.Models/Settings/CreateNewSettingsModel.cs rename to src/KITT.Cms.Web.Models/Settings/CreateNewSettingsModel.cs index 820e550..fbb1903 100644 --- a/src/KITT.Web.Models/Settings/CreateNewSettingsModel.cs +++ b/src/KITT.Cms.Web.Models/Settings/CreateNewSettingsModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace KITT.Web.Models.Settings; +namespace KITT.Cms.Web.Models.Settings; public class CreateNewSettingsModel { diff --git a/src/KITT.Web.Models/Settings/SettingsDetailModel.cs b/src/KITT.Cms.Web.Models/Settings/SettingsDetailModel.cs similarity index 75% rename from src/KITT.Web.Models/Settings/SettingsDetailModel.cs rename to src/KITT.Cms.Web.Models/Settings/SettingsDetailModel.cs index 5ed62cc..d747792 100644 --- a/src/KITT.Web.Models/Settings/SettingsDetailModel.cs +++ b/src/KITT.Cms.Web.Models/Settings/SettingsDetailModel.cs @@ -1,4 +1,4 @@ -namespace KITT.Web.Models.Settings; +namespace KITT.Cms.Web.Models.Settings; public class SettingsDetailModel { diff --git a/src/KITT.Web.Models/Settings/SettingsListModel.cs b/src/KITT.Cms.Web.Models/Settings/SettingsListModel.cs similarity index 77% rename from src/KITT.Web.Models/Settings/SettingsListModel.cs rename to src/KITT.Cms.Web.Models/Settings/SettingsListModel.cs index 0c75dfb..0d10e9d 100644 --- a/src/KITT.Web.Models/Settings/SettingsListModel.cs +++ b/src/KITT.Cms.Web.Models/Settings/SettingsListModel.cs @@ -1,8 +1,8 @@ -namespace KITT.Web.Models.Settings; +namespace KITT.Cms.Web.Models.Settings; public class SettingsListModel { - public IEnumerable Items { get; set; } = Array.Empty(); + public IEnumerable Items { get; set; } = []; public record SettingsDescriptor { diff --git a/src/KITT.Web.Models/Streamings/ImportStreamingModel.cs b/src/KITT.Cms.Web.Models/Streamings/ImportStreamingModel.cs similarity index 94% rename from src/KITT.Web.Models/Streamings/ImportStreamingModel.cs rename to src/KITT.Cms.Web.Models/Streamings/ImportStreamingModel.cs index 7509c03..cd54620 100644 --- a/src/KITT.Web.Models/Streamings/ImportStreamingModel.cs +++ b/src/KITT.Cms.Web.Models/Streamings/ImportStreamingModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace KITT.Web.Models.Streamings; +namespace KITT.Cms.Web.Models.Streamings; public record ImportStreamingModel { diff --git a/src/KITT.Web.Models/Streamings/ScheduleStreamingModel.cs b/src/KITT.Cms.Web.Models/Streamings/ScheduleStreamingModel.cs similarity index 93% rename from src/KITT.Web.Models/Streamings/ScheduleStreamingModel.cs rename to src/KITT.Cms.Web.Models/Streamings/ScheduleStreamingModel.cs index 6075936..a393921 100644 --- a/src/KITT.Web.Models/Streamings/ScheduleStreamingModel.cs +++ b/src/KITT.Cms.Web.Models/Streamings/ScheduleStreamingModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace KITT.Web.Models.Streamings; +namespace KITT.Cms.Web.Models.Streamings; public record ScheduleStreamingModel { diff --git a/src/KITT.Web.Models/Streamings/StreamingDetailModel.cs b/src/KITT.Cms.Web.Models/Streamings/StreamingDetailModel.cs similarity index 94% rename from src/KITT.Web.Models/Streamings/StreamingDetailModel.cs rename to src/KITT.Cms.Web.Models/Streamings/StreamingDetailModel.cs index 82866be..2d4b2ea 100644 --- a/src/KITT.Web.Models/Streamings/StreamingDetailModel.cs +++ b/src/KITT.Cms.Web.Models/Streamings/StreamingDetailModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace KITT.Web.Models.Streamings; +namespace KITT.Cms.Web.Models.Streamings; public record StreamingDetailModel { diff --git a/src/KITT.Cms.Web.Models/Streamings/StreamingStatsModel.cs b/src/KITT.Cms.Web.Models/Streamings/StreamingStatsModel.cs new file mode 100644 index 0000000..09e1634 --- /dev/null +++ b/src/KITT.Cms.Web.Models/Streamings/StreamingStatsModel.cs @@ -0,0 +1,5 @@ +namespace KITT.Cms.Web.Models.Streamings; + +public record StreamingStatsModel( + int NumberOfStreamingsDelivered, + int NumberOfStreamingsScheduled); diff --git a/src/KITT.Web.Models/Streamings/StreamingsListModel.cs b/src/KITT.Cms.Web.Models/Streamings/StreamingsListModel.cs similarity index 93% rename from src/KITT.Web.Models/Streamings/StreamingsListModel.cs rename to src/KITT.Cms.Web.Models/Streamings/StreamingsListModel.cs index bd89500..18ea84f 100644 --- a/src/KITT.Web.Models/Streamings/StreamingsListModel.cs +++ b/src/KITT.Cms.Web.Models/Streamings/StreamingsListModel.cs @@ -1,4 +1,4 @@ -namespace KITT.Web.Models.Streamings; +namespace KITT.Cms.Web.Models.Streamings; public class StreamingsListModel { diff --git a/src/KITT.Web.Models/Streamings/StreamingsQueryModel.cs b/src/KITT.Cms.Web.Models/Streamings/StreamingsQueryModel.cs similarity index 94% rename from src/KITT.Web.Models/Streamings/StreamingsQueryModel.cs rename to src/KITT.Cms.Web.Models/Streamings/StreamingsQueryModel.cs index d9c00ee..fd1322c 100644 --- a/src/KITT.Web.Models/Streamings/StreamingsQueryModel.cs +++ b/src/KITT.Cms.Web.Models/Streamings/StreamingsQueryModel.cs @@ -1,6 +1,6 @@ using System.Web; -namespace KITT.Web.Models.Streamings; +namespace KITT.Cms.Web.Models.Streamings; public record StreamingQueryModel { diff --git a/src/KITT.Core/Commands/StreamingCommands.cs b/src/KITT.Core/Commands/StreamingCommands.cs index 2bd187a..54123af 100644 --- a/src/KITT.Core/Commands/StreamingCommands.cs +++ b/src/KITT.Core/Commands/StreamingCommands.cs @@ -46,16 +46,16 @@ public async Task ScheduleStreamingAsync(string userId, string twitchChann _context.Streamings.Add(streaming); await _context.SaveChangesAsync(); - var message = new StreamingScheduledMessage( - streaming.Id, - streamingTitle, - streamingSlug, - scheduleDate, - startingTime, - endingTime, - hostingChannelUrl); + //var message = new StreamingScheduledMessage( + // streaming.Id, + // streamingTitle, + // streamingSlug, + // scheduleDate, + // startingTime, + // endingTime, + // hostingChannelUrl); - await _messageBus.SendAsync(message); + //await _messageBus.SendAsync(message); return streaming.Id; } @@ -78,30 +78,30 @@ public async Task UpdateStreamingAsync(Guid streamingId, string streamingTitle, if (ScheduleHasChanged(streaming, scheduleDate, startingTime, endingTime)) { streaming.ChangeSchedule(scheduleDate, startingTime, endingTime); - var scheduledChangedMessage = new StreamingScheduleChangedMessage( - streamingId, - streaming.Title, - streaming.Slug, - scheduleDate, - startingTime, - endingTime); - - messagesToSend.Add(scheduledChangedMessage); + //var scheduledChangedMessage = new StreamingScheduleChangedMessage( + // streamingId, + // streaming.Title, + // streaming.Slug, + // scheduleDate, + // startingTime, + // endingTime); + + //messagesToSend.Add(scheduledChangedMessage); } if (streaming.HostingChannelUrl != hostingChannelUrl) { streaming.ChangeHostingChannelUrl(hostingChannelUrl); - var hostingChannelChangedMessage = new StreamingHostingChannelChangedMessage( - streamingId, - streaming.Title, - streaming.Slug, - hostingChannelUrl, - scheduleDate, - startingTime); + //var hostingChannelChangedMessage = new StreamingHostingChannelChangedMessage( + // streamingId, + // streaming.Title, + // streaming.Slug, + // hostingChannelUrl, + // scheduleDate, + // startingTime); - messagesToSend.Add(hostingChannelChangedMessage); + //messagesToSend.Add(hostingChannelChangedMessage); } if (streaming.Abstract != streamingAbstract) @@ -112,13 +112,13 @@ public async Task UpdateStreamingAsync(Guid streamingId, string streamingTitle, if (streaming.YouTubeVideoUrl != youtubeRegistrationLink) { streaming.SetRegistrationYoutubeUrl(youtubeRegistrationLink); - var videoUploadedMessage = new StreamingVideoUploadedMessage( - streamingId, - streaming.Title, - streaming.Slug, - youtubeRegistrationLink); + //var videoUploadedMessage = new StreamingVideoUploadedMessage( + // streamingId, + // streaming.Title, + // streaming.Slug, + // youtubeRegistrationLink); - messagesToSend.Add(videoUploadedMessage); + //messagesToSend.Add(videoUploadedMessage); } if (seo is not null) @@ -130,10 +130,10 @@ public async Task UpdateStreamingAsync(Guid streamingId, string streamingTitle, await _context.SaveChangesAsync(); - foreach (var message in messagesToSend) - { - await _messageBus.SendAsync(message); - } + //foreach (var message in messagesToSend) + //{ + // await _messageBus.SendAsync(message); + //} } public async Task DeleteStreamingAsync(Guid streamingId) @@ -147,14 +147,14 @@ public async Task DeleteStreamingAsync(Guid streamingId) _context.Streamings.Remove(streaming); await _context.SaveChangesAsync(); - var message = new StreamingCanceledMessage( - streamingId, - streaming.Title, - streaming.ScheduleDate, - streaming.StartingTime, - streaming.EndingTime); + //var message = new StreamingCanceledMessage( + // streamingId, + // streaming.Title, + // streaming.ScheduleDate, + // streaming.StartingTime, + // streaming.EndingTime); - await _messageBus.SendAsync(message); + //await _messageBus.SendAsync(message); } public async Task ImportStreamingAsync(string userId, string twitchChannel, string streamingTitle, string streamingSlug, DateOnly scheduleDate, TimeOnly startingTime, TimeOnly endingTime, string hostingChannelUrl, string streamingAbstract, string youtubeRegistrationLink, Content.SeoData seo) diff --git a/src/KITT.Core/KITT.Core.csproj b/src/KITT.Core/KITT.Core.csproj index 5fa59f5..58bb36c 100644 --- a/src/KITT.Core/KITT.Core.csproj +++ b/src/KITT.Core/KITT.Core.csproj @@ -1,18 +1,18 @@ - net8.0 + net9.0 enable - - - - - - - + + + + + + + diff --git a/src/LemonBot.Web/Endpoints/ProposalsEndpoints.cs b/src/KITT.Proposals.Web.Api/Endpoints/ProposalsEndpoints.cs similarity index 87% rename from src/LemonBot.Web/Endpoints/ProposalsEndpoints.cs rename to src/KITT.Proposals.Web.Api/Endpoints/ProposalsEndpoints.cs index bf8ad4b..2371b62 100644 --- a/src/LemonBot.Web/Endpoints/ProposalsEndpoints.cs +++ b/src/KITT.Proposals.Web.Api/Endpoints/ProposalsEndpoints.cs @@ -1,17 +1,18 @@ -using KITT.Web.Models.Proposals; -using LemonBot.Web.Endpoints.Services; +using KITT.Proposals.Web.Api.Endpoints.Services; +using KITT.Proposals.Web.Models; +using KITT.Web.Shared.Security; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; -namespace LemonBot.Web.Endpoints; +namespace KITT.Proposals.Web.Api.Endpoints; public static class ProposalsEndpoints { public static IEndpointRouteBuilder MapProposalsEndpoints(this IEndpointRouteBuilder builder) { var proposalsGroup = builder - .MapGroup("api/console/proposals") + .MapGroup("api/proposals") .RequireAuthorization() .WithParameterValidation(); @@ -45,6 +46,11 @@ public static IEndpointRouteBuilder MapProposalsEndpoints(this IEndpointRouteBui .WithName(nameof(ScheduleProposal)) .WithOpenApi(); + proposalsGroup + .MapGet("stats", GetProposalsStats) + .WithName(nameof(GetProposalsStats)) + .WithOpenApi(); + return builder; } @@ -158,4 +164,11 @@ private static async Task> return TypedResults.NotFound(); } } + + private static async Task> GetProposalsStats( + ProposalsEndpointsServices services) + { + var proposalsStats = await services.GetProposalsStatsAsync(); + return TypedResults.Ok(proposalsStats); + } } diff --git a/src/LemonBot.Web/Endpoints/Services/ProposalsEndpointsServices.cs b/src/KITT.Proposals.Web.Api/Endpoints/Services/ProposalsEndpointsServices.cs similarity index 88% rename from src/LemonBot.Web/Endpoints/Services/ProposalsEndpointsServices.cs rename to src/KITT.Proposals.Web.Api/Endpoints/Services/ProposalsEndpointsServices.cs index 3d75d05..6c1772f 100644 --- a/src/LemonBot.Web/Endpoints/Services/ProposalsEndpointsServices.cs +++ b/src/KITT.Proposals.Web.Api/Endpoints/Services/ProposalsEndpointsServices.cs @@ -1,9 +1,9 @@ using KITT.Core.Commands; using KITT.Core.ReadModels; -using KITT.Web.Models.Proposals; +using KITT.Proposals.Web.Models; using Microsoft.EntityFrameworkCore; -namespace LemonBot.Web.Endpoints.Services; +namespace KITT.Proposals.Web.Api.Endpoints.Services; public class ProposalsEndpointsServices { @@ -102,4 +102,14 @@ public Task ScheduleProposalAsync(Guid proposalId, ScheduleProposalModel model, model.HostingChannelUrl, model.StreamingAbstract); } + + public async Task GetProposalsStatsAsync() + { + var proposalsQuery = Database.Proposals; + + var totalNumberOfProposals = await proposalsQuery.CountAsync(); + var acceptedProposalsNumber = await proposalsQuery.WaitingForApproval().CountAsync(); + + return new ProposalsStatsModel(totalNumberOfProposals, acceptedProposalsNumber); + } } diff --git a/src/KITT.Proposals.Web.Api/KITT.Proposals.Web.Api.csproj b/src/KITT.Proposals.Web.Api/KITT.Proposals.Web.Api.csproj new file mode 100644 index 0000000..50917e0 --- /dev/null +++ b/src/KITT.Proposals.Web.Api/KITT.Proposals.Web.Api.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/KITT.Proposals.Web.Api/KITT.Proposals.Web.Api.http b/src/KITT.Proposals.Web.Api/KITT.Proposals.Web.Api.http new file mode 100644 index 0000000..6e6af92 --- /dev/null +++ b/src/KITT.Proposals.Web.Api/KITT.Proposals.Web.Api.http @@ -0,0 +1,6 @@ +@KITT.Proposals.Web.Api_HostAddress = http://localhost:5278 + +GET {{KITT.Proposals.Web.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/KITT.Proposals.Web.Api/LocalMessageBus.cs b/src/KITT.Proposals.Web.Api/LocalMessageBus.cs new file mode 100644 index 0000000..4b25752 --- /dev/null +++ b/src/KITT.Proposals.Web.Api/LocalMessageBus.cs @@ -0,0 +1,11 @@ +using KITT.Telegram.Messages; + +namespace KITT.Proposals.Web.Api; + +internal class LocalMessageBus : IMessageBus +{ + public Task SendAsync(TMessage message) where TMessage : class + { + return Task.CompletedTask; + } +} diff --git a/src/KITT.Proposals.Web.Api/Program.cs b/src/KITT.Proposals.Web.Api/Program.cs new file mode 100644 index 0000000..a971462 --- /dev/null +++ b/src/KITT.Proposals.Web.Api/Program.cs @@ -0,0 +1,50 @@ +using KITT.Core.DependencyInjection; +using KITT.Core.Persistence; +using KITT.Proposals.Web.Api; +using KITT.Proposals.Web.Api.Endpoints; +using KITT.Proposals.Web.Api.Endpoints.Services; +using KITT.Services; +using KITT.Telegram.Messages; +using Microsoft.AspNetCore.Authentication.JwtBearer; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddSqlServerDbContext(ServiceNames.Database); + +builder.Services.AddAuthentication() + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => + { + options.Authority = $"https://sts.windows.net/{builder.Configuration["Identity:TenantId"]}/"; + options.Audience = $"api://{builder.Configuration["Identity:Proposals:AppId"]}"; + }); + +builder.Services.AddAuthorization(); + +builder.Services.AddOpenApi(); + +builder.Services.AddKittCore(); + +//TOFIX this line will be fixed in future to use the correct service +builder.Services.AddSingleton(); + +builder.Services.AddScoped(); +builder.Services.AddProblemDetails(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseExceptionHandler(); +app.UseStatusCodePages(); + +app.MapDefaultEndpoints(); +app.MapProposalsEndpoints(); + +app.Run(); diff --git a/src/KITT.Proposals.Web.Api/Properties/launchSettings.json b/src/KITT.Proposals.Web.Api/Properties/launchSettings.json new file mode 100644 index 0000000..51a8160 --- /dev/null +++ b/src/KITT.Proposals.Web.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:45167", + "sslPort": 44351 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5278", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7257;http://localhost:5278", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/KITT.Proposals.Web.Api/appsettings.json b/src/KITT.Proposals.Web.Api/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/KITT.Proposals.Web.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/KITT.Proposals.Web.App/Clients/ClientsServiceCollectionExtensions.cs b/src/KITT.Proposals.Web.App/Clients/ClientsServiceCollectionExtensions.cs new file mode 100644 index 0000000..bed3238 --- /dev/null +++ b/src/KITT.Proposals.Web.App/Clients/ClientsServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using KITT.Proposals.Web.App.Clients.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace KITT.Proposals.Web.App.Clients; + +public static class ClientsServiceCollectionExtensions +{ + public static IServiceCollection AddProposalsClients( + this IServiceCollection services, + string apiBaseUrl) + { + services + .AddHttpClient(client => client.BaseAddress = new(apiBaseUrl)); + + return services; + } +} diff --git a/src/KITT.Web.App/Clients/Http/ProposalsHttpClient.cs b/src/KITT.Proposals.Web.App/Clients/Http/ProposalsHttpClient.cs similarity index 85% rename from src/KITT.Web.App/Clients/Http/ProposalsHttpClient.cs rename to src/KITT.Proposals.Web.App/Clients/Http/ProposalsHttpClient.cs index 5e48d60..34ed9bb 100644 --- a/src/KITT.Web.App/Clients/Http/ProposalsHttpClient.cs +++ b/src/KITT.Proposals.Web.App/Clients/Http/ProposalsHttpClient.cs @@ -1,13 +1,13 @@ -using KITT.Web.Models.Proposals; +using KITT.Proposals.Web.Models; using System.Net.Http.Json; -namespace KITT.Web.App.Clients.Http; +namespace KITT.Proposals.Web.App.Clients.Http; public class ProposalsHttpClient : IProposalsClient { public HttpClient Client { get; } - public string ApiResource { get; } = "/api/console/proposals"; + public string ApiResource { get; } = "/api/proposals"; public ProposalsHttpClient(HttpClient client) { @@ -75,4 +75,10 @@ public async Task ScheduleProposalAsync(Guid proposalId, ScheduleProposalModel m throw new ApplicationException("Error scheduling proposal"); } } + + public async Task GetProposalsStatsAsync() + { + var proposalsStats = await Client.GetFromJsonAsync($"{ApiResource}/stats"); + return proposalsStats ?? new(0, 0); + } } diff --git a/src/KITT.Web.App/Clients/IProposalsClient.cs b/src/KITT.Proposals.Web.App/Clients/IProposalsClient.cs similarity index 75% rename from src/KITT.Web.App/Clients/IProposalsClient.cs rename to src/KITT.Proposals.Web.App/Clients/IProposalsClient.cs index 9870be5..3a955db 100644 --- a/src/KITT.Web.App/Clients/IProposalsClient.cs +++ b/src/KITT.Proposals.Web.App/Clients/IProposalsClient.cs @@ -1,6 +1,6 @@ -using KITT.Web.Models.Proposals; +using KITT.Proposals.Web.Models; -namespace KITT.Web.App.Clients; +namespace KITT.Proposals.Web.App.Clients; public interface IProposalsClient { @@ -15,4 +15,6 @@ public interface IProposalsClient Task GetProposalDetailAsync(Guid proposalId); Task ScheduleProposalAsync(Guid proposalId, ScheduleProposalModel model); + + Task GetProposalsStatsAsync(); } diff --git a/src/KITT.Proposals.Web.App/Components/ProposalDetailDialog.razor b/src/KITT.Proposals.Web.App/Components/ProposalDetailDialog.razor new file mode 100644 index 0000000..22735f3 --- /dev/null +++ b/src/KITT.Proposals.Web.App/Components/ProposalDetailDialog.razor @@ -0,0 +1,22 @@ +@implements IDialogContentComponent +@inject IStringLocalizer Localizer + + + + + @Localizer[nameof(Resources.Components.ProposalDetailDialog.AuthorLabel)]: + @model!.AuthorNickname + + + @Localizer[nameof(Resources.Components.ProposalDetailDialog.DescriptionLabel)] +
@model!.Description
+
+
+
+ + + @Localizer[nameof(Resources.Components.ProposalDetailDialog.CloseButtonText)] + + + diff --git a/src/KITT.Proposals.Web.App/Components/ProposalDetailDialog.razor.cs b/src/KITT.Proposals.Web.App/Components/ProposalDetailDialog.razor.cs new file mode 100644 index 0000000..26a89ba --- /dev/null +++ b/src/KITT.Proposals.Web.App/Components/ProposalDetailDialog.razor.cs @@ -0,0 +1,41 @@ +using KITT.Proposals.Web.App.Clients; +using KITT.Proposals.Web.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components; + +namespace KITT.Proposals.Web.App.Components; + +public partial class ProposalDetailDialog +{ + [CascadingParameter] + public FluentDialog Dialog { get; set; } = default!; + + [Parameter] + public InputModel Content { get; set; } = default!; + + [Inject] + public IProposalsClient Client { get; set; } = default!; + + private ProposalDetailModel? model; + + private bool loading = false; + + protected override async Task OnInitializedAsync() + { + loading = true; + + try + { + model = await Client.GetProposalDetailAsync(Content.ProposalId); + } + finally + { + loading = false; + } + } + + private async Task CloseAsync() => await Dialog.CancelAsync(); + + public record InputModel( + Guid ProposalId); +} diff --git a/src/KITT.Proposals.Web.App/Components/ProposalsStats.razor b/src/KITT.Proposals.Web.App/Components/ProposalsStats.razor new file mode 100644 index 0000000..d59abc1 --- /dev/null +++ b/src/KITT.Proposals.Web.App/Components/ProposalsStats.razor @@ -0,0 +1,45 @@ +@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false)) + +@inject KITT.Proposals.Web.App.Clients.IProposalsClient Client +@inject IStringLocalizer Localizer + + + + + + @model.NumberOfProposalAccepted + @Localizer[nameof(Resources.Components.ProposalsStats.ProposalsAcceptedLabel)] + + + + @model.TotalNumberOfProposals + @Localizer[nameof(Resources.Components.ProposalsStats.ProposalsLabel)] + + + + @Localizer[nameof(Resources.Components.ProposalsStats.ViewProposalsButtonText)] + + + + + +@code { + private ProposalsStatsModel model = default!; + + private bool loading = true; + + protected override async Task OnInitializedAsync() + { + loading = true; + + try + { + model = await Client.GetProposalsStatsAsync(); + } + finally + { + loading = false; + } + } +} diff --git a/src/KITT.Proposals.Web.App/KITT.Proposals.Web.App.csproj b/src/KITT.Proposals.Web.App/KITT.Proposals.Web.App.csproj new file mode 100644 index 0000000..1135d10 --- /dev/null +++ b/src/KITT.Proposals.Web.App/KITT.Proposals.Web.App.csproj @@ -0,0 +1,65 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + True + True + ProposalDetailDialog.resx + + + True + True + ProposalsStats.resx + + + True + True + Index.resx + + + + + + ResXFileCodeGenerator + + + ResXFileCodeGenerator + ProposalDetailDialog.Designer.cs + + + ResXFileCodeGenerator + + + ResXFileCodeGenerator + ProposalsStats.Designer.cs + + + PublicResXFileCodeGenerator + + + ResXFileCodeGenerator + Index.Designer.cs + + + + diff --git a/src/KITT.Proposals.Web.App/Pages/Index.razor b/src/KITT.Proposals.Web.App/Pages/Index.razor new file mode 100644 index 0000000..dbe501c --- /dev/null +++ b/src/KITT.Proposals.Web.App/Pages/Index.razor @@ -0,0 +1,106 @@ +@page "/proposals" + +@inject IStringLocalizer Localizer +@inject IStringLocalizer CommonLocalizer + +KITT - @Localizer[nameof(Resources.Pages.Index.PageTitle)] + + + @Localizer[nameof(Resources.Pages.Index.PageTitle)] + + + + + + + + + + + + + + + + + + + + + + + + @Localizer[nameof(Resources.Pages.Index.ProposalSearchLabel)] + + + + + + + + + + + + + + @Localizer[nameof(Resources.Pages.Index.NoProposalsMessage)] + + + + + + @proposal.SubmittedAt.ToShortDateString() + + + + @Localizer[proposal.Status.ToString()] + + + + + @Localizer[nameof(Resources.Pages.Index.ShowProposalDetailLabel)] + + + + + diff --git a/src/KITT.Proposals.Web.App/Pages/Index.razor.cs b/src/KITT.Proposals.Web.App/Pages/Index.razor.cs new file mode 100644 index 0000000..46e475a --- /dev/null +++ b/src/KITT.Proposals.Web.App/Pages/Index.razor.cs @@ -0,0 +1,196 @@ +using KITT.Proposals.Web.App.Clients; +using KITT.Proposals.Web.App.Components; +using KITT.Proposals.Web.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components; +using UI = KITT.Web.App.UI; + +namespace KITT.Proposals.Web.App.Pages; + +public partial class Index +{ + [Inject] + public IProposalsClient Client { get; set; } = default!; + + [Inject] + public NavigationManager Navigation { get; set; } = default!; + + [Inject] + public IDialogService DialogService { get; set; } = default!; + + [Inject] + public IToastService ToastService { get; set; } = default!; + + private ProposalsQueryModel query = new(); + + private ProposalListModel model = new(); + + private IQueryable proposals = new List().AsQueryable(); + + private bool loading = false; + + private Option[] sizes = [ + new() { Value = 5, Text = "5" }, + new() { Value = 10, Text = "10" }, + new() { Value = 25, Text = "25" }, + new() { Value = 50, Text = "50" } + ]; + + private Option[] directions = []; + + private Option[] statuses = []; + + protected override void OnInitialized() + { + directions = Enum.GetValues() + .Select(d => new Option { Value = d, Text = Localizer[d.ToString()] }) + .ToArray(); + + var validStatuses = Enum.GetValues() + .Select(s => new Option { Value = s, Text = Localizer[s.ToString()] }) + .ToArray(); + + statuses = [ + new Option { Value = null, Text = "" }, + ..validStatuses, + ]; + } + + private async Task SearchAsync() + { + await LoadProposalsAsync(query); + } + + private async Task ClearSearchAsync() + { + query = new(); + await LoadProposalsAsync(query); + } + + #region Proposal management will be refactored in a future iteration + //private async Task AcceptProposalAsync(ProposalListModel.ProposalListItemModel proposal) + //{ + // var proposalTitle = proposal.Title; + // string confirmText = Localizer[nameof(Resources.Pages.Index.AcceptProposalConfirmText), proposalTitle]; + + // var confirmDialog = await DialogService.ShowConfirmationAsync( + // confirmText, + // primaryText: CommonLocalizer[UI.Resources.Common.Confirm], + // secondaryText: CommonLocalizer[UI.Resources.Common.Cancel], + // title: Localizer[nameof(Resources.Pages.Index.AcceptProposalConfirmTitle)]); + + // var confirmResult = await confirmDialog.Result; + // if (!confirmResult.Cancelled) + // { + // try + // { + // await Client.AcceptProposalAsync(proposal.Id); + // ToastService.ShowSuccess( + // Localizer[nameof(Resources.Pages.Index.AcceptProposalSuccessMessage), proposalTitle]); + + // await LoadProposalsAsync(query); + // } + // catch + // { + // ToastService.ShowError( + // Localizer[nameof(Resources.Pages.Index.AcceptProposalErrorMessage), proposalTitle]); + // } + // } + //} + + //private async Task RejectProposalAsync(ProposalListModel.ProposalListItemModel proposal) + //{ + // var proposalTitle = proposal.Title; + // string confirmText = Localizer[nameof(Resources.Pages.Index.RejectProposalConfirmText), proposalTitle]; + + // var confirmDialog = await DialogService.ShowConfirmationAsync( + // confirmText, + // primaryText: CommonLocalizer[UI.Resources.Common.Confirm], + // secondaryText: CommonLocalizer[UI.Resources.Common.Cancel], + // title: Localizer[nameof(Resources.Pages.Index.RejectProposalConfirmTitle)]); + + // var confirmResult = await confirmDialog.Result; + // if (!confirmResult.Cancelled) + // { + // try + // { + // await Client.RejectProposalAsync(proposal.Id); + // ToastService.ShowSuccess( + // Localizer[nameof(Resources.Pages.Index.RejectProposalSuccessMessage), proposalTitle]); + + // await LoadProposalsAsync(query); + // } + // catch + // { + // ToastService.ShowError( + // Localizer[nameof(Resources.Pages.Index.RejectProposalErrorMessage), proposalTitle]); + // } + // } + //} + + //private async Task RefuseProposalAsync(ProposalListModel.ProposalListItemModel proposal) + //{ + // var proposalTitle = proposal.Title; + // string confirmText = Localizer[nameof(Resources.Pages.Index.RefuseProposalConfirmText), proposalTitle]; + + // var confirmDialog = await DialogService.ShowConfirmationAsync( + // confirmText, + // primaryText: CommonLocalizer[UI.Resources.Common.Confirm], + // secondaryText: CommonLocalizer[UI.Resources.Common.Cancel], + // title: Localizer[nameof(Resources.Pages.Index.RefuseProposalConfirmTitle)]); + + // var confirmResult = await confirmDialog.Result; + // if (!confirmResult.Cancelled) + // { + // try + // { + // await Client.RefuseProposalAsync(proposal.Id); + // ToastService.ShowSuccess( + // Localizer[nameof(Resources.Pages.Index.RefuseProposalSuccessMessage), proposalTitle]); + + // await LoadProposalsAsync(query); + // } + // catch + // { + // ToastService.ShowError( + // Localizer[nameof(Resources.Pages.Index.RefuseProposalErrorMessage), proposalTitle]); + // } + // } + //} + + //private void ScheduleProposal(ProposalListModel.ProposalListItemModel proposal) + //{ + // Navigation.NavigateTo($"proposals/schedule/{proposal.Id}"); + //} + #endregion + + private async Task LoadProposalsAsync(ProposalsQueryModel query) + { + loading = true; + + try + { + model = await Client.GetAllProposalsAsync(query); + proposals = model.Items.AsQueryable(); + } + finally + { + loading = false; + } + } + + private async Task OpenProposalDetailAsync(ProposalListModel.ProposalListItemModel proposal) + { + var content = new ProposalDetailDialog.InputModel(proposal.Id); + var dialog = await DialogService.ShowPanelAsync( + content, + new DialogParameters + { + Title = proposal.Title, + Alignment = HorizontalAlignment.Right, + Width = "40em" + }); + + await dialog.Result; + } +} \ No newline at end of file diff --git a/src/KITT.Proposals.Web.App/Resources/Components/ProposalDetailDialog.Designer.cs b/src/KITT.Proposals.Web.App/Resources/Components/ProposalDetailDialog.Designer.cs new file mode 100644 index 0000000..b31771e --- /dev/null +++ b/src/KITT.Proposals.Web.App/Resources/Components/ProposalDetailDialog.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace KITT.Proposals.Web.App.Resources.Components { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ProposalDetailDialog { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ProposalDetailDialog() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Proposals.Web.App.Resources.Components.ProposalDetailDialog", typeof(ProposalDetailDialog).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Author. + /// + internal static string AuthorLabel { + get { + return ResourceManager.GetString("AuthorLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Close. + /// + internal static string CloseButtonText { + get { + return ResourceManager.GetString("CloseButtonText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Description. + /// + internal static string DescriptionLabel { + get { + return ResourceManager.GetString("DescriptionLabel", resourceCulture); + } + } + } +} diff --git a/src/KITT.Proposals.Web.App/Resources/Components/ProposalDetailDialog.it.resx b/src/KITT.Proposals.Web.App/Resources/Components/ProposalDetailDialog.it.resx new file mode 100644 index 0000000..4ff0968 --- /dev/null +++ b/src/KITT.Proposals.Web.App/Resources/Components/ProposalDetailDialog.it.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Autore + + + Chiudi + + + Descrizione + + \ No newline at end of file diff --git a/src/KITT.Proposals.Web.App/Resources/Components/ProposalDetailDialog.resx b/src/KITT.Proposals.Web.App/Resources/Components/ProposalDetailDialog.resx new file mode 100644 index 0000000..49b9b96 --- /dev/null +++ b/src/KITT.Proposals.Web.App/Resources/Components/ProposalDetailDialog.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Author + + + Close + + + Description + + \ No newline at end of file diff --git a/src/KITT.Proposals.Web.App/Resources/Components/ProposalsStats.Designer.cs b/src/KITT.Proposals.Web.App/Resources/Components/ProposalsStats.Designer.cs new file mode 100644 index 0000000..c5442e5 --- /dev/null +++ b/src/KITT.Proposals.Web.App/Resources/Components/ProposalsStats.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace KITT.Proposals.Web.App.Resources.Components { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ProposalsStats { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ProposalsStats() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Proposals.Web.App.Resources.Components.ProposalsStats", typeof(ProposalsStats).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to proposals accepted. + /// + internal static string ProposalsAcceptedLabel { + get { + return ResourceManager.GetString("ProposalsAcceptedLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to proposals. + /// + internal static string ProposalsLabel { + get { + return ResourceManager.GetString("ProposalsLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to View all the proposals. + /// + internal static string ViewProposalsButtonText { + get { + return ResourceManager.GetString("ViewProposalsButtonText", resourceCulture); + } + } + } +} diff --git a/src/KITT.Proposals.Web.App/Resources/Components/ProposalsStats.it.resx b/src/KITT.Proposals.Web.App/Resources/Components/ProposalsStats.it.resx new file mode 100644 index 0000000..15c3b03 --- /dev/null +++ b/src/KITT.Proposals.Web.App/Resources/Components/ProposalsStats.it.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + proposte accettate + + + proposte + + + Guarda tutte le proposte + + diff --git a/src/KITT.Proposals.Web.App/Resources/Components/ProposalsStats.resx b/src/KITT.Proposals.Web.App/Resources/Components/ProposalsStats.resx new file mode 100644 index 0000000..9ef6674 --- /dev/null +++ b/src/KITT.Proposals.Web.App/Resources/Components/ProposalsStats.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + proposals accepted + + + proposals + + + View all the proposals + + \ No newline at end of file diff --git a/src/KITT.Web.App/Resources/Pages/Proposals/Index.Designer.cs b/src/KITT.Proposals.Web.App/Resources/Pages/Index.Designer.cs similarity index 81% rename from src/KITT.Web.App/Resources/Pages/Proposals/Index.Designer.cs rename to src/KITT.Proposals.Web.App/Resources/Pages/Index.Designer.cs index 162f2c6..eeb80e4 100644 --- a/src/KITT.Web.App/Resources/Pages/Proposals/Index.Designer.cs +++ b/src/KITT.Proposals.Web.App/Resources/Pages/Index.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Pages.Proposals { +namespace KITT.Proposals.Web.App.Resources.Pages { using System; @@ -22,7 +22,7 @@ namespace KITT.Web.App.Resources.Pages.Proposals { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Index { + internal class Index { private static global::System.Resources.ResourceManager resourceMan; @@ -36,10 +36,10 @@ internal Index() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { + internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Pages.Proposals.Index", typeof(Index).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Proposals.Web.App.Resources.Pages.Index", typeof(Index).Assembly); resourceMan = temp; } return resourceMan; @@ -51,7 +51,7 @@ internal Index() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal Index() { /// /// Looks up a localized string similar to You're going to accept the proposal {0}. Are you sure?. /// - public static string AcceptProposalConfirmText { + internal static string AcceptProposalConfirmText { get { return ResourceManager.GetString("AcceptProposalConfirmText", resourceCulture); } @@ -72,7 +72,7 @@ public static string AcceptProposalConfirmText { /// /// Looks up a localized string similar to Accept this proposal. /// - public static string AcceptProposalConfirmTitle { + internal static string AcceptProposalConfirmTitle { get { return ResourceManager.GetString("AcceptProposalConfirmTitle", resourceCulture); } @@ -81,7 +81,7 @@ public static string AcceptProposalConfirmTitle { /// /// Looks up a localized string similar to There was an error accepting the proposal {0}. /// - public static string AcceptProposalErrorMessage { + internal static string AcceptProposalErrorMessage { get { return ResourceManager.GetString("AcceptProposalErrorMessage", resourceCulture); } @@ -90,7 +90,7 @@ public static string AcceptProposalErrorMessage { /// /// Looks up a localized string similar to The proposal {0} has been accepted successfully!. /// - public static string AcceptProposalSuccessMessage { + internal static string AcceptProposalSuccessMessage { get { return ResourceManager.GetString("AcceptProposalSuccessMessage", resourceCulture); } @@ -99,7 +99,7 @@ public static string AcceptProposalSuccessMessage { /// /// Looks up a localized string similar to Older. /// - public static string Ascending { + internal static string Ascending { get { return ResourceManager.GetString("Ascending", resourceCulture); } @@ -108,7 +108,7 @@ public static string Ascending { /// /// Looks up a localized string similar to Newer. /// - public static string Descending { + internal static string Descending { get { return ResourceManager.GetString("Descending", resourceCulture); } @@ -117,7 +117,7 @@ public static string Descending { /// /// Looks up a localized string similar to Moderating. /// - public static string Moderating { + internal static string Moderating { get { return ResourceManager.GetString("Moderating", resourceCulture); } @@ -126,7 +126,7 @@ public static string Moderating { /// /// Looks up a localized string similar to There aren't any proposals loaded. Please try with some different search terms. /// - public static string NoProposalsMessage { + internal static string NoProposalsMessage { get { return ResourceManager.GetString("NoProposalsMessage", resourceCulture); } @@ -135,7 +135,7 @@ public static string NoProposalsMessage { /// /// Looks up a localized string similar to Manage the proposals. /// - public static string PageTitle { + internal static string PageTitle { get { return ResourceManager.GetString("PageTitle", resourceCulture); } @@ -144,7 +144,7 @@ public static string PageTitle { /// /// Looks up a localized string similar to Accept. /// - public static string ProposalAcceptLabel { + internal static string ProposalAcceptLabel { get { return ResourceManager.GetString("ProposalAcceptLabel", resourceCulture); } @@ -153,7 +153,7 @@ public static string ProposalAcceptLabel { /// /// Looks up a localized string similar to Author nickname. /// - public static string ProposalAuthorLabel { + internal static string ProposalAuthorLabel { get { return ResourceManager.GetString("ProposalAuthorLabel", resourceCulture); } @@ -162,7 +162,7 @@ public static string ProposalAuthorLabel { /// /// Looks up a localized string similar to Date. /// - public static string ProposalDateLabel { + internal static string ProposalDateLabel { get { return ResourceManager.GetString("ProposalDateLabel", resourceCulture); } @@ -171,7 +171,7 @@ public static string ProposalDateLabel { /// /// Looks up a localized string similar to Description. /// - public static string ProposalDescriptionLabel { + internal static string ProposalDescriptionLabel { get { return ResourceManager.GetString("ProposalDescriptionLabel", resourceCulture); } @@ -180,7 +180,7 @@ public static string ProposalDescriptionLabel { /// /// Looks up a localized string similar to Refuse. /// - public static string ProposalRefuseLabel { + internal static string ProposalRefuseLabel { get { return ResourceManager.GetString("ProposalRefuseLabel", resourceCulture); } @@ -189,7 +189,7 @@ public static string ProposalRefuseLabel { /// /// Looks up a localized string similar to Reject. /// - public static string ProposalRejectLabel { + internal static string ProposalRejectLabel { get { return ResourceManager.GetString("ProposalRejectLabel", resourceCulture); } @@ -198,7 +198,7 @@ public static string ProposalRejectLabel { /// /// Looks up a localized string similar to Schedule. /// - public static string ProposalScheduleLabel { + internal static string ProposalScheduleLabel { get { return ResourceManager.GetString("ProposalScheduleLabel", resourceCulture); } @@ -207,7 +207,7 @@ public static string ProposalScheduleLabel { /// /// Looks up a localized string similar to Search. /// - public static string ProposalSearchLabel { + internal static string ProposalSearchLabel { get { return ResourceManager.GetString("ProposalSearchLabel", resourceCulture); } @@ -216,7 +216,7 @@ public static string ProposalSearchLabel { /// /// Looks up a localized string similar to Order. /// - public static string ProposalsQueryOrderLabel { + internal static string ProposalsQueryOrderLabel { get { return ResourceManager.GetString("ProposalsQueryOrderLabel", resourceCulture); } @@ -225,7 +225,7 @@ public static string ProposalsQueryOrderLabel { /// /// Looks up a localized string similar to Search for.... /// - public static string ProposalsQuerySearchLabel { + internal static string ProposalsQuerySearchLabel { get { return ResourceManager.GetString("ProposalsQuerySearchLabel", resourceCulture); } @@ -234,7 +234,7 @@ public static string ProposalsQuerySearchLabel { /// /// Looks up a localized string similar to Nr. of elements. /// - public static string ProposalsQuerySizeLabel { + internal static string ProposalsQuerySizeLabel { get { return ResourceManager.GetString("ProposalsQuerySizeLabel", resourceCulture); } @@ -243,7 +243,7 @@ public static string ProposalsQuerySizeLabel { /// /// Looks up a localized string similar to Status. /// - public static string ProposalStatusLabel { + internal static string ProposalStatusLabel { get { return ResourceManager.GetString("ProposalStatusLabel", resourceCulture); } @@ -252,7 +252,7 @@ public static string ProposalStatusLabel { /// /// Looks up a localized string similar to Title. /// - public static string ProposalTitleLabel { + internal static string ProposalTitleLabel { get { return ResourceManager.GetString("ProposalTitleLabel", resourceCulture); } @@ -261,7 +261,7 @@ public static string ProposalTitleLabel { /// /// Looks up a localized string similar to You're going to refuse the proposal {0}. Are you sure?. /// - public static string RefuseProposalConfirmText { + internal static string RefuseProposalConfirmText { get { return ResourceManager.GetString("RefuseProposalConfirmText", resourceCulture); } @@ -270,7 +270,7 @@ public static string RefuseProposalConfirmText { /// /// Looks up a localized string similar to Refuse this proposal. /// - public static string RefuseProposalConfirmTitle { + internal static string RefuseProposalConfirmTitle { get { return ResourceManager.GetString("RefuseProposalConfirmTitle", resourceCulture); } @@ -279,7 +279,7 @@ public static string RefuseProposalConfirmTitle { /// /// Looks up a localized string similar to There was an error refusing the proposal {0}. /// - public static string RefuseProposalErrorMessage { + internal static string RefuseProposalErrorMessage { get { return ResourceManager.GetString("RefuseProposalErrorMessage", resourceCulture); } @@ -288,7 +288,7 @@ public static string RefuseProposalErrorMessage { /// /// Looks up a localized string similar to The proposal {0} has been refused successfully!. /// - public static string RefuseProposalSuccessMessage { + internal static string RefuseProposalSuccessMessage { get { return ResourceManager.GetString("RefuseProposalSuccessMessage", resourceCulture); } @@ -297,7 +297,7 @@ public static string RefuseProposalSuccessMessage { /// /// Looks up a localized string similar to You're going to reject the proposal {0}. Are you sure?. /// - public static string RejectProposalConfirmText { + internal static string RejectProposalConfirmText { get { return ResourceManager.GetString("RejectProposalConfirmText", resourceCulture); } @@ -306,7 +306,7 @@ public static string RejectProposalConfirmText { /// /// Looks up a localized string similar to Reject this proposal. /// - public static string RejectProposalConfirmTitle { + internal static string RejectProposalConfirmTitle { get { return ResourceManager.GetString("RejectProposalConfirmTitle", resourceCulture); } @@ -315,7 +315,7 @@ public static string RejectProposalConfirmTitle { /// /// Looks up a localized string similar to There was an error rejecting the proposal {0}. /// - public static string RejectProposalErrorMessage { + internal static string RejectProposalErrorMessage { get { return ResourceManager.GetString("RejectProposalErrorMessage", resourceCulture); } @@ -324,16 +324,25 @@ public static string RejectProposalErrorMessage { /// /// Looks up a localized string similar to The proposal {0} has been rejected successfully!. /// - public static string RejectProposalSuccessMessage { + internal static string RejectProposalSuccessMessage { get { return ResourceManager.GetString("RejectProposalSuccessMessage", resourceCulture); } } + /// + /// Looks up a localized string similar to Show. + /// + internal static string ShowProposalDetailLabel { + get { + return ResourceManager.GetString("ShowProposalDetailLabel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Waiting for approval. /// - public static string WaitingForApproval { + internal static string WaitingForApproval { get { return ResourceManager.GetString("WaitingForApproval", resourceCulture); } diff --git a/src/KITT.Web.App/Resources/Pages/Proposals/Index.it.resx b/src/KITT.Proposals.Web.App/Resources/Pages/Index.it.resx similarity index 98% rename from src/KITT.Web.App/Resources/Pages/Proposals/Index.it.resx rename to src/KITT.Proposals.Web.App/Resources/Pages/Index.it.resx index da676ad..c2aeda7 100644 --- a/src/KITT.Web.App/Resources/Pages/Proposals/Index.it.resx +++ b/src/KITT.Proposals.Web.App/Resources/Pages/Index.it.resx @@ -165,6 +165,9 @@ Pianifica + + Cerca + Ordinamento @@ -204,10 +207,10 @@ La proposta {0} è stata rimossa correttamente! + + Mostra + In attesa di approvazione - - Cerca - \ No newline at end of file diff --git a/src/KITT.Web.App/Resources/Pages/Proposals/Index.resx b/src/KITT.Proposals.Web.App/Resources/Pages/Index.resx similarity index 98% rename from src/KITT.Web.App/Resources/Pages/Proposals/Index.resx rename to src/KITT.Proposals.Web.App/Resources/Pages/Index.resx index 382f544..ba2aae3 100644 --- a/src/KITT.Web.App/Resources/Pages/Proposals/Index.resx +++ b/src/KITT.Proposals.Web.App/Resources/Pages/Index.resx @@ -165,6 +165,9 @@ Schedule + + Search + Order @@ -204,10 +207,10 @@ The proposal {0} has been rejected successfully! + + Show + Waiting for approval - - Search - \ No newline at end of file diff --git a/src/KITT.Proposals.Web.App/_Imports.razor b/src/KITT.Proposals.Web.App/_Imports.razor new file mode 100644 index 0000000..4a7e9c3 --- /dev/null +++ b/src/KITT.Proposals.Web.App/_Imports.razor @@ -0,0 +1,17 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Sections +@using Microsoft.Extensions.Localization +@using Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.JSInterop +@using KITT.Web.App.UI.Components +@using KITT.Web.App.UI +@using KITT.Proposals.Web.Models +@using UI = KITT.Web.App.UI +@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons + +@attribute [Authorize] \ No newline at end of file diff --git a/src/KITT.Proposals.Web.Models/KITT.Proposals.Web.Models.csproj b/src/KITT.Proposals.Web.Models/KITT.Proposals.Web.Models.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/src/KITT.Proposals.Web.Models/KITT.Proposals.Web.Models.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/src/KITT.Web.Models/Proposals/ProposalDetailModel.cs b/src/KITT.Proposals.Web.Models/ProposalDetailModel.cs similarity index 89% rename from src/KITT.Web.Models/Proposals/ProposalDetailModel.cs rename to src/KITT.Proposals.Web.Models/ProposalDetailModel.cs index 2158245..c6e71b1 100644 --- a/src/KITT.Web.Models/Proposals/ProposalDetailModel.cs +++ b/src/KITT.Proposals.Web.Models/ProposalDetailModel.cs @@ -1,4 +1,4 @@ -namespace KITT.Web.Models.Proposals; +namespace KITT.Proposals.Web.Models; public class ProposalDetailModel { diff --git a/src/KITT.Web.Models/Proposals/ProposalListModel.cs b/src/KITT.Proposals.Web.Models/ProposalListModel.cs similarity index 93% rename from src/KITT.Web.Models/Proposals/ProposalListModel.cs rename to src/KITT.Proposals.Web.Models/ProposalListModel.cs index 4379b16..09c68e8 100644 --- a/src/KITT.Web.Models/Proposals/ProposalListModel.cs +++ b/src/KITT.Proposals.Web.Models/ProposalListModel.cs @@ -1,4 +1,4 @@ -namespace KITT.Web.Models.Proposals; +namespace KITT.Proposals.Web.Models; public class ProposalListModel { diff --git a/src/KITT.Web.Models/Proposals/ProposalStatus.cs b/src/KITT.Proposals.Web.Models/ProposalStatus.cs similarity index 64% rename from src/KITT.Web.Models/Proposals/ProposalStatus.cs rename to src/KITT.Proposals.Web.Models/ProposalStatus.cs index 5f3347a..e2cc45a 100644 --- a/src/KITT.Web.Models/Proposals/ProposalStatus.cs +++ b/src/KITT.Proposals.Web.Models/ProposalStatus.cs @@ -1,4 +1,4 @@ -namespace KITT.Web.Models.Proposals; +namespace KITT.Proposals.Web.Models; public enum ProposalStatus { diff --git a/src/KITT.Web.Models/Proposals/ProposalsQueryModel.cs b/src/KITT.Proposals.Web.Models/ProposalsQueryModel.cs similarity index 95% rename from src/KITT.Web.Models/Proposals/ProposalsQueryModel.cs rename to src/KITT.Proposals.Web.Models/ProposalsQueryModel.cs index 655cd90..5c842bc 100644 --- a/src/KITT.Web.Models/Proposals/ProposalsQueryModel.cs +++ b/src/KITT.Proposals.Web.Models/ProposalsQueryModel.cs @@ -1,6 +1,6 @@ using System.Web; -namespace KITT.Web.Models.Proposals; +namespace KITT.Proposals.Web.Models; public record ProposalsQueryModel { diff --git a/src/KITT.Proposals.Web.Models/ProposalsStatsModel.cs b/src/KITT.Proposals.Web.Models/ProposalsStatsModel.cs new file mode 100644 index 0000000..70ad221 --- /dev/null +++ b/src/KITT.Proposals.Web.Models/ProposalsStatsModel.cs @@ -0,0 +1,5 @@ +namespace KITT.Proposals.Web.Models; + +public record ProposalsStatsModel( + int TotalNumberOfProposals, + int NumberOfProposalAccepted); diff --git a/src/KITT.Web.Models/Proposals/ScheduleProposalModel.cs b/src/KITT.Proposals.Web.Models/ScheduleProposalModel.cs similarity index 86% rename from src/KITT.Web.Models/Proposals/ScheduleProposalModel.cs rename to src/KITT.Proposals.Web.Models/ScheduleProposalModel.cs index 2cd9aaf..610907a 100644 --- a/src/KITT.Web.Models/Proposals/ScheduleProposalModel.cs +++ b/src/KITT.Proposals.Web.Models/ScheduleProposalModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace KITT.Web.Models.Proposals; +namespace KITT.Proposals.Web.Models; public record ScheduleProposalModel { @@ -24,5 +24,5 @@ public record ScheduleProposalModel public string? StreamingAbstract { get; set; } - public SeoData Seo { get; set; } = new(); + //public SeoData Seo { get; set; } = new(); } diff --git a/src/KITT.ServiceDefaults/Extensions.cs b/src/KITT.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..b34d762 --- /dev/null +++ b/src/KITT.ServiceDefaults/Extensions.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/src/KITT.ServiceDefaults/KITT.ServiceDefaults.csproj b/src/KITT.ServiceDefaults/KITT.ServiceDefaults.csproj new file mode 100644 index 0000000..16239b5 --- /dev/null +++ b/src/KITT.ServiceDefaults/KITT.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/src/KITT.Services/KITT.Services.csproj b/src/KITT.Services/KITT.Services.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/src/KITT.Services/KITT.Services.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/src/KITT.Services/ServiceNames.cs b/src/KITT.Services/ServiceNames.cs new file mode 100644 index 0000000..d83058b --- /dev/null +++ b/src/KITT.Services/ServiceNames.cs @@ -0,0 +1,15 @@ +namespace KITT.Services; + +public static class ServiceNames +{ + public const string Sql = "kitt-sql"; + public const string Database = "KittDatabase"; + + public const string CmsApi = "kitt-cms-api"; + + public const string ProposalsApi = "kitt-proposals-api"; + + public const string WebApp = "kitt-webapp"; + + public const string Seeder = "kitt-support-seeder"; +} diff --git a/src/KITT.Support.Seeder/KITT.Support.Seeder.csproj b/src/KITT.Support.Seeder/KITT.Support.Seeder.csproj new file mode 100644 index 0000000..92c7caf --- /dev/null +++ b/src/KITT.Support.Seeder/KITT.Support.Seeder.csproj @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + dotnet-KITT.Support.Seeder-7ba1aa34-63f1-4522-9240-26830e3d203a + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/src/KITT.Support.Seeder/Program.cs b/src/KITT.Support.Seeder/Program.cs new file mode 100644 index 0000000..cba8a62 --- /dev/null +++ b/src/KITT.Support.Seeder/Program.cs @@ -0,0 +1,15 @@ +using KITT.Core.Persistence; +using KITT.Services; +using KITT.Support.Seeder; + +var builder = Host.CreateApplicationBuilder(args); + +builder.AddServiceDefaults(); + +builder.AddSqlServerDbContext(ServiceNames.Database); +//builder.AddAzureKeyVaultClientWithEmulatorFallback("kitt-keyvault"); + +builder.Services.AddHostedService(); + +var host = builder.Build(); +host.Run(); diff --git a/src/KITT.Support.Seeder/Properties/launchSettings.json b/src/KITT.Support.Seeder/Properties/launchSettings.json new file mode 100644 index 0000000..8b6d658 --- /dev/null +++ b/src/KITT.Support.Seeder/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "KITT.Support.Seeder": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/KITT.Support.Seeder/Worker.cs b/src/KITT.Support.Seeder/Worker.cs new file mode 100644 index 0000000..f0d1b03 --- /dev/null +++ b/src/KITT.Support.Seeder/Worker.cs @@ -0,0 +1,169 @@ +using Azure; +using Azure.Security.KeyVault.Secrets; +using KITT.Core.Persistence; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using System.Diagnostics; + +namespace KITT.Support.Seeder; + +public class Worker : BackgroundService +{ + public const string ActivitySourceName = "KITT.Support.Seeder"; + + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly IServiceScopeFactory _serviceScopeFactory; + + private static readonly ActivitySource _activitySource = new(ActivitySourceName); + + public Worker( + ILogger logger, + IConfiguration configuration, + IHostApplicationLifetime hostApplicationLifetime, + IServiceScopeFactory serviceScopeFactory) + { + _logger = logger; + _configuration = configuration; + _hostApplicationLifetime = hostApplicationLifetime; + _serviceScopeFactory = serviceScopeFactory; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + //await SetupSecretsAsync(stoppingToken); + await SetupDatabaseAsync(stoppingToken); + + _hostApplicationLifetime.StopApplication(); + } + + private async Task SetupDatabaseAsync(CancellationToken stoppingToken) + { + using var activity = _activitySource.StartActivity("Setup database"); + + using var scope = _serviceScopeFactory.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + await EnsureDatabaseIsCreatedAsync(context, stoppingToken); + + _logger.LogInformation("Applying all pending migrations..."); + await context.Database.MigrateAsync(stoppingToken).ConfigureAwait(false); + _logger.LogInformation("Migrations applied correctly!"); + + await CreateSecurityCacheAsync(context, stoppingToken); + } + + private async Task CreateSecurityCacheAsync(KittDbContext context, CancellationToken stoppingToken) + { + try + { + var securityCacheSqlString = """ + IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'SecurityCache') + BEGIN + CREATE TABLE [dbo].[SecurityCache] ( + [Id] NVARCHAR (449) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL, + [Value] VARBINARY (MAX) NOT NULL, + [ExpiresAtTime] DATETIMEOFFSET (7) NOT NULL, + [SlidingExpirationInSeconds] BIGINT NULL, + [AbsoluteExpiration] DATETIMEOFFSET (7) NULL, + PRIMARY KEY CLUSTERED ([Id] ASC) + ); + END + """; + + _logger.LogInformation("Creating security cache table"); + await context.Database.ExecuteSqlRawAsync(securityCacheSqlString).ConfigureAwait(false); + _logger.LogInformation("Security cache table created successfully"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error creating SecurityCache table: {ErrorMessage}", ex.Message); + return; + } + + try + { + var expiresAtTimeIndexSqlString = """ + IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = 'Index_ExpiresAtTime' + AND object_id = OBJECT_ID('dbo.SecurityCache') + ) + BEGIN + CREATE NONCLUSTERED INDEX [Index_ExpiresAtTime] + ON [dbo].[SecurityCache]([ExpiresAtTime] ASC); + END + """; + + _logger.LogInformation("Creating index on ExpiresAtTime table"); + await context.Database.ExecuteSqlRawAsync(expiresAtTimeIndexSqlString).ConfigureAwait(false); + _logger.LogInformation("Index created successfully"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error creating index on ExpiresAtTime column: {ErrorMessage}", ex.Message); + } + } + + private async Task EnsureDatabaseIsCreatedAsync(KittDbContext context, CancellationToken stoppingToken) + { + var creator = context.GetService(); + if (await creator.ExistsAsync(stoppingToken)) + { + _logger.LogInformation("Database already exist!"); + return; + } + + _logger.LogInformation("Creating database..."); + await creator.CreateAsync(stoppingToken); + _logger.LogInformation("Database created successfully."); + } + + //private async Task SetupSecretsAsync(CancellationToken stoppingToken) + //{ + // using var activity = _activitySource.StartActivity("Setup secrets"); + + // try + // { + // if (await ExistsSecretAsync("Identity__WebApp__AppSecret", stoppingToken)) + // { + // return; + // } + + // await _secretClient.SetSecretAsync( + // "Identity__WebApp__AppSecret", + // _configuration["Identity:WebApp:AppSecret"], + // cancellationToken: stoppingToken); + // } + // catch (Exception ex) + // { + // activity?.AddException(ex); + // throw; + // } + + //} + + //private async Task ExistsSecretAsync(string secretName, CancellationToken cancellationToken) + //{ + // try + // { + // var webAppSecretResponse = await _secretClient.GetSecretAsync("Identity__WebApp__AppSecret"); + // if (webAppSecretResponse.Value is not null) + // { + // _logger.LogInformation("WebApp secret already set"); + // return true; + // } + + // return false; + // } + // catch (RequestFailedException ex) when (ex.Status == StatusCodes.Status404NotFound) + // { + // _logger.LogWarning(ex, "Secret not found"); + // return false; + // } + //} +} diff --git a/src/KITT.Support.Seeder/appsettings.json b/src/KITT.Support.Seeder/appsettings.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/src/KITT.Support.Seeder/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/KITT.Telegram.Functions/KITT.Telegram.Functions.csproj b/src/KITT.Telegram.Functions/KITT.Telegram.Functions.csproj index c12bbc3..0b73a36 100644 --- a/src/KITT.Telegram.Functions/KITT.Telegram.Functions.csproj +++ b/src/KITT.Telegram.Functions/KITT.Telegram.Functions.csproj @@ -7,10 +7,10 @@ enable - - - - + + + + diff --git a/src/KITT.Telegram.Functions/StreamingNotificationFunction.cs b/src/KITT.Telegram.Functions/StreamingNotificationFunction.cs index 7277640..434cfc4 100644 --- a/src/KITT.Telegram.Functions/StreamingNotificationFunction.cs +++ b/src/KITT.Telegram.Functions/StreamingNotificationFunction.cs @@ -36,7 +36,7 @@ public async Task NotifyScheduledStreaming( { var messageText = MessageConverter.ToText(message); - var messageSent = await _telegramClient.SendTextMessageAsync( + var messageSent = await _telegramClient.SendMessage( _telegramOptions.ChatId, messageText, parseMode: ParseMode.MarkdownV2); @@ -67,7 +67,7 @@ public async Task NotifyCanceledStreaming( { var messageText = MessageConverter.ToText(message); - var messageSent = await _telegramClient.SendTextMessageAsync( + var messageSent = await _telegramClient.SendMessage( _telegramOptions.ChatId, messageText, parseMode: ParseMode.MarkdownV2); @@ -97,7 +97,7 @@ public async Task NotifyChangeSchedule( { var messageText = MessageConverter.ToText(message); - await _telegramClient.SendTextMessageAsync( + await _telegramClient.SendMessage( _telegramOptions.ChatId, messageText, parseMode: ParseMode.MarkdownV2); @@ -123,7 +123,7 @@ public async Task NotifyStreamingVideoUploaded( { var messageText = MessageConverter.ToText(message); - await _telegramClient.SendTextMessageAsync( + await _telegramClient.SendMessage( _telegramOptions.ChatId, messageText, parseMode: ParseMode.MarkdownV2); @@ -149,7 +149,7 @@ public async Task NotifyStreamingHostingChannelChanged( { var messageText = MessageConverter.ToText(message); - await _telegramClient.SendTextMessageAsync( + await _telegramClient.SendMessage( _telegramOptions.ChatId, messageText, parseMode: ParseMode.MarkdownV2); diff --git a/src/KITT.Telegram.Functions/TextNotificationFunction.cs b/src/KITT.Telegram.Functions/TextNotificationFunction.cs index f6963d4..fbb57c6 100644 --- a/src/KITT.Telegram.Functions/TextNotificationFunction.cs +++ b/src/KITT.Telegram.Functions/TextNotificationFunction.cs @@ -35,7 +35,7 @@ public async Task NotifyTextMessage( { var messageText = MessageConverter.ToText(message); - var messageSent = await _telegramClient.SendTextMessageAsync( + var messageSent = await _telegramClient.SendMessage( _telegramOptions.ChatId, messageText, parseMode: ParseMode.MarkdownV2); diff --git a/src/KITT.Telegram.Messages/KITT.Telegram.Messages.csproj b/src/KITT.Telegram.Messages/KITT.Telegram.Messages.csproj index 48c0521..75cd54f 100644 --- a/src/KITT.Telegram.Messages/KITT.Telegram.Messages.csproj +++ b/src/KITT.Telegram.Messages/KITT.Telegram.Messages.csproj @@ -7,9 +7,9 @@ - - - + + +
diff --git a/src/KITT.Web.App.UI/KITT.Web.App.UI.csproj b/src/KITT.Web.App.UI/KITT.Web.App.UI.csproj index 15d009c..72d738a 100644 --- a/src/KITT.Web.App.UI/KITT.Web.App.UI.csproj +++ b/src/KITT.Web.App.UI/KITT.Web.App.UI.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable @@ -12,10 +12,11 @@
- - - - + + + + + diff --git a/src/KITT.Web.App.UI/ServiceCollectionExtensions.cs b/src/KITT.Web.App.UI/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..5bed970 --- /dev/null +++ b/src/KITT.Web.App.UI/ServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.FluentUI.AspNetCore.Components; + +namespace KITT.Web.App.UI; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddDefaultServices(this IServiceCollection services) + { + services.AddLocalization(); + services.AddFluentUIComponents(options => + { + options.ValidateClassNames = false; + }); + + return services; + } +} diff --git a/src/KITT.Web.App/App.razor b/src/KITT.Web.App/App.razor deleted file mode 100644 index 4f0d478..0000000 --- a/src/KITT.Web.App/App.razor +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - @if (!context!.User!.Identity!.IsAuthenticated) - { - - } - else - { -

You are not authorized to access this resource.

- } -
-
-
- - -

Sorry, there's nothing at this address.

-
-
-
-
diff --git a/src/KITT.Web.App/Clients/ClientsServiceCollectionExtensions.cs b/src/KITT.Web.App/Clients/ClientsServiceCollectionExtensions.cs deleted file mode 100644 index 2864d29..0000000 --- a/src/KITT.Web.App/Clients/ClientsServiceCollectionExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using KITT.Web.App.Clients.Http; -using Microsoft.AspNetCore.Components.WebAssembly.Authentication; - -namespace KITT.Web.App.Clients; - -public static class ClientsServiceCollectionExtensions -{ - public static IServiceCollection AddConsoleClients(this IServiceCollection services, string clientBaseAddress) - { - services - .AddHttpClient(client => client.BaseAddress = new Uri(clientBaseAddress)) - .AddHttpMessageHandler(); - - services - .AddHttpClient(client => client.BaseAddress = new Uri(clientBaseAddress)) - .AddHttpMessageHandler(); - - services - .AddHttpClient(client => client.BaseAddress = new Uri(clientBaseAddress)) - .AddHttpMessageHandler(); - - services - .AddHttpClient(client => client.BaseAddress = new Uri(clientBaseAddress)) - .AddHttpMessageHandler(); - - services - .AddHttpClient(client => client.BaseAddress = new Uri(clientBaseAddress)) - .AddHttpMessageHandler(); - - return services; - } -} diff --git a/src/KITT.Web.App/Clients/Http/DashboardHttpClient.cs b/src/KITT.Web.App/Clients/Http/DashboardHttpClient.cs deleted file mode 100644 index 081d9b6..0000000 --- a/src/KITT.Web.App/Clients/Http/DashboardHttpClient.cs +++ /dev/null @@ -1,22 +0,0 @@ -using KITT.Web.Models.Dashboard; -using System.Net.Http.Json; - -namespace KITT.Web.App.Clients.Http; - -public class DashboardHttpClient : IDashboardClient -{ - public HttpClient Client { get; } - - public string ApiResource { get; } = "api/dashboard"; - - public DashboardHttpClient(HttpClient client) - { - Client = client ?? throw new ArgumentNullException(nameof(client)); - } - - public async Task GetDashboardStatsAsync() - { - var model = await Client.GetFromJsonAsync(ApiResource); - return model ?? new(); - } -} diff --git a/src/KITT.Web.App/Clients/Http/MessagesHttpClient.cs b/src/KITT.Web.App/Clients/Http/MessagesHttpClient.cs deleted file mode 100644 index 18ff8e7..0000000 --- a/src/KITT.Web.App/Clients/Http/MessagesHttpClient.cs +++ /dev/null @@ -1,21 +0,0 @@ -using KITT.Web.Models.Messages; -using System.Net.Http.Json; - -namespace KITT.Web.App.Clients.Http; - -public class MessagesHttpClient : IMessagesClient -{ - public HttpClient Client { get; } - - public string ApiResource { get; } = "/api/messages"; - - public MessagesHttpClient(HttpClient client) - { - Client = client ?? throw new ArgumentNullException(nameof(client)); - } - - public async Task SendMessageAsync(SendMessageModel message) - { - await Client.PostAsJsonAsync(ApiResource, message); - } -} diff --git a/src/KITT.Web.App/Clients/IDashboardClient.cs b/src/KITT.Web.App/Clients/IDashboardClient.cs deleted file mode 100644 index 9168462..0000000 --- a/src/KITT.Web.App/Clients/IDashboardClient.cs +++ /dev/null @@ -1,8 +0,0 @@ -using KITT.Web.Models.Dashboard; - -namespace KITT.Web.App.Clients; - -public interface IDashboardClient -{ - Task GetDashboardStatsAsync(); -} diff --git a/src/KITT.Web.App/Clients/IMessagesClient.cs b/src/KITT.Web.App/Clients/IMessagesClient.cs deleted file mode 100644 index b2ca4c6..0000000 --- a/src/KITT.Web.App/Clients/IMessagesClient.cs +++ /dev/null @@ -1,8 +0,0 @@ -using KITT.Web.Models.Messages; - -namespace KITT.Web.App.Clients; - -public interface IMessagesClient -{ - Task SendMessageAsync(SendMessageModel message); -} diff --git a/src/KITT.Web.App/Components/MarkdownEditor.razor b/src/KITT.Web.App/Components/MarkdownEditor.razor deleted file mode 100644 index a2fa414..0000000 --- a/src/KITT.Web.App/Components/MarkdownEditor.razor +++ /dev/null @@ -1,46 +0,0 @@ -@using System.Linq.Expressions - -@* - - - - - - - - -*@ - -@code { - [Parameter] - public string Value { get; set; } = string.Empty; - - [Parameter] - public EventCallback ValueChanged { get; set; } - - [Parameter] - public Expression> ValueExpression { get; set; } = default!; - - [Parameter] - [EditorRequired] - public string EditorLabel { get; set; } = string.Empty; - - [Parameter] - public string PreviewLabel { get; set; } = "Preview"; - - [Parameter] - [EditorRequired] - public string Id { get; set; } = string.Empty; - - [Parameter] - public bool Disabled { get; set; } = false; - - [Parameter] - public int Lines { get; set; } = 5; -} diff --git a/src/KITT.Web.App/KITT.Web.App.Client/Clients/ClientsServiceCollectionExtensions.cs b/src/KITT.Web.App/KITT.Web.App.Client/Clients/ClientsServiceCollectionExtensions.cs new file mode 100644 index 0000000..0fd30f1 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App.Client/Clients/ClientsServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +using KITT.Web.App.Client.Clients.Http; + +namespace KITT.Web.App.Client.Clients; + +public static class ClientsServiceCollectionExtensions +{ + public static IServiceCollection AddGeneralClients( + this IServiceCollection services, + string apiBaseUrl) + { + services + .AddHttpClient(client => client.BaseAddress = new(apiBaseUrl)); + + return services; + } +} diff --git a/src/KITT.Web.App/Clients/Http/SettingsHttpClient.cs b/src/KITT.Web.App/KITT.Web.App.Client/Clients/Http/SettingsHttpClient.cs similarity index 75% rename from src/KITT.Web.App/Clients/Http/SettingsHttpClient.cs rename to src/KITT.Web.App/KITT.Web.App.Client/Clients/Http/SettingsHttpClient.cs index 9232f35..893a04c 100644 --- a/src/KITT.Web.App/Clients/Http/SettingsHttpClient.cs +++ b/src/KITT.Web.App/KITT.Web.App.Client/Clients/Http/SettingsHttpClient.cs @@ -1,13 +1,13 @@ -using KITT.Web.Models.Settings; +using KITT.Cms.Web.Models.Settings; using System.Net.Http.Json; -namespace KITT.Web.App.Clients.Http; +namespace KITT.Web.App.Client.Clients.Http; public class SettingsHttpClient : ISettingsClient { public HttpClient Client { get; } - public string ApiResource { get; } = "/api/console/settings"; + public string ApiResource { get; } = "/api/cms/settings"; public SettingsHttpClient(HttpClient client) { @@ -19,7 +19,7 @@ public async Task CreateNewSettingsAsync(CreateNewSettingsModel model) var response = await Client.PostAsJsonAsync(ApiResource, model); if (!response.IsSuccessStatusCode) { - throw new ApplicationException("Error scheduling streaming"); + throw new ApplicationException("Error creating settings"); } } diff --git a/src/KITT.Web.App/Clients/ISettingsClient.cs b/src/KITT.Web.App/KITT.Web.App.Client/Clients/ISettingsClient.cs similarity index 66% rename from src/KITT.Web.App/Clients/ISettingsClient.cs rename to src/KITT.Web.App/KITT.Web.App.Client/Clients/ISettingsClient.cs index 1f1120d..6793cda 100644 --- a/src/KITT.Web.App/Clients/ISettingsClient.cs +++ b/src/KITT.Web.App/KITT.Web.App.Client/Clients/ISettingsClient.cs @@ -1,6 +1,6 @@ -using KITT.Web.Models.Settings; +using KITT.Cms.Web.Models.Settings; -namespace KITT.Web.App.Clients; +namespace KITT.Web.App.Client.Clients; public interface ISettingsClient { diff --git a/src/KITT.Web.App/Shared/LoginDisplay.razor b/src/KITT.Web.App/KITT.Web.App.Client/Components/LoginDisplay.razor similarity index 74% rename from src/KITT.Web.App/Shared/LoginDisplay.razor rename to src/KITT.Web.App/KITT.Web.App.Client/Components/LoginDisplay.razor index 7e98f4a..5187b6a 100644 --- a/src/KITT.Web.App/Shared/LoginDisplay.razor +++ b/src/KITT.Web.App/KITT.Web.App.Client/Components/LoginDisplay.razor @@ -18,17 +18,17 @@ @code{ private void BeginSignOut(/*MouseEventArgs args*/) { - Navigation.NavigateToLogout("authentication/logout"); + Navigation.NavigateTo("authentication/logout", forceLoad: true); } private string GetUserEmailAddress(System.Security.Claims.ClaimsPrincipal user) { - var emails = user.FindFirst(c => c.Type == "emails")?.Value ?? string.Empty; - if (string.IsNullOrEmpty(emails)) + var email = user.FindFirst(c => c.Type == "preferred_username")?.Value ?? string.Empty; + if (string.IsNullOrEmpty(email)) { return string.Empty; } - return System.Text.Json.JsonSerializer.Deserialize(emails)?.FirstOrDefault() ?? string.Empty; + return email; } } \ No newline at end of file diff --git a/src/KITT.Web.App/Components/MessageComposer.razor b/src/KITT.Web.App/KITT.Web.App.Client/Components/MessageComposer.razor similarity index 59% rename from src/KITT.Web.App/Components/MessageComposer.razor rename to src/KITT.Web.App/KITT.Web.App.Client/Components/MessageComposer.razor index 64d7df9..09d71a4 100644 --- a/src/KITT.Web.App/Components/MessageComposer.razor +++ b/src/KITT.Web.App/KITT.Web.App.Client/Components/MessageComposer.razor @@ -1,15 +1,17 @@ @inject IDialogService DialogService @inject IStringLocalizer Localizer - + + @Localizer[nameof(Resources.Components.MessageComposer.ComposeMessageButtonLabel)] + + +@* - - @Localizer[nameof(Resources.Components.MessageComposer.ComposeMessageButtonLabel)] - + - +*@ @code { private async Task OpenMessageEditorAsync() diff --git a/src/KITT.Web.App/Components/MessageEditorDialog.razor b/src/KITT.Web.App/KITT.Web.App.Client/Components/MessageEditorDialog.razor similarity index 95% rename from src/KITT.Web.App/Components/MessageEditorDialog.razor rename to src/KITT.Web.App/KITT.Web.App.Client/Components/MessageEditorDialog.razor index 956df91..9be347f 100644 --- a/src/KITT.Web.App/Components/MessageEditorDialog.razor +++ b/src/KITT.Web.App/KITT.Web.App.Client/Components/MessageEditorDialog.razor @@ -4,12 +4,12 @@ - + + Style="width:100%;" /> diff --git a/src/KITT.Web.App/Components/MessageEditorDialog.razor.cs b/src/KITT.Web.App/KITT.Web.App.Client/Components/MessageEditorDialog.razor.cs similarity index 85% rename from src/KITT.Web.App/Components/MessageEditorDialog.razor.cs rename to src/KITT.Web.App/KITT.Web.App.Client/Components/MessageEditorDialog.razor.cs index ae2c47f..f7a26c3 100644 --- a/src/KITT.Web.App/Components/MessageEditorDialog.razor.cs +++ b/src/KITT.Web.App/KITT.Web.App.Client/Components/MessageEditorDialog.razor.cs @@ -1,10 +1,9 @@ -using KITT.Web.App.Clients; using KITT.Web.Models.Messages; -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; +using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; -namespace KITT.Web.App.Components; +namespace KITT.Web.App.Client.Components; public partial class MessageEditorDialog { @@ -14,8 +13,8 @@ public partial class MessageEditorDialog [Inject] public IToastService ToastService { get; set; } = default!; - [Inject] - public IMessagesClient Client { get; set; } = default!; + //[Inject] + //public IMessagesClient Client { get; set; } = default!; private SendMessageModel model = new(); @@ -41,7 +40,7 @@ private async Task SendMessageAsync() return; } - await Client.SendMessageAsync(model); + //await Client.SendMessageAsync(model); ToastService.ShowSuccess(Localizer[nameof(Resources.Components.MessageEditorDialog.MessageSentSuccessMessage)]); await Dialog.CloseAsync(); diff --git a/src/KITT.Web.App/KITT.Web.App.Client/Components/RedirectToLogin.razor b/src/KITT.Web.App/KITT.Web.App.Client/Components/RedirectToLogin.razor new file mode 100644 index 0000000..5ae0094 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App.Client/Components/RedirectToLogin.razor @@ -0,0 +1,9 @@ +@inject NavigationManager NavigationManager + +@code { + protected override void OnInitialized() + { + var loginUrl = $"authentication/login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}"; + NavigationManager.NavigateTo(loginUrl, forceLoad: true); + } +} diff --git a/src/KITT.Web.App/KITT.Web.App.Client/KITT.Web.App.Client.csproj b/src/KITT.Web.App/KITT.Web.App.Client/KITT.Web.App.Client.csproj new file mode 100644 index 0000000..9d2a983 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App.Client/KITT.Web.App.Client.csproj @@ -0,0 +1,92 @@ + + + + net9.0 + enable + enable + true + Default + + + + + + + + + + + + + + + + + + + + True + True + MessageComposer.resx + + + True + True + MessageEditorDialog.resx + + + True + True + CreateNewSettings.resx + + + True + True + Settings.resx + + + True + True + NavMenu.resx + + + + + + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + MessageComposer.Designer.cs + + + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + MessageEditorDialog.Designer.cs + + + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + CreateNewSettings.Designer.cs + + + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + Settings.Designer.cs + + + PublicResXFileCodeGenerator + + + PublicResXFileCodeGenerator + NavMenu.Designer.cs + + + + diff --git a/src/KITT.Web.App/Shared/MainLayout.razor b/src/KITT.Web.App/KITT.Web.App.Client/Layout/MainLayout.razor similarity index 89% rename from src/KITT.Web.App/Shared/MainLayout.razor rename to src/KITT.Web.App/KITT.Web.App.Client/Layout/MainLayout.razor index 0456b7d..366c443 100644 --- a/src/KITT.Web.App/Shared/MainLayout.razor +++ b/src/KITT.Web.App/KITT.Web.App.Client/Layout/MainLayout.razor @@ -31,7 +31,14 @@
- @Body + @if (!RendererInfo.IsInteractive) + { + + } + else + { + @Body + }
diff --git a/src/KITT.Web.App/Shared/MainLayout.razor.css b/src/KITT.Web.App/KITT.Web.App.Client/Layout/MainLayout.razor.css similarity index 75% rename from src/KITT.Web.App/Shared/MainLayout.razor.css rename to src/KITT.Web.App/KITT.Web.App.Client/Layout/MainLayout.razor.css index c865427..e9459c5 100644 --- a/src/KITT.Web.App/Shared/MainLayout.razor.css +++ b/src/KITT.Web.App/KITT.Web.App.Client/Layout/MainLayout.razor.css @@ -27,14 +27,14 @@ main { text-decoration: none; } - .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; - } + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } - .top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; - } + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } @media (max-width: 640.98px) { .top-row:not(.auth) { @@ -68,11 +68,11 @@ main { z-index: 1; } - .top-row.auth ::deep a:first-child { - flex: 1; - text-align: right; - width: 0; - } + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } .top-row, article { padding-left: 2rem !important; diff --git a/src/KITT.Web.App/Shared/NavMenu.razor b/src/KITT.Web.App/KITT.Web.App.Client/Layout/NavMenu.razor similarity index 58% rename from src/KITT.Web.App/Shared/NavMenu.razor rename to src/KITT.Web.App/KITT.Web.App.Client/Layout/NavMenu.razor index fc48259..9e0636c 100644 --- a/src/KITT.Web.App/Shared/NavMenu.razor +++ b/src/KITT.Web.App/KITT.Web.App.Client/Layout/NavMenu.razor @@ -7,12 +7,12 @@ diff --git a/src/KITT.Web.App/KITT.Web.App.Client/Modules.cs b/src/KITT.Web.App/KITT.Web.App.Client/Modules.cs new file mode 100644 index 0000000..da87a0a --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App.Client/Modules.cs @@ -0,0 +1,11 @@ +using System.Reflection; + +namespace KITT.Web.App.Client; + +public static class Modules +{ + public static IEnumerable AdditionalAssemblies { get; } = [ + typeof(Cms.Web.App._Imports).Assembly, + typeof(Proposals.Web.App._Imports).Assembly, + ]; +} diff --git a/src/KITT.Web.App/Pages/CreateNewSettings.razor b/src/KITT.Web.App/KITT.Web.App.Client/Pages/CreateNewSettings.razor similarity index 96% rename from src/KITT.Web.App/Pages/CreateNewSettings.razor rename to src/KITT.Web.App/KITT.Web.App.Client/Pages/CreateNewSettings.razor index aaabfa7..823a4e2 100644 --- a/src/KITT.Web.App/Pages/CreateNewSettings.razor +++ b/src/KITT.Web.App/KITT.Web.App.Client/Pages/CreateNewSettings.razor @@ -1,13 +1,14 @@ @page "/settings/new" +@using KITT.Cms.Web.Models.Settings @inject ISettingsClient Client @inject NavigationManager Navigation -@inject IMessageService MessageService +@inject IMessageService MessageService @inject IStringLocalizer CommonLocalizer @inject IStringLocalizer Localizer -@attribute [Authorize] +@*@attribute [Authorize]*@ KITT - @Localizer[nameof(Resources.Pages.CreateNewSettings.PageTitle)] diff --git a/src/KITT.Web.App/KITT.Web.App.Client/Pages/Home.razor b/src/KITT.Web.App/KITT.Web.App.Client/Pages/Home.razor new file mode 100644 index 0000000..f1db68b --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App.Client/Pages/Home.razor @@ -0,0 +1,24 @@ +@page "/" + +KITT - Console + + + KITT - Console + + + + + + + + + + + + + + + + + + diff --git a/src/KITT.Web.App/Pages/Settings.razor b/src/KITT.Web.App/KITT.Web.App.Client/Pages/Settings.razor similarity index 94% rename from src/KITT.Web.App/Pages/Settings.razor rename to src/KITT.Web.App/KITT.Web.App.Client/Pages/Settings.razor index 1d33048..6e1c61b 100644 --- a/src/KITT.Web.App/Pages/Settings.razor +++ b/src/KITT.Web.App/KITT.Web.App.Client/Pages/Settings.razor @@ -1,10 +1,9 @@ @page "/settings" - +@using KITT.Cms.Web.Models.Settings @inject ISettingsClient Client @inject IStringLocalizer Localizer @inject NavigationManager Navigation - -@attribute [Authorize] +@*@attribute [Authorize]*@ KITT - @Localizer[nameof(Resources.Pages.Settings.PageTitle)] @@ -50,6 +49,7 @@ finally { loading = false; + StateHasChanged(); } }; } diff --git a/src/KITT.Web.App/KITT.Web.App.Client/Program.cs b/src/KITT.Web.App/KITT.Web.App.Client/Program.cs new file mode 100644 index 0000000..d6c0fd1 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App.Client/Program.cs @@ -0,0 +1,19 @@ +using KITT.Web.App.UI; +using KITT.Cms.Web.App.Clients; +using KITT.Proposals.Web.App.Clients; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using KITT.Web.App.Client.Clients; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +builder.Services.AddAuthorizationCore(); +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddAuthenticationStateDeserialization(); + +builder.Services.AddDefaultServices(); + +builder.Services.AddGeneralClients(builder.HostEnvironment.BaseAddress); +builder.Services.AddCmsClients(builder.HostEnvironment.BaseAddress); +builder.Services.AddProposalsClients(builder.HostEnvironment.BaseAddress); + +await builder.Build().RunAsync(); diff --git a/src/KITT.Web.App/Resources/Components/MessageComposer.Designer.cs b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageComposer.Designer.cs similarity index 94% rename from src/KITT.Web.App/Resources/Components/MessageComposer.Designer.cs rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageComposer.Designer.cs index 9b3cf41..7e4790f 100644 --- a/src/KITT.Web.App/Resources/Components/MessageComposer.Designer.cs +++ b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageComposer.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Components { +namespace KITT.Web.App.Client.Resources.Components { using System; @@ -39,7 +39,7 @@ internal MessageComposer() { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Components.MessageComposer", typeof(MessageComposer).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Client.Resources.Components.MessageComposer", typeof(MessageComposer).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/KITT.Web.App/Resources/Components/MessageComposer.it.resx b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageComposer.it.resx similarity index 100% rename from src/KITT.Web.App/Resources/Components/MessageComposer.it.resx rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageComposer.it.resx diff --git a/src/KITT.Web.App/Resources/Components/MessageComposer.resx b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageComposer.resx similarity index 100% rename from src/KITT.Web.App/Resources/Components/MessageComposer.resx rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageComposer.resx diff --git a/src/KITT.Web.App/Resources/Components/MessageEditorDialog.Designer.cs b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageEditorDialog.Designer.cs similarity index 94% rename from src/KITT.Web.App/Resources/Components/MessageEditorDialog.Designer.cs rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageEditorDialog.Designer.cs index b3ed56d..020dcb2 100644 --- a/src/KITT.Web.App/Resources/Components/MessageEditorDialog.Designer.cs +++ b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageEditorDialog.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Components { +namespace KITT.Web.App.Client.Resources.Components { using System; @@ -39,7 +39,7 @@ internal MessageEditorDialog() { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Components.MessageEditorDialog", typeof(MessageEditorDialog).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Client.Resources.Components.MessageEditorDialog", typeof(MessageEditorDialog).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/KITT.Web.App/Resources/Components/MessageEditorDialog.it.resx b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageEditorDialog.it.resx similarity index 100% rename from src/KITT.Web.App/Resources/Components/MessageEditorDialog.it.resx rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageEditorDialog.it.resx diff --git a/src/KITT.Web.App/Resources/Components/MessageEditorDialog.resx b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageEditorDialog.resx similarity index 100% rename from src/KITT.Web.App/Resources/Components/MessageEditorDialog.resx rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Components/MessageEditorDialog.resx diff --git a/src/KITT.Web.App/Resources/Pages/CreateNewSettings.Designer.cs b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/CreateNewSettings.Designer.cs similarity index 94% rename from src/KITT.Web.App/Resources/Pages/CreateNewSettings.Designer.cs rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/CreateNewSettings.Designer.cs index 7db7690..a0003d8 100644 --- a/src/KITT.Web.App/Resources/Pages/CreateNewSettings.Designer.cs +++ b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/CreateNewSettings.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Pages { +namespace KITT.Web.App.Client.Resources.Pages { using System; @@ -39,7 +39,7 @@ internal CreateNewSettings() { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Pages.CreateNewSettings", typeof(CreateNewSettings).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Client.Resources.Pages.CreateNewSettings", typeof(CreateNewSettings).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/KITT.Web.App/Resources/Pages/CreateNewSettings.it.resx b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/CreateNewSettings.it.resx similarity index 99% rename from src/KITT.Web.App/Resources/Pages/CreateNewSettings.it.resx rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/CreateNewSettings.it.resx index c109f37..c15b6b5 100644 --- a/src/KITT.Web.App/Resources/Pages/CreateNewSettings.it.resx +++ b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/CreateNewSettings.it.resx @@ -123,4 +123,4 @@ Canale Twitch - \ No newline at end of file + diff --git a/src/KITT.Web.App/Resources/Pages/CreateNewSettings.resx b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/CreateNewSettings.resx similarity index 100% rename from src/KITT.Web.App/Resources/Pages/CreateNewSettings.resx rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/CreateNewSettings.resx diff --git a/src/KITT.Web.App/Resources/Pages/Settings.Designer.cs b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/Settings.Designer.cs similarity index 95% rename from src/KITT.Web.App/Resources/Pages/Settings.Designer.cs rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/Settings.Designer.cs index 1efee2b..106da5f 100644 --- a/src/KITT.Web.App/Resources/Pages/Settings.Designer.cs +++ b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/Settings.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Pages { +namespace KITT.Web.App.Client.Resources.Pages { using System; @@ -39,7 +39,7 @@ internal Settings() { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Pages.Settings", typeof(Settings).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Client.Resources.Pages.Settings", typeof(Settings).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/KITT.Web.App/Resources/Pages/Settings.it.resx b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/Settings.it.resx similarity index 100% rename from src/KITT.Web.App/Resources/Pages/Settings.it.resx rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/Settings.it.resx diff --git a/src/KITT.Web.App/Resources/Pages/Settings.resx b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/Settings.resx similarity index 100% rename from src/KITT.Web.App/Resources/Pages/Settings.resx rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Pages/Settings.resx diff --git a/src/KITT.Web.App/Resources/Shared/NavMenu.Designer.cs b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Shared/NavMenu.Designer.cs similarity index 85% rename from src/KITT.Web.App/Resources/Shared/NavMenu.Designer.cs rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Shared/NavMenu.Designer.cs index 4bafed5..998bc40 100644 --- a/src/KITT.Web.App/Resources/Shared/NavMenu.Designer.cs +++ b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Shared/NavMenu.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace KITT.Web.App.Resources.Shared { +namespace KITT.Web.App.Client.Resources.Shared { using System; @@ -22,7 +22,7 @@ namespace KITT.Web.App.Resources.Shared { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class NavMenu { + public class NavMenu { private static global::System.Resources.ResourceManager resourceMan; @@ -36,10 +36,10 @@ internal NavMenu() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Resources.Shared.NavMenu", typeof(NavMenu).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KITT.Web.App.Client.Resources.Shared.NavMenu", typeof(NavMenu).Assembly); resourceMan = temp; } return resourceMan; @@ -51,7 +51,7 @@ internal NavMenu() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal NavMenu() { /// /// Looks up a localized string similar to Contents. /// - internal static string ContentsMenuLabel { + public static string ContentsMenuLabel { get { return ResourceManager.GetString("ContentsMenuLabel", resourceCulture); } @@ -72,7 +72,7 @@ internal static string ContentsMenuLabel { /// /// Looks up a localized string similar to Dashboard. /// - internal static string DashboardMenuLabel { + public static string DashboardMenuLabel { get { return ResourceManager.GetString("DashboardMenuLabel", resourceCulture); } @@ -81,7 +81,7 @@ internal static string DashboardMenuLabel { /// /// Looks up a localized string similar to Proposals. /// - internal static string ProposalsMenuLabel { + public static string ProposalsMenuLabel { get { return ResourceManager.GetString("ProposalsMenuLabel", resourceCulture); } @@ -90,7 +90,7 @@ internal static string ProposalsMenuLabel { /// /// Looks up a localized string similar to Settings. /// - internal static string SettingsMenuLabel { + public static string SettingsMenuLabel { get { return ResourceManager.GetString("SettingsMenuLabel", resourceCulture); } @@ -99,7 +99,7 @@ internal static string SettingsMenuLabel { /// /// Looks up a localized string similar to My streamings. /// - internal static string StreamingsMenuLabel { + public static string StreamingsMenuLabel { get { return ResourceManager.GetString("StreamingsMenuLabel", resourceCulture); } diff --git a/src/KITT.Web.App/Resources/Shared/NavMenu.it.resx b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Shared/NavMenu.it.resx similarity index 99% rename from src/KITT.Web.App/Resources/Shared/NavMenu.it.resx rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Shared/NavMenu.it.resx index a229e59..ba26043 100644 --- a/src/KITT.Web.App/Resources/Shared/NavMenu.it.resx +++ b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Shared/NavMenu.it.resx @@ -132,4 +132,4 @@ Dirette - \ No newline at end of file + diff --git a/src/KITT.Web.App/Resources/Shared/NavMenu.resx b/src/KITT.Web.App/KITT.Web.App.Client/Resources/Shared/NavMenu.resx similarity index 100% rename from src/KITT.Web.App/Resources/Shared/NavMenu.resx rename to src/KITT.Web.App/KITT.Web.App.Client/Resources/Shared/NavMenu.resx diff --git a/src/KITT.Web.App/KITT.Web.App.Client/Routes.razor b/src/KITT.Web.App/KITT.Web.App.Client/Routes.razor new file mode 100644 index 0000000..e47ae56 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App.Client/Routes.razor @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/KITT.Web.App/_Imports.razor b/src/KITT.Web.App/KITT.Web.App.Client/_Imports.razor similarity index 53% rename from src/KITT.Web.App/_Imports.razor rename to src/KITT.Web.App/KITT.Web.App.Client/_Imports.razor index 5a35e33..aaa9974 100644 --- a/src/KITT.Web.App/_Imports.razor +++ b/src/KITT.Web.App/KITT.Web.App.Client/_Imports.razor @@ -3,23 +3,20 @@ @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.Web.Virtualization -@using Microsoft.AspNetCore.Components.WebAssembly.Http -@using Microsoft.JSInterop -@using KITT.Web.App -@using KITT.Web.App.Shared -@using KITT.Web.Models.Streamings -@using KITT.Web.Models.Settings -@using KITT.Web.Models.Tools -@using Microsoft.AspNetCore.Authorization -@using Microsoft.AspNetCore.Components.Authorization -@using KITT.Web.App.Clients -@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Components.Sections @using Microsoft.Extensions.Localization -@using KITT.Web.App.Components -@using KITT.Web.App.UI.Components -@using KITT.Web.App.UI -@using KITT.Web.Models.Proposals -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication @using Microsoft.FluentUI.AspNetCore.Components -@using Microsoft.AspNetCore.Components.Sections \ No newline at end of file +@using Microsoft.JSInterop +@using KITT.Web.App.Client +@using KITT.Web.App.Client.Components +@using KITT.Web.App.Client.Clients +@using KITT.Cms.Web.Models.Streamings +@using KITT.Web.App.UI +@using KITT.Web.App.UI.Components +@using KITT.Web.Models.Dashboard +@using KITT.Cms.Web.Models.Settings +@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Authorization diff --git a/src/KITT.Web.App/KITT.Web.App.Client/wwwroot/appsettings.json b/src/KITT.Web.App/KITT.Web.App.Client/wwwroot/appsettings.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App.Client/wwwroot/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/KITT.Web.App/KITT.Web.App.csproj b/src/KITT.Web.App/KITT.Web.App.csproj deleted file mode 100644 index fdff918..0000000 --- a/src/KITT.Web.App/KITT.Web.App.csproj +++ /dev/null @@ -1,188 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - - - - - - - - - - - - - - True - True - MessageComposer.resx - - - True - True - MessageEditorDialog.resx - - - True - True - ScheduleForm.resx - - - True - True - CreateNewSettings.resx - - - True - True - Index.resx - - - True - True - Index.resx - - - True - True - Settings.resx - - - True - True - Import.resx - - - True - True - Index.resx - - - True - True - Schedule.resx - - - True - True - StreamingDetail.resx - - - True - True - NavMenu.resx - - - - - - PreserveNewest - - - PreserveNewest - - - - - - PublicResXFileCodeGenerator - - - PublicResXFileCodeGenerator - MessageComposer.Designer.cs - - - PublicResXFileCodeGenerator - - - PublicResXFileCodeGenerator - MessageEditorDialog.Designer.cs - - - PublicResXFileCodeGenerator - - - PublicResXFileCodeGenerator - ScheduleForm.Designer.cs - - - PublicResXFileCodeGenerator - - - PublicResXFileCodeGenerator - CreateNewSettings.Designer.cs - - - PublicResXFileCodeGenerator - - - PublicResXFileCodeGenerator - Index.Designer.cs - - - PublicResXFileCodeGenerator - - - PublicResXFileCodeGenerator - Index.Designer.cs - - - PublicResXFileCodeGenerator - - - PublicResXFileCodeGenerator - Settings.Designer.cs - - - ResXFileCodeGenerator - - - ResXFileCodeGenerator - Import.Designer.cs - - - PublicResXFileCodeGenerator - - - PublicResXFileCodeGenerator - Index.Designer.cs - - - PublicResXFileCodeGenerator - - - PublicResXFileCodeGenerator - Schedule.Designer.cs - - - ResXFileCodeGenerator - - - ResXFileCodeGenerator - StreamingDetail.Designer.cs - - - ResXFileCodeGenerator - - - ResXFileCodeGenerator - NavMenu.Designer.cs - - - - diff --git a/src/KITT.Web.App/KITT.Web.App/Components/App.razor b/src/KITT.Web.App/KITT.Web.App/Components/App.razor new file mode 100644 index 0000000..7ef8067 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App/Components/App.razor @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/KITT.Web.App/KITT.Web.App/Components/Pages/Error.razor b/src/KITT.Web.App/KITT.Web.App/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/src/KITT.Web.App/KITT.Web.App/Components/_Imports.razor b/src/KITT.Web.App/KITT.Web.App/Components/_Imports.razor new file mode 100644 index 0000000..977d7d9 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App/Components/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.JSInterop +@using KITT.Web.App +@using KITT.Web.App.Client +@using KITT.Web.App.Client.Layout +@using KITT.Web.App.Components diff --git a/src/KITT.Web.App/KITT.Web.App/Endpoints/AuthenticationEndpoints.cs b/src/KITT.Web.App/KITT.Web.App/Endpoints/AuthenticationEndpoints.cs new file mode 100644 index 0000000..4662852 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App/Endpoints/AuthenticationEndpoints.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace KITT.Web.App.Endpoints; + +public static class AuthenticationEndpoints +{ + public static IEndpointRouteBuilder MapAuthenticationEndpoints(this IEndpointRouteBuilder builder) + { + var authenticationGroup = builder.MapGroup("/authentication"); + + authenticationGroup + .MapGet("login", Login) + .WithName(nameof(Login)) + .AllowAnonymous(); + + authenticationGroup + .MapGet("logout", Logout) + .WithName(nameof(Logout)); + + return builder; + } + + private static async Task Logout( + HttpContext httpContext, + string? returnUrl) + { + await Task.CompletedTask; + return TypedResults.SignOut( + BuildAuthenticationProperties(returnUrl, httpContext), + [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]); + } + + private static async Task Login( + HttpContext httpContext, + string? returnUrl) + { + await Task.CompletedTask; + return TypedResults.Challenge(BuildAuthenticationProperties(returnUrl, httpContext)); + } + + private static AuthenticationProperties BuildAuthenticationProperties(string? returnUrl, HttpContext httpContext) + { + string pathBase = httpContext.Request.PathBase.Value ?? "/"; + + // Prevent open redirects. + if (string.IsNullOrEmpty(returnUrl)) + { + returnUrl = pathBase; + } + else if (!Uri.IsWellFormedUriString(returnUrl, UriKind.Relative)) + { + returnUrl = new Uri(returnUrl, UriKind.Absolute).PathAndQuery; + } + else if (returnUrl[0] != '/') + { + returnUrl = $"{pathBase}{returnUrl}"; + } + + return new AuthenticationProperties { RedirectUri = returnUrl }; + } +} diff --git a/src/KITT.Web.App/KITT.Web.App/Endpoints/CmsEndpoints.cs b/src/KITT.Web.App/KITT.Web.App/Endpoints/CmsEndpoints.cs new file mode 100644 index 0000000..5f972c3 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App/Endpoints/CmsEndpoints.cs @@ -0,0 +1,66 @@ +using KITT.Services; +using KITT.Web.App.Endpoints.ReverseProxy; + +namespace KITT.Web.App.Endpoints; + +internal static class CmsEndpoints +{ + public static IEndpointRouteBuilder MapCmsEndpoints(this IEndpointRouteBuilder builder) + { + builder.MapForwarder( + "/api/cms/streamings", + $"https+http://{ServiceNames.CmsApi}", + transformBuilder => transformBuilder.ConfigureWithTargetPath( + "/api/streamings", + GetScopes)) + .RequireAuthorization(); + + builder.MapForwarder( + "/api/cms/streamings/{id}", + $"https+http://{ServiceNames.CmsApi}", + transformBuilder => transformBuilder.ConfigureWithTargetPath( + "/api/streamings/{id}", + GetScopes)) + .RequireAuthorization(); + + builder.MapForwarder( + "/api/cms/streamings/import", + $"https+http://{ServiceNames.CmsApi}", + transformBuilder => transformBuilder.ConfigureWithTargetPath( + "/api/streamings/import", + GetScopes)) + .RequireAuthorization(); + + builder.MapForwarder( + "/api/cms/streamings/stats", + $"https+http://{ServiceNames.CmsApi}", + transformBuilder => transformBuilder.ConfigureWithTargetPath( + "/api/streamings/stats", + GetScopes)) + .RequireAuthorization(); + + builder.MapForwarder( + "/api/cms/settings", + $"https+http://{ServiceNames.CmsApi}", + transformBuilder => transformBuilder.ConfigureWithTargetPath( + "/api/settings", + GetScopes)) + .RequireAuthorization(); + + builder.MapForwarder( + "/api/cms/settings/{id}", + $"https+http://{ServiceNames.CmsApi}", + transformBuilder => transformBuilder.ConfigureWithTargetPath( + "/api/settings/{id}", + GetScopes)) + .RequireAuthorization(); + + return builder; + } + + private static IEnumerable GetScopes(IConfiguration configuration) + => [ + $"api://{configuration["Identity:Cms:AppId"]}/Cms.Read", + $"api://{configuration["Identity:Cms:AppId"]}/Cms.Write", + ]; +} diff --git a/src/KITT.Web.App/KITT.Web.App/Endpoints/ProposalEndpoints.cs b/src/KITT.Web.App/KITT.Web.App/Endpoints/ProposalEndpoints.cs new file mode 100644 index 0000000..8418680 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App/Endpoints/ProposalEndpoints.cs @@ -0,0 +1,51 @@ +using KITT.Services; +using KITT.Web.App.Endpoints.ReverseProxy; + +namespace KITT.Web.App.Endpoints; + +internal static class ProposalEndpoints +{ + public static IEndpointRouteBuilder MapProposalEndpoints(this IEndpointRouteBuilder builder) + { + builder.MapForwarder( + "/api/proposals", + $"https+http://{ServiceNames.ProposalsApi}", + transformBuilder => transformBuilder.ConfigureWithTargetPath( + "/api/proposals", + GetScopes)) + .RequireAuthorization(); + + builder.MapForwarder( + "/api/proposals/{id}", + $"https+http://{ServiceNames.ProposalsApi}", + transformBuilder => transformBuilder.ConfigureWithTargetPath( + "/api/proposals/{id}", + GetScopes)) + .RequireAuthorization(); + + builder.MapForwarder( + "/api/proposals/{id}/refuse", + $"https+http://{ServiceNames.ProposalsApi}", + transformBuilder => transformBuilder.ConfigureWithTargetPath( + "/api/proposals/{id}/refuse", + GetScopes)) + .RequireAuthorization(); + + builder.MapForwarder( + "/api/proposals/stats", + $"https+http://{ServiceNames.ProposalsApi}", + transformBuilder => transformBuilder.ConfigureWithTargetPath( + "/api/proposals/stats", + GetScopes)) + .RequireAuthorization(); + + return builder; + } + + private static IEnumerable GetScopes(IConfiguration configuration) + => [ + $"api://{configuration["Identity:Proposals:AppId"]}/Proposals.Read", + $"api://{configuration["Identity:Proposals:AppId"]}/Proposals.Write", + ]; + +} diff --git a/src/KITT.Web.App/KITT.Web.App/Endpoints/ReverseProxy/TransformBuilderContextExtensions.cs b/src/KITT.Web.App/KITT.Web.App/Endpoints/ReverseProxy/TransformBuilderContextExtensions.cs new file mode 100644 index 0000000..69097e7 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App/Endpoints/ReverseProxy/TransformBuilderContextExtensions.cs @@ -0,0 +1,35 @@ +using Microsoft.Identity.Web; +using Yarp.ReverseProxy.Transforms; +using Yarp.ReverseProxy.Transforms.Builder; + +namespace KITT.Web.App.Endpoints.ReverseProxy; + +public static class TransformBuilderContextExtensions +{ + public static void ConfigureWithTargetPath( + this TransformBuilderContext transformBuilder, + string targetPath, + Func> scopesResolver) + { + transformBuilder.AddRequestTransform(async transformContext => + { + transformContext.Path = new(targetPath); + + var tokenAcquisition = transformContext.HttpContext.RequestServices.GetRequiredService(); + var configuration = transformContext.HttpContext.RequestServices.GetRequiredService(); + var user = transformContext.HttpContext.User; + + var scopes = scopesResolver.Invoke(configuration) + ?? throw new IOException("No downstream API scopes!"); + try + { + var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes, user: user); + transformContext.ProxyRequest.Headers.Authorization = new("Bearer", accessToken); + } + catch (MicrosoftIdentityWebChallengeUserException) + { + transformContext.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; + } + }); + } +} diff --git a/src/KITT.Web.App/KITT.Web.App/KITT.Web.App.csproj b/src/KITT.Web.App/KITT.Web.App/KITT.Web.App.csproj new file mode 100644 index 0000000..c82e360 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App/KITT.Web.App.csproj @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + + diff --git a/src/KITT.Web.App/KITT.Web.App/Program.cs b/src/KITT.Web.App/KITT.Web.App/Program.cs new file mode 100644 index 0000000..c60f452 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App/Program.cs @@ -0,0 +1,107 @@ +using KITT.Web.App.Client; +using KITT.Web.App.Components; +using KITT.Web.App.Endpoints; +using KITT.Web.App.UI; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.TokenCacheProviders.Distributed; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +//builder.AddAzureKeyVaultSecretsWithEmulatorFallback("kitt-keyvault"); + +// Add services to the container. +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + options.CallbackPath = "/signin-oidc"; + options.ClientId = builder.Configuration["Identity:WebApp:AppId"]; + options.Domain = $"{builder.Configuration["Identity:DomainName"]}.onmicrosoft.com"; + options.Instance = "https://login.microsoftonline.com/"; + options.ResponseType = "code"; + options.TenantId = builder.Configuration["Identity:TenantId"]; + options.ClientSecret = builder.Configuration["Identity:WebApp:AppSecret"]; + options.SaveTokens = true; + + options.TokenValidationParameters.NameClaimType = "name"; + }) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDistributedTokenCaches(); + +builder.Services.Configure(options => +{ + options.Encrypt = !builder.Environment.IsDevelopment(); +}); + +builder.Services.AddDistributedSqlServerCache(options => +{ + options.ConnectionString = builder.Configuration.GetConnectionString("KittDatabase"); + options.SchemaName = "dbo"; + options.TableName = "SecurityCache"; + + options.ExpiredItemsDeletionInterval = TimeSpan.FromMinutes(5); +}); + +builder.Services + .AddOptions(OpenIdConnectDefaults.AuthenticationScheme) + .Configure(options => + { + options.Scope.Add(OpenIdConnectScope.OfflineAccess); + options.ClientSecret = builder.Configuration["Identity:WebApp:AppSecret"]; + options.SaveTokens = true; + + options.TokenValidationParameters.NameClaimType = "name"; + }); + +builder.Services.AddAuthorization(); +builder.Services.AddCascadingAuthenticationState(); + +builder.Services.AddMicrosoftIdentityConsentHandler(); + +builder.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents() + .AddAuthenticationStateSerialization(options => options.SerializeAllClaims = true); + +builder.Services.AddEndpointsApiExplorer(); + +builder.Services.AddDefaultServices(); +builder.Services.AddHttpForwarderWithServiceDiscovery(); +builder.Services.AddProblemDetails(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.UseExceptionHandler(); + +app.MapStaticAssets(); +app.UseAntiforgery(); + +app.MapRazorComponents() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies([typeof(KITT.Web.App.Client._Imports).Assembly, ..Modules.AdditionalAssemblies]); + +app + .MapAuthenticationEndpoints() + .MapCmsEndpoints() + .MapProposalEndpoints(); + +app.Run(); diff --git a/src/KITT.Web.App/KITT.Web.App/Properties/launchSettings.json b/src/KITT.Web.App/KITT.Web.App/Properties/launchSettings.json new file mode 100644 index 0000000..bf8491a --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:12595", + "sslPort": 44344 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5146", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7154;http://localhost:5146", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/src/KITT.Web.App/KITT.Web.App/appsettings.json b/src/KITT.Web.App/KITT.Web.App/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/KITT.Web.App/KITT.Web.App/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/KITT.Web.App/libman.json b/src/KITT.Web.App/KITT.Web.App/libman.json similarity index 100% rename from src/KITT.Web.App/libman.json rename to src/KITT.Web.App/KITT.Web.App/libman.json diff --git a/src/KITT.Web.App/wwwroot/css/app.css b/src/KITT.Web.App/KITT.Web.App/wwwroot/app.css similarity index 98% rename from src/KITT.Web.App/wwwroot/css/app.css rename to src/KITT.Web.App/KITT.Web.App/wwwroot/app.css index 533e6d9..ad51cc3 100644 --- a/src/KITT.Web.App/wwwroot/css/app.css +++ b/src/KITT.Web.App/KITT.Web.App/wwwroot/app.css @@ -1,4 +1,4 @@ -@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css'; +@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css'; body { --body-font: "Segoe UI Variable", "Segoe UI", sans-serif; diff --git a/src/KITT.Web.App/wwwroot/bootstrap/css/bootstrap-utilities.min.css b/src/KITT.Web.App/KITT.Web.App/wwwroot/bootstrap/css/bootstrap-utilities.min.css similarity index 100% rename from src/KITT.Web.App/wwwroot/bootstrap/css/bootstrap-utilities.min.css rename to src/KITT.Web.App/KITT.Web.App/wwwroot/bootstrap/css/bootstrap-utilities.min.css diff --git a/src/KITT.Web.App/wwwroot/bootstrap/css/bootstrap-utilities.min.css.map b/src/KITT.Web.App/KITT.Web.App/wwwroot/bootstrap/css/bootstrap-utilities.min.css.map similarity index 100% rename from src/KITT.Web.App/wwwroot/bootstrap/css/bootstrap-utilities.min.css.map rename to src/KITT.Web.App/KITT.Web.App/wwwroot/bootstrap/css/bootstrap-utilities.min.css.map diff --git a/src/KITT.Web.App/KITT.Web.App/wwwroot/favicon.ico b/src/KITT.Web.App/KITT.Web.App/wwwroot/favicon.ico new file mode 100644 index 0000000..e189d8e Binary files /dev/null and b/src/KITT.Web.App/KITT.Web.App/wwwroot/favicon.ico differ diff --git a/src/KITT.Web.App/Pages/Authentication.razor b/src/KITT.Web.App/Pages/Authentication.razor deleted file mode 100644 index 608dca9..0000000 --- a/src/KITT.Web.App/Pages/Authentication.razor +++ /dev/null @@ -1,7 +0,0 @@ -@page "/authentication/{action}" -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication - - -@code{ - [Parameter] public string Action { get; set; } = string.Empty; -} diff --git a/src/KITT.Web.App/Pages/Index.razor b/src/KITT.Web.App/Pages/Index.razor deleted file mode 100644 index a3192ea..0000000 --- a/src/KITT.Web.App/Pages/Index.razor +++ /dev/null @@ -1,64 +0,0 @@ -@page "/" - -@inject IStringLocalizer Localizer - -KITT - Console - - - - - KITT - Console - - - - - @if (model.StreamingStats is not null) - { - - - - @model.StreamingStats.NumberOfStreamingsScheduled - @Localizer[nameof(Resources.Pages.Index.LivesScheduledLabel)] - - - - @model.StreamingStats.NumberOfStreamingsDelivered - @Localizer[nameof(Resources.Pages.Index.LivesDeliveredLabel)] - - - - @Localizer[nameof(Resources.Pages.Index.ViewStreamingsButtonText)] - - - - } - - @if (model.ProposalStats is not null) - { - - - - @model.ProposalStats.NumberOfProposalAccepted - @Localizer[nameof(Resources.Pages.Index.ProposalsAcceptedLabel)] - - - - @model.ProposalStats.TotalNumberOfProposals - @Localizer[nameof(Resources.Pages.Index.ProposalsLabel)] - - - - @Localizer[nameof(Resources.Pages.Index.ViewProposalsButtonText)] - - - - } - - - - - - - diff --git a/src/KITT.Web.App/Pages/Index.razor.cs b/src/KITT.Web.App/Pages/Index.razor.cs deleted file mode 100644 index b2c1ae6..0000000 --- a/src/KITT.Web.App/Pages/Index.razor.cs +++ /dev/null @@ -1,29 +0,0 @@ -using KITT.Web.App.Clients; -using KITT.Web.Models.Dashboard; -using Microsoft.AspNetCore.Components; - -namespace KITT.Web.App.Pages; - -public partial class Index -{ - [Inject] - public IDashboardClient Client { get; set; } = default!; - - private DashboardModel model = new(); - - private bool loading = false; - - protected override async Task OnInitializedAsync() - { - loading = true; - - try - { - model = await Client.GetDashboardStatsAsync(); - } - finally - { - loading = false; - } - } -} diff --git a/src/KITT.Web.App/Pages/Proposals/Index.razor b/src/KITT.Web.App/Pages/Proposals/Index.razor deleted file mode 100644 index 75e9234..0000000 --- a/src/KITT.Web.App/Pages/Proposals/Index.razor +++ /dev/null @@ -1,230 +0,0 @@ -@page "/proposals" - -@inject IStringLocalizer Localizer -@inject IStringLocalizer CommonLocalizer - -@attribute [Authorize] - - - @Localizer[nameof(Resources.Pages.Proposals.Index.PageTitle)] - - - - - - - - - - - - - - - - - - - - - - - - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalSearchLabel)] - - - - - - - - - - - - - - @Localizer[nameof(Resources.Pages.Proposals.Index.NoProposalsMessage)] - - - - - - @proposal.SubmittedAt.ToShortDateString() - - - - @Localizer[proposal.Status.ToString()] - - - - @if (proposal.Status == ProposalStatus.Moderating) - { - - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalAcceptLabel)] - - - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalRefuseLabel)] - - } - else if (proposal.Status == ProposalStatus.WaitingForApproval) - { - - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalScheduleLabel)] - - - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalRejectLabel)] - - } - - - - - -@* - - - - - - @foreach (var sort in Enum.GetValues()) - { - @Localizer[sort.ToString()] - } - - - - - - @foreach (var status in Enum.GetValues()) - { - @Localizer[status.ToString()] - } - - - - - @foreach (var size in sizes) - { - @size - } - - - - - - - - - - - - - - - - - - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalTitleLabel)] - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalDescriptionLabel)] - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalDateLabel)] - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalAuthorLabel)] - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalStatusLabel)] -   - - - @context.Title - @context.Description - @context.SubmittedAt.ToShortDateString() - @context.AuthorNickname - @Localizer[context.Status.ToString()] - - - @if (context.Status == ProposalStatus.Moderating) - { - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalAcceptLabel)] - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalRefuseLabel)] - } - else if (context.Status == ProposalStatus.WaitingForApproval) - { - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalScheduleLabel)] - @Localizer[nameof(Resources.Pages.Proposals.Index.ProposalRejectLabel)] - } - - - - - @Localizer[nameof(Resources.Pages.Proposals.Index.NoProposalsMessage)] - - - - - - - - *@ diff --git a/src/KITT.Web.App/Pages/Proposals/Index.razor.cs b/src/KITT.Web.App/Pages/Proposals/Index.razor.cs deleted file mode 100644 index 2281b78..0000000 --- a/src/KITT.Web.App/Pages/Proposals/Index.razor.cs +++ /dev/null @@ -1,177 +0,0 @@ -using KITT.Web.App.Clients; -using KITT.Web.Models.Proposals; -using Microsoft.AspNetCore.Components; -using Microsoft.FluentUI.AspNetCore.Components; - -namespace KITT.Web.App.Pages.Proposals; - -public partial class Index -{ - [Inject] - public IProposalsClient Client { get; set; } = default!; - - [Inject] - public NavigationManager Navigation { get; set; } = default!; - - [Inject] - public IDialogService DialogService { get; set; } = default!; - - [Inject] - public IToastService ToastService { get; set; } = default!; - - private ProposalsQueryModel query = new(); - - private ProposalListModel model = new(); - - private IQueryable proposals = new List().AsQueryable(); - - private bool loading = false; - - private Option[] sizes = [ - new() { Value = 5, Text = "5" }, - new() { Value = 10, Text = "10" }, - new() { Value = 25, Text = "25" }, - new() { Value = 50, Text = "50" } - ]; - - private Option[] directions = []; - - private Option[] statuses = []; - - protected override void OnInitialized() - { - directions = Enum.GetValues() - .Select(d => new Option { Value = d, Text = Localizer[d.ToString()] }) - .ToArray(); - - var validStatuses = Enum.GetValues() - .Select (s => new Option { Value = s, Text = Localizer[s.ToString()] }) - .ToArray(); - - statuses = [ - new Option { Value = null, Text = "" }, - ..validStatuses, - ]; - } - - private async Task SearchAsync() - { - await LoadProposalsAsync(query); - } - - private async Task ClearSearchAsync() - { - query = new(); - await LoadProposalsAsync(query); - } - - private async Task AcceptProposalAsync(ProposalListModel.ProposalListItemModel proposal) - { - var proposalTitle = proposal.Title; - string confirmText = Localizer[nameof(Resources.Pages.Proposals.Index.AcceptProposalConfirmText), proposalTitle]; - - var confirmDialog = await DialogService.ShowConfirmationAsync( - confirmText, - primaryText: CommonLocalizer[UI.Resources.Common.Confirm], - secondaryText: CommonLocalizer[UI.Resources.Common.Cancel], - title: Localizer[nameof(Resources.Pages.Proposals.Index.AcceptProposalConfirmTitle)]); - - var confirmResult = await confirmDialog.Result; - if (!confirmResult.Cancelled) - { - try - { - await Client.AcceptProposalAsync(proposal.Id); - ToastService.ShowSuccess( - Localizer[nameof(Resources.Pages.Proposals.Index.AcceptProposalSuccessMessage), proposalTitle]); - - await LoadProposalsAsync(query); - } - catch - { - ToastService.ShowError( - Localizer[nameof(Resources.Pages.Proposals.Index.AcceptProposalErrorMessage), proposalTitle]); - } - } - } - - private async Task RejectProposalAsync(ProposalListModel.ProposalListItemModel proposal) - { - var proposalTitle = proposal.Title; - string confirmText = Localizer[nameof(Resources.Pages.Proposals.Index.RejectProposalConfirmText), proposalTitle]; - - var confirmDialog = await DialogService.ShowConfirmationAsync( - confirmText, - primaryText: CommonLocalizer[UI.Resources.Common.Confirm], - secondaryText: CommonLocalizer[UI.Resources.Common.Cancel], - title: Localizer[nameof(Resources.Pages.Proposals.Index.RejectProposalConfirmTitle)]); - - var confirmResult = await confirmDialog.Result; - if (!confirmResult.Cancelled) - { - try - { - await Client.RejectProposalAsync(proposal.Id); - ToastService.ShowSuccess( - Localizer[nameof(Resources.Pages.Proposals.Index.RejectProposalSuccessMessage), proposalTitle]); - - await LoadProposalsAsync(query); - } - catch - { - ToastService.ShowError( - Localizer[nameof(Resources.Pages.Proposals.Index.RejectProposalErrorMessage), proposalTitle]); - } - } - } - - private async Task RefuseProposalAsync(ProposalListModel.ProposalListItemModel proposal) - { - var proposalTitle = proposal.Title; - string confirmText = Localizer[nameof(Resources.Pages.Proposals.Index.RefuseProposalConfirmText), proposalTitle]; - - var confirmDialog = await DialogService.ShowConfirmationAsync( - confirmText, - primaryText: CommonLocalizer[UI.Resources.Common.Confirm], - secondaryText: CommonLocalizer[UI.Resources.Common.Cancel], - title: Localizer[nameof(Resources.Pages.Proposals.Index.RefuseProposalConfirmTitle)]); - - var confirmResult = await confirmDialog.Result; - if (!confirmResult.Cancelled) - { - try - { - await Client.RefuseProposalAsync(proposal.Id); - ToastService.ShowSuccess( - Localizer[nameof(Resources.Pages.Proposals.Index.RefuseProposalSuccessMessage), proposalTitle]); - - await LoadProposalsAsync(query); - } - catch - { - ToastService.ShowError( - Localizer[nameof(Resources.Pages.Proposals.Index.RefuseProposalErrorMessage), proposalTitle]); - } - } - } - - private void ScheduleProposal(ProposalListModel.ProposalListItemModel proposal) - { - Navigation.NavigateTo($"proposals/schedule/{proposal.Id}"); - } - - private async Task LoadProposalsAsync(ProposalsQueryModel query) - { - loading = true; - - try - { - model = await Client.GetAllProposalsAsync(query); - proposals = model.Items.AsQueryable(); - } - finally - { - loading = false; - } - } -} diff --git a/src/KITT.Web.App/Pages/Proposals/Schedule.razor b/src/KITT.Web.App/Pages/Proposals/Schedule.razor deleted file mode 100644 index b1710aa..0000000 --- a/src/KITT.Web.App/Pages/Proposals/Schedule.razor +++ /dev/null @@ -1,47 +0,0 @@ -@page "/proposals/schedule/{id:guid}" - -@inject IStringLocalizer CommonLocalizer -@inject IStringLocalizer Localizer - -@attribute [Authorize] - - - @Localizer[nameof(Resources.Pages.Streamings.Schedule.PageTitle)] - - - - @CommonLocalizer[nameof(UI.Resources.Common.Back)] - - - - - - - -@* - - - - - @if (isLoading) - { - - } - else - { - @if (!string.IsNullOrWhiteSpace(errorMessage)) - { - - @errorMessage - - - } - - - } - -*@ diff --git a/src/KITT.Web.App/Pages/Proposals/Schedule.razor.cs b/src/KITT.Web.App/Pages/Proposals/Schedule.razor.cs deleted file mode 100644 index 986be27..0000000 --- a/src/KITT.Web.App/Pages/Proposals/Schedule.razor.cs +++ /dev/null @@ -1,108 +0,0 @@ -using KITT.Web.App.Clients; -using KITT.Web.App.Components; -using KITT.Web.App.UI; -using KITT.Web.Models.Proposals; -using Microsoft.AspNetCore.Components; -using Microsoft.FluentUI.AspNetCore.Components; - -namespace KITT.Web.App.Pages.Proposals; - -public partial class Schedule -{ - [Parameter] - public Guid Id { get; set; } - - [Inject] - public IProposalsClient Client { get; set; } = default!; - - [Inject] - public NavigationManager Navigation { get; set; } = default!; - - [Inject] - public IMessageService MessageService { get; set; } = default!; - - private ScheduleForm.ViewModel model = new(); - - private bool isLoading = false; - - protected override void OnInitialized() - { - base.OnInitialized(); - isLoading = true; - } - - protected override async Task OnParametersSetAsync() - { - try - { - await base.OnParametersSetAsync(); - - var proposalDetail = await Client.GetProposalDetailAsync(Id); - if (proposalDetail is not null) - { - model = ConvertToViewModel(proposalDetail); - } - } - finally - { - isLoading = false; - } - } - - private ScheduleForm.ViewModel ConvertToViewModel(ProposalDetailModel proposalDetail) - { - return new ScheduleForm.ViewModel - { - Title = proposalDetail.Title, - StreamingAbstract = proposalDetail.Description - }; - } - - async Task ScheduleProposalAsync(ScheduleForm.ViewModel model) - { - try - { - var scheduleProposalModel = ConvertToApiModel(model); - - await Client.ScheduleProposalAsync(Id, scheduleProposalModel); - Navigation.NavigateTo("proposals"); - } - catch (Exception ex) - { - await MessageService.ShowMessageBarAsync( - ex.Message, - MessageIntent.Error, - SectionNames.MessagesTopSectionName); - } - } - - private ScheduleProposalModel ConvertToApiModel(ScheduleForm.ViewModel model) - { - if (model.ScheduleDate is null) - { - throw new ArgumentNullException(nameof(model.ScheduleDate)); - } - - if (model.StartingTime is null) - { - throw new ArgumentNullException(nameof(model.StartingTime)); - } - - if (model.EndingTime is null) - { - throw new ArgumentNullException(nameof(model.EndingTime)); - } - - return new ScheduleProposalModel - { - Title = model.Title, - ScheduleDate = model.ScheduleDate.Value, - EndingTime = model.EndingTime.Value.TimeOfDay, - HostingChannelUrl = $"https://www.twitch.tv/{model.HostingChannelUrl}", - Slug = model.Slug, - StartingTime = model.StartingTime.Value.TimeOfDay, - StreamingAbstract = model.StreamingAbstract, - Seo = model.Seo - }; - } -} diff --git a/src/KITT.Web.App/Program.cs b/src/KITT.Web.App/Program.cs deleted file mode 100644 index 9aa993c..0000000 --- a/src/KITT.Web.App/Program.cs +++ /dev/null @@ -1,38 +0,0 @@ -using KITT.Web.App; -using KITT.Web.App.Clients; -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Components.WebAssembly.Authentication; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using Microsoft.FluentUI.AspNetCore.Components; - -var builder = WebAssemblyHostBuilder.CreateDefault(args); -builder.RootComponents.Add("#app"); -builder.RootComponents.Add("head::after"); - -builder.Services.AddLocalization(); - -#region Fluent UI -builder.Services.AddFluentUIComponents(options => -{ - options.ValidateClassNames = false; -}); -#endregion - -builder.Services.AddConsoleClients(builder.HostEnvironment.BaseAddress); - -builder.Services - .AddHttpClient("KITT.Web.App.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) - .AddHttpMessageHandler(); - -builder.Services - .AddScoped(sp => sp.GetRequiredService().CreateClient("KITT.Web.App.ServerAPI")); - -builder.Services.AddMsalAuthentication(options => -{ - builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication); - options.ProviderOptions.DefaultAccessTokenScopes.Add($"https://{builder.Configuration["ApiApp:Tenant"]}/{builder.Configuration["ApiApp:AppId"]}/Api.Development"); - - options.ProviderOptions.LoginMode = "redirect"; -}); - -await builder.Build().RunAsync(); diff --git a/src/KITT.Web.App/Properties/launchSettings.json b/src/KITT.Web.App/Properties/launchSettings.json deleted file mode 100644 index 4e32565..0000000 --- a/src/KITT.Web.App/Properties/launchSettings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:56511", - "sslPort": 44385 - } - }, - "profiles": { - "KITT.Web.App": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/src/KITT.Web.App/Shared/NavMenu.razor.css b/src/KITT.Web.App/Shared/NavMenu.razor.css deleted file mode 100644 index acc5f9f..0000000 --- a/src/KITT.Web.App/Shared/NavMenu.razor.css +++ /dev/null @@ -1,62 +0,0 @@ -.navbar-toggler { - background-color: rgba(255, 255, 255, 0.1); -} - -.top-row { - height: 3.5rem; - background-color: rgba(0,0,0,0.4); -} - -.navbar-brand { - font-size: 1.1rem; -} - -.oi { - width: 2rem; - font-size: 1.1rem; - vertical-align: text-top; - top: -2px; -} - -.nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; -} - - .nav-item:first-of-type { - padding-top: 1rem; - } - - .nav-item:last-of-type { - padding-bottom: 1rem; - } - - .nav-item ::deep a { - color: #d7d7d7; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - } - -.nav-item ::deep a.active { - background-color: rgba(255,255,255,0.25); - color: white; -} - -.nav-item ::deep a:hover { - background-color: rgba(255,255,255,0.1); - color: white; -} - -@media (min-width: 641px) { - .navbar-toggler { - display: none; - } - - .collapse { - /* Never collapse the sidebar for wide screens */ - display: block; - } -} diff --git a/src/KITT.Web.App/Shared/RedirectToLogin.razor b/src/KITT.Web.App/Shared/RedirectToLogin.razor deleted file mode 100644 index 9dacb8c..0000000 --- a/src/KITT.Web.App/Shared/RedirectToLogin.razor +++ /dev/null @@ -1,8 +0,0 @@ -@inject NavigationManager Navigation -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@code { - protected override void OnInitialized() - { - Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}"); - } -} \ No newline at end of file diff --git a/src/KITT.Web.App/wwwroot/appsettings.json b/src/KITT.Web.App/wwwroot/appsettings.json deleted file mode 100644 index aea69a9..0000000 --- a/src/KITT.Web.App/wwwroot/appsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "AzureAdB2C": { - "Authority": "", - "ClientId": "", - "ValidateAuthority": false, - "LoginMode": "redirect" - }, - "ApiApp": { - "Tenant": "", - "AppId": "" - } -} \ No newline at end of file diff --git a/src/KITT.Web.App/wwwroot/index.html b/src/KITT.Web.App/wwwroot/index.html deleted file mode 100644 index 7e8d6a0..0000000 --- a/src/KITT.Web.App/wwwroot/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - KITT - Streaming tools - - - - - - - - - - - -
- - - - -
-
- -
- An unhandled error has occurred. - Reload - 🗙 -
- - - - - - - diff --git a/src/KITT.Web.Models/KITT.Web.Models.csproj b/src/KITT.Web.Models/KITT.Web.Models.csproj index 5d6e827..9623deb 100644 --- a/src/KITT.Web.Models/KITT.Web.Models.csproj +++ b/src/KITT.Web.Models/KITT.Web.Models.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable diff --git a/src/KITT.Web.Shared/Azure/AzureKeyVaultExtensions.cs b/src/KITT.Web.Shared/Azure/AzureKeyVaultExtensions.cs new file mode 100644 index 0000000..653edbb --- /dev/null +++ b/src/KITT.Web.Shared/Azure/AzureKeyVaultExtensions.cs @@ -0,0 +1,47 @@ +using Azure.Extensions.AspNetCore.Configuration.Secrets; +using AzureKeyVaultEmulator.Aspire.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace KITT.Web.Shared.Azure; + +public static class AzureKeyVaultExtensions +{ + public static IHostApplicationBuilder AddAzureKeyVaultClientWithEmulatorFallback(this IHostApplicationBuilder builder, string connectionStringName) + { + ArgumentNullException.ThrowIfNull(builder); + + if (builder.Environment.IsDevelopment()) + { + var vaultUri = builder.Configuration.GetConnectionString(connectionStringName); + builder.Services.AddAzureKeyVaultEmulator(vaultUri); + } + else + { + builder.AddAzureKeyVaultClient(connectionStringName); + } + + return builder; + } + + public static IHostApplicationBuilder AddAzureKeyVaultSecretsWithEmulatorFallback(this IHostApplicationBuilder builder, string connectionStringName) + { + ArgumentNullException.ThrowIfNull(builder); + + if (builder.Environment.IsDevelopment()) + { + var vaultUri = builder.Configuration.GetConnectionString(connectionStringName); + + var secretClient = KeyVaultHelper.GetSecretClient(vaultUri); + builder.Configuration.AddAzureKeyVault( + secretClient, + new AzureKeyVaultConfigurationOptions()); + } + else + { + builder.Configuration.AddAzureKeyVaultSecrets(connectionStringName); + } + + return builder; + } +} diff --git a/src/KITT.Web.Shared/KITT.Web.Shared.csproj b/src/KITT.Web.Shared/KITT.Web.Shared.csproj new file mode 100644 index 0000000..c44adeb --- /dev/null +++ b/src/KITT.Web.Shared/KITT.Web.Shared.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + diff --git a/src/KITT.Web.Shared/Security/ClaimsPrincipalExtensions.cs b/src/KITT.Web.Shared/Security/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000..a46fcd5 --- /dev/null +++ b/src/KITT.Web.Shared/Security/ClaimsPrincipalExtensions.cs @@ -0,0 +1,10 @@ +using Microsoft.Identity.Web; +using System.Security.Claims; + +namespace KITT.Web.Shared.Security; + +public static class ClaimsPrincipalExtensions +{ + public static string GetUserId(this ClaimsPrincipal user) + => user.GetObjectId()!; +} diff --git a/src/LemonBot.Clients/LemonBot.Clients.csproj b/src/LemonBot.Clients/LemonBot.Clients.csproj index 776dfc2..7714b8c 100644 --- a/src/LemonBot.Clients/LemonBot.Clients.csproj +++ b/src/LemonBot.Clients/LemonBot.Clients.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/LemonBot.Commands/LemonBot.Commands.csproj b/src/LemonBot.Commands/LemonBot.Commands.csproj index 9855eb6..c1839c2 100644 --- a/src/LemonBot.Commands/LemonBot.Commands.csproj +++ b/src/LemonBot.Commands/LemonBot.Commands.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/LemonBot.Web/Configuration/BotConfiguration.cs b/src/LemonBot.Web/Configuration/BotConfiguration.cs deleted file mode 100644 index 250e4c3..0000000 --- a/src/LemonBot.Web/Configuration/BotConfiguration.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace LemonBot.Web.Configuration; - -public record BotConfiguration -{ - public string Endpoint { get; set; } = string.Empty; - - public string Username { get; set; } = string.Empty; - - public string Password { get; set; } = string.Empty; -} diff --git a/src/LemonBot.Web/Endpoints/DashboardEndpoints.cs b/src/LemonBot.Web/Endpoints/DashboardEndpoints.cs deleted file mode 100644 index 98a0a1a..0000000 --- a/src/LemonBot.Web/Endpoints/DashboardEndpoints.cs +++ /dev/null @@ -1,31 +0,0 @@ -using KITT.Web.Models.Dashboard; -using LemonBot.Web.Endpoints.Services; -using Microsoft.AspNetCore.Http.HttpResults; -using System.Security.Claims; - -namespace LemonBot.Web.Endpoints; - -public static class DashboardEndpoints -{ - public static IEndpointRouteBuilder MapDashboardEndpoints(this IEndpointRouteBuilder builder) - { - var dashboardGroup = builder - .MapGroup("api/dashboard") - .RequireAuthorization(); - - dashboardGroup - .MapGet("/", GetDashboard); - - return builder; - } - - private static async Task> GetDashboard( - DashboardEndpointsServices services, - ClaimsPrincipal user) - { - var userId = user.GetUserId(); - - var model = await services.GetDashboardAsync(userId); - return TypedResults.Ok(model); - } -} diff --git a/src/LemonBot.Web/Endpoints/MessagesEndpoints.cs b/src/LemonBot.Web/Endpoints/MessagesEndpoints.cs deleted file mode 100644 index df41dc3..0000000 --- a/src/LemonBot.Web/Endpoints/MessagesEndpoints.cs +++ /dev/null @@ -1,31 +0,0 @@ -using KITT.Web.Models.Messages; -using LemonBot.Web.Endpoints.Services; -using Microsoft.AspNetCore.Mvc; - -namespace LemonBot.Web.Endpoints; - -public static class MessagesEndpoints -{ - public static IEndpointRouteBuilder MapMessagesEndpoints(this IEndpointRouteBuilder builder) - { - var messagesGroup = builder - .MapGroup("api/messages") - .RequireAuthorization() - .WithParameterValidation(); - - messagesGroup - .MapPost("", SendMessage) - .WithName(nameof(SendMessage)) - .WithOpenApi(); - - return builder; - } - - private static async Task SendMessage( - MessagesEndpointsServices services, - [FromBody] SendMessageModel model) - { - await services.SendMessageAsync(model); - return Results.Accepted(); - } -} diff --git a/src/LemonBot.Web/Endpoints/Services/DashboardEndpointsServices.cs b/src/LemonBot.Web/Endpoints/Services/DashboardEndpointsServices.cs deleted file mode 100644 index b590298..0000000 --- a/src/LemonBot.Web/Endpoints/Services/DashboardEndpointsServices.cs +++ /dev/null @@ -1,35 +0,0 @@ -using KITT.Core.ReadModels; -using KITT.Web.Models.Dashboard; -using Microsoft.EntityFrameworkCore; - -namespace LemonBot.Web.Endpoints.Services; - -public class DashboardEndpointsServices -{ - public IDatabase Database { get; } - - public DashboardEndpointsServices(IDatabase database) - { - Database = database ?? throw new ArgumentNullException(nameof(database)); - } - - public async Task GetDashboardAsync(string userId) - { - var streamingsQuery = Database.Streamings.ByUserId(userId).OrderedBySchedule(); - var deliveredStreamingsNumber = await streamingsQuery.DeliveredOnly().CountAsync(); - var scheduledStreamingsNumber = await streamingsQuery.Scheduled().CountAsync(); - - var proposalsQuery = Database.Proposals; - - var totalNumberOfProposals = await proposalsQuery.CountAsync(); - var acceptedProposalsNumber = await proposalsQuery.WaitingForApproval().CountAsync(); - - var model = new DashboardModel - { - StreamingStats = new(deliveredStreamingsNumber, scheduledStreamingsNumber), - ProposalStats = new(totalNumberOfProposals, acceptedProposalsNumber) - }; - - return model; - } -} diff --git a/src/LemonBot.Web/Endpoints/Services/MessagesEndpointsServices.cs b/src/LemonBot.Web/Endpoints/Services/MessagesEndpointsServices.cs deleted file mode 100644 index b683a48..0000000 --- a/src/LemonBot.Web/Endpoints/Services/MessagesEndpointsServices.cs +++ /dev/null @@ -1,20 +0,0 @@ -using KITT.Telegram.Messages; -using KITT.Web.Models.Messages; - -namespace LemonBot.Web.Endpoints.Services; - -public class MessagesEndpointsServices -{ - public IMessageBus MessageBus { get; } - - public MessagesEndpointsServices(IMessageBus messageBus) - { - MessageBus = messageBus ?? throw new ArgumentNullException(nameof(messageBus)); - } - - public async Task SendMessageAsync(SendMessageModel model) - { - var message = new SendTextMessage(model.Text); - await MessageBus.SendAsync(message); - } -} diff --git a/src/LemonBot.Web/Extensions/ClaimsPrincipalExtensions.cs b/src/LemonBot.Web/Extensions/ClaimsPrincipalExtensions.cs deleted file mode 100644 index ea6bd70..0000000 --- a/src/LemonBot.Web/Extensions/ClaimsPrincipalExtensions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Security.Claims; - -namespace LemonBot.Web.Extensions; - -public static class ClaimsPrincipalExtensions -{ - public static string GetUserId(this ClaimsPrincipal user) - => user.FindFirstValue(ClaimTypes.NameIdentifier)!; -} diff --git a/src/LemonBot.Web/Extensions/WebApplicationBuilderExtensions.cs b/src/LemonBot.Web/Extensions/WebApplicationBuilderExtensions.cs deleted file mode 100644 index 48b337b..0000000 --- a/src/LemonBot.Web/Extensions/WebApplicationBuilderExtensions.cs +++ /dev/null @@ -1,113 +0,0 @@ -using FluentValidation; -using Hellang.Middleware.ProblemDetails; -using KITT.Core.DependencyInjection; -using KITT.Core.Persistence; -using KITT.Telegram.Messages; -using LemonBot.Web.Configuration; -using LemonBot.Web.Endpoints.Services; -using LemonBot.Web.GraphQL; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.EntityFrameworkCore; -using Microsoft.Identity.Web; -using Microsoft.OpenApi.Any; - -namespace LemonBot.Web.Extensions; - -public static class WebApplicationBuilderExtensions -{ - public static WebApplicationBuilder ConfigureServices(this WebApplicationBuilder builder) - { - builder.Services.AddDbContext( - options => options.UseSqlServer(builder.Configuration.GetConnectionString("KittDatabase"))); - - builder.Services.AddDatabaseDeveloperPageExceptionFilter(); - - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(options => - { - options.MapType(() => new Microsoft.OpenApi.Models.OpenApiSchema - { - Type = "string", - Example = new OpenApiString("00:00:00") - }); - }); - - builder.Services.Configure(builder.Configuration.GetSection(nameof(BotConfiguration))); - - builder.Services.Configure( - JwtBearerDefaults.AuthenticationScheme, - options => - { - options.TokenValidationParameters.NameClaimType = "name"; - }); - - builder.Services - .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C")); - - builder.Services.AddKittCore(); - - builder.Services - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped(); - - builder.Services.AddControllersWithViews(); - - builder.Services.AddKittGrahpQL(); - - builder.Services.AddProblemDetails(options => - { - options.Map(ex => - { - int statusCode = StatusCodes.Status503ServiceUnavailable; - if (ex.StatusCode.HasValue) - { - statusCode = (int)ex.StatusCode.Value; - } - - return new StatusCodeProblemDetails(statusCode); - }); - - options.Map(ex => - { - var result = new StatusCodeProblemDetails(StatusCodes.Status400BadRequest); - result.Extensions.Add("errors", ex.Errors.Select(e => new { Name = e.PropertyName, Message = e.ErrorMessage })); - - return result; - }); - - options.Map(ex => - { - var result = new StatusCodeProblemDetails(StatusCodes.Status404NotFound); - return result; - }); - - options.Map(ex => - { - var result = new StatusCodeProblemDetails(StatusCodes.Status400BadRequest); - result.Extensions.Add("errors", new[] { new { Name = ex.ParamName, Message = ex.Message } }); - - return result; - }); - - options.Map(ex => new StatusCodeProblemDetails(StatusCodes.Status400BadRequest)); - }); - - builder.Services.Configure( - options => options.ConnectionString = builder.Configuration["QueueClientOptions:ConnectionString"]!); - - if (builder.Environment.IsDevelopment()) - { - builder.Services.AddSingleton(); - } - else - { - builder.Services.AddSingleton(); - } - - return builder; - } -} diff --git a/src/LemonBot.Web/Extensions/WebApplicationExtensions.cs b/src/LemonBot.Web/Extensions/WebApplicationExtensions.cs deleted file mode 100644 index a367faa..0000000 --- a/src/LemonBot.Web/Extensions/WebApplicationExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Hellang.Middleware.ProblemDetails; -using LemonBot.Web.Endpoints; -using LemonBot.Web.GraphQL; - -namespace LemonBot.Web.Extensions; - -public static class WebApplicationExtensions -{ - public static WebApplication Configure(this WebApplication app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseMigrationsEndPoint(); - app.UseWebAssemblyDebugging(); - - app.UseSwagger(); - app.UseSwaggerUI(); - } - else - { - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseBlazorFrameworkFiles(); - app.UseStaticFiles(); - - app.UseProblemDetails(); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.MapControllers(); - - app - .MapStreamingsEndpoints() - .MapSettingsEndpoints() - .MapMessagesEndpoints() - .MapProposalsEndpoints() - .MapDashboardEndpoints(); - - app.MapKittGraphQL(env); - - app.MapFallbackToFile("index.html"); - - return app; - } -} diff --git a/src/LemonBot.Web/GraphQL/Models/SeoInfo.cs b/src/LemonBot.Web/GraphQL/Models/SeoInfo.cs deleted file mode 100644 index d89de39..0000000 --- a/src/LemonBot.Web/GraphQL/Models/SeoInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace LemonBot.Web.GraphQL.Models; - -public record SeoInfo -{ - public string Title { get; init; } = string.Empty; - - public string Description { get; init; } = string.Empty; - - public string Keywords { get; init; } = string.Empty; -} diff --git a/src/LemonBot.Web/GraphQL/Models/Streaming.cs b/src/LemonBot.Web/GraphQL/Models/Streaming.cs deleted file mode 100644 index c46d203..0000000 --- a/src/LemonBot.Web/GraphQL/Models/Streaming.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace LemonBot.Web.GraphQL.Models; - -public class Streaming -{ - public Guid Id { get; init; } - - public string Title { get; init; } = string.Empty; - - public string Slug { get; init; } = string.Empty; - - public string Abstract { get; init; } = string.Empty; - - public DateTime ScheduleDate { get; init; } - - public TimeSpan StartingTime { get; init; } - - public TimeSpan EndingTime { get; init; } - - public string HostingChannelUrl { get; init; } = string.Empty; - - public string YouTubeVideoUrl { get; init; } = string.Empty; - - public SeoInfo Seo { get; init; } = default!; -} diff --git a/src/LemonBot.Web/GraphQL/Queries/StreamingsQuery.cs b/src/LemonBot.Web/GraphQL/Queries/StreamingsQuery.cs deleted file mode 100644 index ba7e09d..0000000 --- a/src/LemonBot.Web/GraphQL/Queries/StreamingsQuery.cs +++ /dev/null @@ -1,61 +0,0 @@ -using KITT.Core.ReadModels; -using KITT.Core.ReadModels.Extensions; -using LemonBot.Web.GraphQL.Models; - -namespace LemonBot.Web.GraphQL.Queries; - -public class StreamingsQuery -{ - [UseOffsetPaging] - [UseFiltering] - [UseSorting] - public IQueryable GetStreamings([Service(ServiceKind.Synchronized)] IDatabase database) - { - return database.Streamings - .PublishedOnly() - .Select(s => new Streaming - { - Id = s.Id, - Abstract = s.Abstract, - EndingTime = s.EndingTime.ToTimeSpan(), - HostingChannelUrl = s.HostingChannelUrl, - ScheduleDate = s.ScheduleDate.ToDateTime(TimeOnly.MinValue), - Seo = s.Seo != null ? new() { Title = s.Seo.Title, Description = s.Seo.Description, Keywords = s.Seo.Keywords } : new(), - Slug = s.Slug, - StartingTime = s.StartingTime.ToTimeSpan(), - Title = s.Title, - YouTubeVideoUrl = s.YouTubeVideoUrl - }); - } - - public Streaming? GetStreamingBySlug([Service(ServiceKind.Synchronized)] IDatabase database, string slug) - { - var streaming = database.Streamings - .PublishedOnly() - .WithSlug(slug) - .SingleOrDefault(); - - if (streaming is null) - { - return null; - } - - var response = new Streaming - { - Id = streaming.Id, - Abstract = streaming.Abstract, - EndingTime = streaming.EndingTime.ToTimeSpan(), - HostingChannelUrl = streaming.HostingChannelUrl, - ScheduleDate = streaming.ScheduleDate.ToDateTime(TimeOnly.MinValue), - Seo = streaming.Seo != null ? - new() { Title = streaming.Seo.Title, Description = streaming.Seo.Description, Keywords = streaming.Seo.Keywords } - : new(), - Slug = streaming.Slug, - StartingTime = streaming.StartingTime.ToTimeSpan(), - Title = streaming.Title, - YouTubeVideoUrl = streaming.YouTubeVideoUrl - }; - - return response; - } -} diff --git a/src/LemonBot.Web/GraphQL/ServiceCollectionExtensions.cs b/src/LemonBot.Web/GraphQL/ServiceCollectionExtensions.cs deleted file mode 100644 index 73c5995..0000000 --- a/src/LemonBot.Web/GraphQL/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using HotChocolate.Types.Pagination; -using LemonBot.Web.GraphQL.Queries; - -namespace LemonBot.Web.GraphQL; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddKittGrahpQL(this IServiceCollection services) - { - services - .AddGraphQLServer() - .AddFiltering() - .AddSorting() - .SetPagingOptions(new PagingOptions - { - IncludeTotalCount = true - }) - .AddQueryType(); - - return services; - } -} diff --git a/src/LemonBot.Web/GraphQL/WebApplicationExtensions.cs b/src/LemonBot.Web/GraphQL/WebApplicationExtensions.cs deleted file mode 100644 index 892a8b9..0000000 --- a/src/LemonBot.Web/GraphQL/WebApplicationExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using HotChocolate.AspNetCore; - -namespace LemonBot.Web.GraphQL; - -public static class WebApplicationExtensions -{ - public static WebApplication MapKittGraphQL(this WebApplication app, IWebHostEnvironment env) - { - var isDevelopment = env.IsDevelopment(); - - var options = new GraphQLServerOptions { EnableSchemaRequests = isDevelopment }; - options.Tool.Enable = isDevelopment; - - app.MapGraphQL().WithOptions(options); - - return app; - } -} diff --git a/src/LemonBot.Web/Imports.cs b/src/LemonBot.Web/Imports.cs deleted file mode 100644 index 77c2440..0000000 --- a/src/LemonBot.Web/Imports.cs +++ /dev/null @@ -1 +0,0 @@ -global using LemonBot.Web.Extensions; diff --git a/src/LemonBot.Web/LemonBot.Web.csproj b/src/LemonBot.Web/LemonBot.Web.csproj deleted file mode 100644 index 3e0e176..0000000 --- a/src/LemonBot.Web/LemonBot.Web.csproj +++ /dev/null @@ -1,48 +0,0 @@ - - - - net8.0 - enable - enable - - - - $(NoWarn);AD0001 - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - diff --git a/src/LemonBot.Web/Program.cs b/src/LemonBot.Web/Program.cs deleted file mode 100644 index 21de9b1..0000000 --- a/src/LemonBot.Web/Program.cs +++ /dev/null @@ -1,10 +0,0 @@ -var builder = WebApplication.CreateBuilder(args); - -var app = builder - .ConfigureServices() - .Build() - .Configure(builder.Environment); - -app.Run(); - -public partial class Program { } \ No newline at end of file diff --git a/src/LemonBot.Web/Properties/launchSettings.json b/src/LemonBot.Web/Properties/launchSettings.json deleted file mode 100644 index c4dd120..0000000 --- a/src/LemonBot.Web/Properties/launchSettings.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:33795", - "sslPort": 44350 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "LemonBot.Web": { - "commandName": "Project", - "dotnetRunMessages": "true", - "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/src/LemonBot.Web/appsettings.json b/src/LemonBot.Web/appsettings.json deleted file mode 100644 index ab12168..0000000 --- a/src/LemonBot.Web/appsettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AzureAdB2C": { - "Instance": "https://{TENANT}.b2clogin.com/", - "ClientId": "{SERVER API APP CLIENT ID}", - "Domain": "{TENANT DOMAIN}", - "SignUpSignInPolicyId": "{SIGN UP OR SIGN IN POLICY}" - }, - "AllowedHosts": "*", - "ConnectionStrings": { - "KittDatabase": "Server=.\\SQLEXPRESS;Database=KITT;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True" - }, - "BotConfiguration": { - "Endpoint": "", - "Username": "", - "Password": "" - }, - "QueueClientOptions": { - "ConnectionString": "UseDevelopmentStorage=true" - } -} diff --git a/src/LemonBot/LemonBot.csproj b/src/LemonBot/LemonBot.csproj index 8faab6d..dc6c81f 100644 --- a/src/LemonBot/LemonBot.csproj +++ b/src/LemonBot/LemonBot.csproj @@ -6,9 +6,9 @@ enable - - - + + + diff --git a/tests/KITT.Cms.Web.Api.Test/Internals/CmsWebApplicationFactory.cs b/tests/KITT.Cms.Web.Api.Test/Internals/CmsWebApplicationFactory.cs new file mode 100644 index 0000000..e3586b5 --- /dev/null +++ b/tests/KITT.Cms.Web.Api.Test/Internals/CmsWebApplicationFactory.cs @@ -0,0 +1,42 @@ +using KITT.Core.Persistence; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; + +namespace KITT.Cms.Web.Api.Test.Internals; + +public class CmsWebApplicationFactory : WebApplicationFactory +{ + public static string TenantId { get; } = Guid.NewGuid().ToString(); + public static string AppId { get; } = Guid.NewGuid().ToString(); + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + base.ConfigureWebHost(builder); + + builder.ConfigureAppConfiguration(configurationBuilder => + { + configurationBuilder.AddInMemoryCollection( + [ + new KeyValuePair("Identity__TenantId", TenantId), + new KeyValuePair("Identity__Cms__AppId", AppId), + ]); + }); + + builder.ConfigureServices(services => + { + ReplaceDbContext(services); + }); + } + + private static void ReplaceDbContext(IServiceCollection services) + { + var dbContextDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); + if (dbContextDescriptor is not null) + { + services.Remove(dbContextDescriptor); + } + + services.AddDbContextPool( + options => options.UseInMemoryDatabase("Kitt-InMemory")); + } +} diff --git a/tests/KITT.Web.App.Test/KITT.Web.App.Test.csproj b/tests/KITT.Cms.Web.Api.Test/KITT.Cms.Web.Api.Test.csproj similarity index 53% rename from tests/KITT.Web.App.Test/KITT.Web.App.Test.csproj rename to tests/KITT.Cms.Web.Api.Test/KITT.Cms.Web.Api.Test.csproj index 38a38e3..ab70dd0 100644 --- a/tests/KITT.Web.App.Test/KITT.Web.App.Test.csproj +++ b/tests/KITT.Cms.Web.Api.Test/KITT.Cms.Web.Api.Test.csproj @@ -1,29 +1,34 @@ - + - net8.0 + net9.0 + enable enable - false - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive + all - - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + diff --git a/tests/LemonBot.Web.Test.Integration/Properties/launchSettings.json b/tests/KITT.Cms.Web.Api.Test/Properties/launchSettings.json similarity index 61% rename from tests/LemonBot.Web.Test.Integration/Properties/launchSettings.json rename to tests/KITT.Cms.Web.Api.Test/Properties/launchSettings.json index 267147d..6eff8b8 100644 --- a/tests/LemonBot.Web.Test.Integration/Properties/launchSettings.json +++ b/tests/KITT.Cms.Web.Api.Test/Properties/launchSettings.json @@ -1,12 +1,12 @@ { "profiles": { - "LemonBot.Web.Test.Integration": { + "KITT.Cms.Web.Api.Test": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:61388;http://localhost:61389" + "applicationUrl": "https://localhost:39330;http://localhost:39331" } } } \ No newline at end of file diff --git a/tests/KITT.Cms.Web.Api.Test/SettingsEndpointsTest.cs b/tests/KITT.Cms.Web.Api.Test/SettingsEndpointsTest.cs new file mode 100644 index 0000000..b9ff3aa --- /dev/null +++ b/tests/KITT.Cms.Web.Api.Test/SettingsEndpointsTest.cs @@ -0,0 +1,73 @@ +using KITT.Cms.Web.Api.Test.Internals; +using KITT.Cms.Web.Models.Settings; +using KITT.Web.Testing.Authentication; +using KITT.Web.Testing.DependencyInjection; +using KITT.Web.Testing.Security; +using System.IdentityModel.Tokens.Jwt; + +namespace KITT.Cms.Web.Api.Test; + +public class SettingsEndpointsTest : IClassFixture +{ + private readonly CmsWebApplicationFactory _factory; + + private readonly static string endpoint = "api/settings"; + + public SettingsEndpointsTest(CmsWebApplicationFactory factory) + { + _factory = factory; + } + + #region GetAllSettings tests + [Fact] + public async Task GetAllSettings_Should_Return_Unauthorized_If_User_Is_Not_Authenticated() + { + // Arrange + using var client = _factory.CreateClient(); + + // Act + var response = await client.GetAsync(endpoint); + + // Assert + Assert.Equal(System.Net.HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact(Skip = "Must be fixed")] + public async Task GetAllSettings_Should_Return_Empty_List_If_No_Settings_Are_Available() + { + // Arrange + using var app = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices( + services => services.ConfigureTestJwtOptions(CmsWebApplicationFactory.TenantId, CmsWebApplicationFactory.AppId)); + }); + + var token = TestJwtTokenProvider.JwtSecurityTokenHandler.WriteToken( + new JwtSecurityToken( + $"https://sts.windows.net/{CmsWebApplicationFactory.TenantId}/", + $"api://{CmsWebApplicationFactory.AppId}", + new TestClaimsProvider().Claims, + expires: DateTime.Now.AddMinutes(30), + signingCredentials: TestJwtTokenProvider.SigningCredentials)); + + using var client = app.CreateClient(); + client.DefaultRequestHeaders.Authorization = new("Bearer", token); + + // Act + var response = await client.GetAsync(endpoint); + + // Assert + response.EnsureSuccessStatusCode(); + var model = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(model); + Assert.Empty(model.Items); + } + #endregion + + #region GetSettingsDetails tests + #endregion + + #region CreateNewSettings tests + #endregion +} diff --git a/tests/KITT.Cms.Web.Api.Test/StreamingsEndpointsTest.cs b/tests/KITT.Cms.Web.Api.Test/StreamingsEndpointsTest.cs new file mode 100644 index 0000000..2b30781 --- /dev/null +++ b/tests/KITT.Cms.Web.Api.Test/StreamingsEndpointsTest.cs @@ -0,0 +1,13 @@ +using KITT.Cms.Web.Api.Test.Internals; + +namespace KITT.Cms.Web.Api.Test; + +internal class StreamingsEndpointsTest : IClassFixture +{ + private readonly CmsWebApplicationFactory _factory; + + public StreamingsEndpointsTest(CmsWebApplicationFactory factory) + { + _factory = factory; + } +} diff --git a/tests/KITT.Core.Test/Commands/StreamingCommandsTest.cs b/tests/KITT.Core.Test/Commands/StreamingCommandsTest.cs index 0b9be7b..ca6538e 100644 --- a/tests/KITT.Core.Test/Commands/StreamingCommandsTest.cs +++ b/tests/KITT.Core.Test/Commands/StreamingCommandsTest.cs @@ -138,7 +138,7 @@ public async Task ScheduleStreamingAsync_Should_Add_Streaming_With_Specified_Val Assert.Equal(streamingAbstract, scheduledStreaming.Abstract); } - [Fact] + [Fact(Skip = "At this moment the message bus has been disabled")] public async Task ScheduleStreamingAsync_Should_Send_ScheduledStreamingMessage_Correctly() { var messageBusMock = new Mock(); diff --git a/tests/KITT.Core.Test/KITT.Core.Test.csproj b/tests/KITT.Core.Test/KITT.Core.Test.csproj index 02f18ca..26246c2 100644 --- a/tests/KITT.Core.Test/KITT.Core.Test.csproj +++ b/tests/KITT.Core.Test/KITT.Core.Test.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable false @@ -14,15 +14,15 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/KITT.Telegram.Functions.Test/KITT.Telegram.Functions.Test.csproj b/tests/KITT.Telegram.Functions.Test/KITT.Telegram.Functions.Test.csproj index acbd29d..413a24b 100644 --- a/tests/KITT.Telegram.Functions.Test/KITT.Telegram.Functions.Test.csproj +++ b/tests/KITT.Telegram.Functions.Test/KITT.Telegram.Functions.Test.csproj @@ -10,13 +10,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/KITT.Web.App.Test/BUnitContextExtensions.cs b/tests/KITT.Web.App.Test/BUnitContextExtensions.cs deleted file mode 100644 index 9ce1688..0000000 --- a/tests/KITT.Web.App.Test/BUnitContextExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Bunit; -using Microsoft.FluentUI.AspNetCore.Components; - -namespace KITT.Web.App.Test; - -public static class BUnitContextExtensions -{ - public static void SetupFluentUI(this TestContext ctx) - { - ctx.JSInterop.Mode = JSRuntimeMode.Loose; - - ctx.Services.AddFluentUIComponents(options => - { - options.ValidateClassNames = false; - }); - - var module = ctx.JSInterop - .SetupModule("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/DataGrid/FluentDataGrid.razor.js?v=4.9.3.24205"); - - module.SetupVoid("init", _ => true); - } -} diff --git a/tests/KITT.Web.App.Test/Pages/CreateNewSettingsTest.razor b/tests/KITT.Web.App.Test/Pages/CreateNewSettingsTest.razor deleted file mode 100644 index babf11e..0000000 --- a/tests/KITT.Web.App.Test/Pages/CreateNewSettingsTest.razor +++ /dev/null @@ -1,86 +0,0 @@ -@inherits TestContext - -@code { - public CreateNewSettingsTest() - { - this.AddTestAuthorization(); - - this.SetupFluentUI(); - - Services - .AddSingleton>(new Mock>().Object) - .AddSingleton>(new Mock>().Object); - } - - [Fact] - public void CreateNewSettings_Should_Block_Submit_If_Form_Is_Invalid() - { - var clientMock = new Mock(); - - Services.AddSingleton(clientMock.Object); - - var component = Render(@); - var newSettingsForm = component.Find("form"); - - newSettingsForm.Submit(); - - var validationContainer = component.FindAll(".validation-message") - .Where(c => !string.IsNullOrWhiteSpace(c.TextContent)); - - Assert.NotEmpty(validationContainer); - } - - [Fact] - public void CreateNewSettings_Should_Redirect_To_Settings_Page_If_Creation_Ended_Correctly() - { - var clientMock = new Mock(); - clientMock - .Setup(c => c.CreateNewSettingsAsync(It.IsAny())) - .Returns(Task.CompletedTask); - - Services.AddSingleton(clientMock.Object); - - var navigationManager = Services.GetRequiredService(); - - var component = Render(@); - - var channelInput = component.Find("#twitchChannel"); - channelInput.Change("albx87"); - - var newSettingsForm = component.Find("form"); - newSettingsForm.Submit(); - - var expectedUri = $"{navigationManager.BaseUri}settings"; - Assert.Equal(expectedUri, navigationManager.Uri); - } - - [Fact] - public void CreateNewSettings_Should_Show_Error_Message_If_An_Error_Occurred_Calling_Api() - { - var expectedErrorMessage = "Error calling api"; - - var clientMock = new Mock(); - clientMock - .Setup(c => c.CreateNewSettingsAsync(It.IsAny())) - .ThrowsAsync(new ApplicationException(expectedErrorMessage)); - - Services.AddSingleton(clientMock.Object); - - var messageServiceMock = new Mock(); - Services.AddSingleton(messageServiceMock.Object); - - var navigationManager = Services.GetRequiredService(); - - var component = Render(@); - - var channelInput = component.Find("#twitchChannel"); - channelInput.Change("albx87"); - - var newSettingsForm = component.Find("form"); - newSettingsForm.Submit(); - - messageServiceMock.Verify( - m => m.ShowMessageBarAsync(expectedErrorMessage, MessageIntent.Error, KITT.Web.App.UI.SectionNames.MessagesTopSectionName), - Times.Once); - } -} diff --git a/tests/KITT.Web.App.Test/Pages/SettingsTest.razor b/tests/KITT.Web.App.Test/Pages/SettingsTest.razor deleted file mode 100644 index 559ba8a..0000000 --- a/tests/KITT.Web.App.Test/Pages/SettingsTest.razor +++ /dev/null @@ -1,54 +0,0 @@ -@inherits TestContext - -@code { - private string noSettingsConfiguredMessage = "No settings configured"; - - public SettingsTest() - { - this.AddTestAuthorization(); - - var settingsResourcesMock = new Mock>(); - settingsResourcesMock - .Setup(r => r[nameof(Resources.Pages.Settings.NoSettingsConfiguredMessage)]) - .Returns(new LocalizedString(nameof(Resources.Pages.Settings.NoSettingsConfiguredMessage), noSettingsConfiguredMessage)); - - Services.AddSingleton>(settingsResourcesMock.Object); - - this.SetupFluentUI(); - } - - [Fact] - public void Settings_Should_Show_Expected_Message_If_Settings_Items_Are_Empty() - { - var clientMock = new Mock(); - clientMock.Setup(c => c.GetAllSettingsAsync()) - .Returns(Task.FromResult(new SettingsListModel())); - - Services.AddSingleton(clientMock.Object); - - var component = Render(@); - var noSettingsContainer = component.Find("h2#noSettingsConfiguredContainer"); - Assert.Equal(noSettingsConfiguredMessage, noSettingsContainer.TextContent); - } - - [Fact] - public void Settings_Should_Render_Items_Rows_Correctly() - { - var items = new[] - { - new SettingsListModel.SettingsDescriptor { Id = Guid.NewGuid(), TwitchChannel = "albx87" } - }; - - var clientMock = new Mock(); - clientMock.Setup(c => c.GetAllSettingsAsync()) - .Returns(Task.FromResult(new SettingsListModel { Items = items })); - - Services.AddSingleton(clientMock.Object); - - var component = Render(@); - var rows = component.FindAll("""fluent-data-grid > fluent-data-grid-row[row-type="default"]"""); - - Assert.Equal(items.Length, rows.Count); - Assert.Equal(items.First().TwitchChannel, rows.First().TextContent); - } -} diff --git a/tests/KITT.Web.App.Test/Pages/Streamings/IndexTest.razor b/tests/KITT.Web.App.Test/Pages/Streamings/IndexTest.razor deleted file mode 100644 index 989728a..0000000 --- a/tests/KITT.Web.App.Test/Pages/Streamings/IndexTest.razor +++ /dev/null @@ -1,125 +0,0 @@ -@inherits TestContext - -@code { - public IndexTest() - { - this.AddTestAuthorization(); - - var commonLocalizerMock = new Mock>(); - var indexLocalizerMock = new Mock>(); - indexLocalizerMock - .Setup(r => r[nameof(KITT.Web.App.Resources.Pages.Streamings.Index.DeleteStreamingConfirmTitle)]) - .Returns(new LocalizedString(nameof(KITT.Web.App.Resources.Pages.Streamings.Index.DeleteStreamingConfirmTitle), nameof(KITT.Web.App.Resources.Pages.Streamings.Index.DeleteStreamingConfirmTitle))); - - indexLocalizerMock - .Setup(r => r[nameof(Resources.Pages.Streamings.Index.DeleteStreamingConfirmText), "test"]) - .Returns(new LocalizedString(nameof(Resources.Pages.Streamings.Index.DeleteStreamingConfirmText), nameof(Resources.Pages.Streamings.Index.DeleteStreamingConfirmText))); - - Services - .AddSingleton>(commonLocalizerMock.Object) - .AddSingleton>(indexLocalizerMock.Object); - - this.SetupFluentUI(); - } - - [Fact] - public void Index_OpenStreamingDetail_Should_Open_Streaming_Detail_Url() - { - var item = new StreamingsListModel.StreamingListItemModel { Id = Guid.NewGuid(), Title = "test" }; - - var clientMock = new Mock(); - clientMock.Setup(c => c.GetAllStreamingsAsync(It.IsAny())) - .ReturnsAsync(new StreamingsListModel { Items = new[] { item }, TotalItems = 1 }); - - RegisterServices(clientMock: clientMock); - RenderComponent(); - - var component = Render(@); - - var searchForm = component.Find("#streamingSearchForm"); - searchForm.Submit(); - - var openDetailButton = component.Find($"#openStreamingDetail-{item.Id}"); - openDetailButton.Click(); - - var navigationManager = Services.GetRequiredService(); - - var expectedUri = $"{navigationManager.BaseUri}streamings/{item.Id}"; - Assert.Equal(expectedUri, navigationManager.Uri); - } - - [Fact] - public void Index_DeleteStreaming_Should_Call_Delete_Api_If_Confirmed() - { - var item = new StreamingsListModel.StreamingListItemModel { Id = Guid.NewGuid(), Title = "test" }; - - var clientMock = new Mock(); - clientMock.Setup(c => c.GetAllStreamingsAsync(It.IsAny())) - .ReturnsAsync(new StreamingsListModel { Items = new[] { item }, TotalItems = 1 }); - - var dialogServiceMock = new Mock(); - var dialogReferenceMock = new Mock(); - dialogReferenceMock.SetupGet(r => r.Result) - .Returns(Task.FromResult(DialogResult.Ok(null))); - - dialogServiceMock - .Setup(d => d.ShowConfirmationAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(dialogReferenceMock.Object)); - - RegisterServices(clientMock: clientMock, dialogServiceMock: dialogServiceMock); - RenderComponent(); - - var component = Render(@); - - var searchForm = component.Find("#streamingSearchForm"); - searchForm.Submit(); - - var deleteButton = component.Find($"#deleteStreaming-{item.Id}"); - deleteButton.Click(); - - clientMock.Verify(c => c.DeleteStreamingAsync(item.Id), Times.Once(), ""); - } - - [Fact] - public void Index_Should_Render_All_Streamings_Correctly() - { - var items = new[] - { - new StreamingsListModel.StreamingListItemModel { Id = Guid.NewGuid(), Title = "test1" }, - new StreamingsListModel.StreamingListItemModel { Id = Guid.NewGuid(), Title = "test2" } - }; - - var clientMock = new Mock(); - clientMock.Setup(c => c.GetAllStreamingsAsync(It.IsAny())) - .ReturnsAsync(new StreamingsListModel { Items = items, TotalItems = items.Length }); - - RegisterServices(clientMock: clientMock); - - var component = Render(@); - - var searchForm = component.Find("#streamingSearchForm"); - searchForm.Submit(); - - var streamingsRows = component.FindAll("""fluent-data-grid > fluent-data-grid-row[row-type="default"]"""); - - Assert.Equal(items.Length, streamingsRows.Count); - } - - private void RegisterServices( - Mock? clientMock = null, - Mock? toastServiceMock = null, - Mock? dialogServiceMock = null) - { - if (clientMock is null) - clientMock = new Mock(); - if (toastServiceMock is null) - toastServiceMock = new Mock(); - if (dialogServiceMock is null) - dialogServiceMock = new Mock(); - - Services - .AddSingleton(clientMock.Object) - .AddSingleton(toastServiceMock.Object) - .AddSingleton(dialogServiceMock.Object); - } -} diff --git a/tests/KITT.Web.App.Test/Pages/Streamings/ScheduleTest.razor b/tests/KITT.Web.App.Test/Pages/Streamings/ScheduleTest.razor deleted file mode 100644 index c4e847d..0000000 --- a/tests/KITT.Web.App.Test/Pages/Streamings/ScheduleTest.razor +++ /dev/null @@ -1,68 +0,0 @@ -@inherits TestContext - -@code { - public ScheduleTest() - { - this.AddTestAuthorization(); - this.SetupFluentUI(); - - var commonLocalizerMock = new Mock>(); - var scheduleLocalizerMock = new Mock>(); - var scheduleFormLocalizerMock = new Mock>(); - - Services - .AddSingleton(commonLocalizerMock.Object) - .AddSingleton(scheduleLocalizerMock.Object) - .AddSingleton(scheduleFormLocalizerMock.Object) - .AddSingleton(new Mock().Object); - } - - [Fact] - public void Schedule_Should_Not_Submit_If_Form_Is_Invalid() - { - var clientMock = new Mock(); - Services.AddSingleton(clientMock.Object); - - Services.AddSingleton(new Mock().Object); - - var component = Render(@); - - var form = component.Find("form"); - form.Submit(); - - var errors = component.FindAll(".validation-message").Where(e => !string.IsNullOrEmpty(e.TextContent)); - Assert.NotEmpty(errors); - - clientMock.Verify(c => c.ScheduleStreamingAsync(It.IsAny()), Times.Never); - } - - [Fact] - public void Schedule_Should_Save_Streaming_Correctly_And_Redirect_To_Index() - { - var clientMock = new Mock(); - Services.AddSingleton(clientMock.Object); - - Services.AddSingleton(new Mock().Object); - - var navigationManager = Services.GetRequiredService(); - - var component = Render(@); - - component.Find("#streamingTitle").Change("test"); - component.Find("#streamingSlug").Change("test"); - component.Find("#streamingChannel").Change("albx87"); - component.Find("#streamingScheduleDate").Change(DateTime.Today.AddDays(5)); - component.Find("#streamingStartTime").Change(new TimeSpan(18, 30, 0)); - component.Find("#streamingEndTime").Change(new TimeSpan(20, 0, 0)); - - var saveButton = component.Find("#saveContentButton"); - saveButton.Click(); - - clientMock.Verify( - c => c.ScheduleStreamingAsync(It.Is(s => s.Title == "test" && s.Slug == "test" && s.HostingChannelUrl == "https://www.twitch.tv/albx87")), - Times.Once); - - var expectedUrl = $"{navigationManager.BaseUri}streamings"; - Assert.Equal(expectedUrl, navigationManager.Uri); - } -} diff --git a/tests/KITT.Web.App.Test/_Imports.razor b/tests/KITT.Web.App.Test/_Imports.razor deleted file mode 100644 index 6180095..0000000 --- a/tests/KITT.Web.App.Test/_Imports.razor +++ /dev/null @@ -1,16 +0,0 @@ -@using Xunit -@using Bunit -@using Bunit.TestDoubles -@using Microsoft.Extensions.DependencyInjection -@using Microsoft.Extensions.Localization -@using Moq -@using KITT.Web.App.Shared -@using KITT.Web.App.Components -@using KITT.Web.App.Pages -@using KITT.Web.App.Pages.Streamings -@using KITT.Web.App.Clients -@using KITT.Web.Models.Settings -@using KITT.Web.Models.Streamings -@using Microsoft.AspNetCore.Components.Forms -@using KITT.Web.App.UI.Components -@using Microsoft.FluentUI.AspNetCore.Components \ No newline at end of file diff --git a/tests/KITT.Web.Testing/Authentication/TestAuthenticationBuilder.cs b/tests/KITT.Web.Testing/Authentication/TestAuthenticationBuilder.cs new file mode 100644 index 0000000..2e1d9e1 --- /dev/null +++ b/tests/KITT.Web.Testing/Authentication/TestAuthenticationBuilder.cs @@ -0,0 +1,33 @@ +using System; +using KITT.Web.Testing.Security; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace KITT.Web.Testing.Authentication; + +public sealed class TestAuthenticationBuilder +{ + public IServiceCollection Services { get; } + public string AuthenticationScheme { get; } + + internal TestAuthenticationBuilder(IServiceCollection services, string authenticationScheme) + { + Services = services ?? throw new ArgumentNullException(nameof(services)); + AuthenticationScheme = authenticationScheme; + + ConfigureClaimsProvider(); + ConfigureAuthentication(); + } + + private void ConfigureAuthentication() + { + Services.AddAuthentication(AuthenticationScheme) + .AddScheme(AuthenticationScheme, null); + } + + private void ConfigureClaimsProvider() + { + Services.TryAddScoped(); + } +} diff --git a/tests/KITT.Web.Testing/Authentication/TestAuthenticationHandler.cs b/tests/KITT.Web.Testing/Authentication/TestAuthenticationHandler.cs new file mode 100644 index 0000000..ebabedf --- /dev/null +++ b/tests/KITT.Web.Testing/Authentication/TestAuthenticationHandler.cs @@ -0,0 +1,34 @@ +using System.Security.Claims; +using System.Security.Principal; +using System.Text.Encodings.Web; +using KITT.Web.Testing.Security; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace KITT.Web.Testing.Authentication; + +public class TestAuthenticationHandler : AuthenticationHandler +{ + private readonly TestClaimsProvider _claimsProvider; + + public TestAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + TestClaimsProvider claimsProvider) : base(options, logger, encoder) + { + _claimsProvider = claimsProvider ?? throw new ArgumentNullException(nameof(claimsProvider)); + } + + protected override Task HandleAuthenticateAsync() + { + var identity = new ClaimsIdentity("Test"); + identity.AddClaims(_claimsProvider.Claims); + + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, Scheme.Name); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } +} diff --git a/tests/KITT.Web.Testing/Authentication/TestJwtTokenProvider.cs b/tests/KITT.Web.Testing/Authentication/TestJwtTokenProvider.cs new file mode 100644 index 0000000..fdb9c02 --- /dev/null +++ b/tests/KITT.Web.Testing/Authentication/TestJwtTokenProvider.cs @@ -0,0 +1,15 @@ +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; + +namespace KITT.Web.Testing.Authentication; + +public static class TestJwtTokenProvider +{ + public static string Issuer { get; } = "test.identity"; + + public static SecurityKey SecurityKey { get; } = new SymmetricSecurityKey(new byte[256]); + + public static SigningCredentials SigningCredentials { get; } = new(SecurityKey, SecurityAlgorithms.HmacSha256); + + public static JwtSecurityTokenHandler JwtSecurityTokenHandler { get; } = new(); +} diff --git a/tests/KITT.Web.Testing/DependencyInjection/TestAuthenticationServiceCollectionExtensions.cs b/tests/KITT.Web.Testing/DependencyInjection/TestAuthenticationServiceCollectionExtensions.cs new file mode 100644 index 0000000..900a4c1 --- /dev/null +++ b/tests/KITT.Web.Testing/DependencyInjection/TestAuthenticationServiceCollectionExtensions.cs @@ -0,0 +1,44 @@ +using KITT.Web.Testing.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace KITT.Web.Testing.DependencyInjection; + +public static class TestAuthenticationServiceCollectionExtensions +{ + public static TestAuthenticationBuilder AddTestAuthentication( + this IServiceCollection services, + string authenticationScheme = "Test") + { + ArgumentNullException.ThrowIfNull(services); + return new TestAuthenticationBuilder(services, authenticationScheme); + } + + public static IServiceCollection ConfigureTestJwtOptions( + this IServiceCollection services, + string tenantId, + string appId) + { + services.Configure( + JwtBearerDefaults.AuthenticationScheme, + options => + { + options.Configuration = new OpenIdConnectConfiguration + { + Issuer = $"https://sts.windows.net/{tenantId}/", + }; + + options.Authority = $"https://sts.windows.net/{tenantId}/"; + options.Audience = $"api://{appId}"; + + // ValidIssuer and ValidAudience is not required, but it helps to define them as otherwise they can be overriden by for example the `user-jwts` tool which will cause the validation to fail + options.TokenValidationParameters.ValidIssuer = $"https://sts.windows.net/{tenantId}/"; + options.TokenValidationParameters.ValidAudience = $"api://{appId}"; + + options.Configuration.SigningKeys.Add(TestJwtTokenProvider.SecurityKey); + }); + + return services; + } +} diff --git a/tests/KITT.Web.Testing/KITT.Web.Testing.csproj b/tests/KITT.Web.Testing/KITT.Web.Testing.csproj new file mode 100644 index 0000000..75a6224 --- /dev/null +++ b/tests/KITT.Web.Testing/KITT.Web.Testing.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + diff --git a/tests/KITT.Web.Testing/Security/TestClaimsProvider.cs b/tests/KITT.Web.Testing/Security/TestClaimsProvider.cs new file mode 100644 index 0000000..f381136 --- /dev/null +++ b/tests/KITT.Web.Testing/Security/TestClaimsProvider.cs @@ -0,0 +1,16 @@ +using System; +using System.Security.Claims; + +namespace KITT.Web.Testing.Security; + +public class TestClaimsProvider +{ + public IList Claims { get; } = []; + + public TestClaimsProvider() + { + Claims.Add(new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString())); + Claims.Add(new Claim(ClaimTypes.Name, "Test User")); + Claims.Add(new Claim("oid", Guid.NewGuid().ToString())); + } +} diff --git a/tests/LemonBot.Web.Test.Integration/Console/StreamingsControllerTest.cs b/tests/LemonBot.Web.Test.Integration/Console/StreamingsControllerTest.cs deleted file mode 100644 index 06f7c2b..0000000 --- a/tests/LemonBot.Web.Test.Integration/Console/StreamingsControllerTest.cs +++ /dev/null @@ -1,722 +0,0 @@ -using KITT.Core.Models; -using KITT.Web.Models.Streamings; -using LemonBot.Web.Test.Integration.Fixtures; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; -using System.Net; -using Xunit; - -namespace LemonBot.Web.Test.Integration.Console; - -public class StreamingsControllerTest : - IClassFixture -{ - private readonly KittWebApplicationFactory factory; - - public StreamingsControllerTest(KittWebApplicationFactory factory) - { - this.factory = factory; - } - - #region GetAllStreamings tests - [Fact] - public async Task GetAllStreamings_Should_Return_Unauthorized_When_No_Access_Token_Is_Specified() - { - var client = this.factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - var response = await client.GetAsync("/api/console/streamings"); - - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task GetAllStreamings_Should_Return_Ok_As_Expected() - { - var createdStreamingId = Guid.Empty; - var scheduleDate = DateOnly.FromDateTime(DateTime.Today.AddDays(1)); - TimeOnly startingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(16)); - TimeOnly endingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(18)); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest( - services, - context => - { - var streaming = Streaming.Schedule("test", "test", "albx87", scheduleDate, startingTime, endingTime, "albx87", userId); - context.Streamings.Add(streaming); - context.SaveChanges(); - createdStreamingId = streaming.Id; - }); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var response = await client.GetAsync("/api/console/streamings"); - - var model = await response.Content.ReadFromJsonAsync(); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - Assert.NotNull(model); - Assert.Equal( - new StreamingsListModel.StreamingListItemModel { Id = createdStreamingId, EndingTime = new(18, 0), StartingTime = new(16, 0), HostingChannelUrl = "albx87", ScheduledOn = scheduleDate, Title = "test" }, - model!.Items.First()); - } - #endregion - - #region GetStreamingDetail tests - [Fact] - public async Task GetStreamingDetail_Should_Return_Not_Found_If_Streaming_Does_Not_Exist() - { - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var response = await client.GetAsync($"/api/console/streamings/{Guid.NewGuid()}"); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - public async Task GetStreamingDetail_Should_Return_Streaming_Detail_Information() - { - var createdStreamingId = Guid.Empty; - var scheduleDate = DateOnly.FromDateTime(DateTime.Today.AddDays(1)); - var startingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(16)); - var endingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(18)); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest( - services, - context => - { - var streaming = Streaming.Schedule("test", "test", "albx87", scheduleDate, startingTime, endingTime, "albx87", userId); - context.Streamings.Add(streaming); - context.SaveChanges(); - createdStreamingId = streaming.Id; - }); - }); - }) - .CreateClient(); - - var response = await client.GetAsync($"/api/console/streamings/{createdStreamingId}"); - - var model = await response.Content.ReadFromJsonAsync(); - - var expected = new StreamingDetailModel - { - Id = createdStreamingId, - EndingTime = endingTime, - HostingChannelUrl = "albx87", - ScheduleDate = scheduleDate, - Slug = "test", - StartingTime = startingTime, - Title = "test" - }; - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(expected, model); - } - #endregion - - #region ScheduleStreaming tests - [Fact] - public async Task ScheduleStreaming_Should_Return_Bad_Request_If_Model_Is_Invalid() - { - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest(services); - }); - }) - .CreateClient(); - - var model = new ScheduleStreamingModel(); - - var response = await client.PostAsJsonAsync("/api/console/streamings", model); - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } - - [Fact] - public async Task ScheduleStreaming_Should_Return_Created_As_Expected() - { - var scheduleDate = DateOnly.FromDateTime(DateTime.Today.AddDays(1)); - var startingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(16)); - var endingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(18)); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest(services); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new ScheduleStreamingModel - { - EndingTime = endingTime, - HostingChannelUrl = "https://www.twitch.tv/albx87", - ScheduleDate = scheduleDate, - Slug = "test-create", - StartingTime = startingTime, - StreamingAbstract = "test", - Title = "test create" - }; - - var response = await client.PostAsJsonAsync("/api/console/streamings", model); - var modelFromResponse = await response.Content.ReadFromJsonAsync(); - - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.Equal(model, modelFromResponse); - } - - [Fact] - public async Task ScheduleStreaming_Should_Return_Bad_Request_If_Schedule_Date_Is_Previous_Than_Today() - { - var scheduleDate = DateOnly.FromDateTime(DateTime.Today.AddDays(-1)); - var startingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(16)); - var endingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(18)); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest(services); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new ScheduleStreamingModel - { - EndingTime = endingTime, - HostingChannelUrl = "https://www.twitch.tv/albx87", - ScheduleDate = scheduleDate, - Slug = "test-create", - StartingTime = startingTime, - StreamingAbstract = "test", - Title = "test create" - }; - - var response = await client.PostAsJsonAsync("/api/console/streamings", model); - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } - - [Fact] - public async Task ScheduleStreaming_Should_Return_Bad_Request_If_Ending_Time_Is_Previous_Than_Starting_Time() - { - var scheduleDate = DateOnly.FromDateTime(DateTime.Today.AddDays(1)); - var startingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(18)); - var endingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(16)); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest(services); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new ScheduleStreamingModel - { - EndingTime = endingTime, - HostingChannelUrl = "https://www.twitch.tv/albx87", - ScheduleDate = scheduleDate, - Slug = "test-create", - StartingTime = startingTime, - StreamingAbstract = "test", - Title = "test create" - }; - - var response = await client.PostAsJsonAsync("/api/console/streamings", model); - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } - - [Fact] - public async Task ScheduleStreaming_Should_Save_Schedule_Time_Correctly() - { - var scheduleDate = DateOnly.FromDateTime(DateTime.Now.AddDays(1)); - var startingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(16)); - var endingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(18)); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest(services); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new ScheduleStreamingModel - { - EndingTime = endingTime, - HostingChannelUrl = "https://www.twitch.tv/albx87", - ScheduleDate = scheduleDate, - Slug = "test-create", - StartingTime = startingTime, - StreamingAbstract = "test", - Title = "test create" - }; - - var response = await client.PostAsJsonAsync("/api/console/streamings", model); - var modelFromResponse = await response.Content.ReadFromJsonAsync(); - - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - - Assert.Equal(startingTime, modelFromResponse!.StartingTime); - Assert.Equal(endingTime, modelFromResponse!.EndingTime); - } - #endregion - - #region ImportStreaming tests - [Fact] - public async Task ImportStreaming_Should_Return_Bad_Request_If_Model_Is_Invalid() - { - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest(services); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new ImportStreamingModel(); - - var response = await client.PostAsJsonAsync("/api/console/streamings/import", model); - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } - - [Fact] - public async Task ImportStreaming_Should_Return_Created_As_Expected() - { - var scheduleDate = DateTime.Today.AddDays(-1); - var startingTime = TimeSpan.FromHours(16); - var endingTime = TimeSpan.FromHours(18); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest(services); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new ImportStreamingModel - { - EndingTime = endingTime, - HostingChannelUrl = "https://www.twitch.tv/albx87", - ScheduleDate = scheduleDate, - Slug = "test-import", - StartingTime = startingTime, - StreamingAbstract = "test", - Title = "test import", - YoutubeVideoUrl = "youtube.com/test" - }; - - var response = await client.PostAsJsonAsync("/api/console/streamings/import", model); - var modelFromResponse = await response.Content.ReadFromJsonAsync(); - - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.Equal(model, modelFromResponse); - } - - [Fact] - public async Task ImportStreaming_Should_Return_Bad_Request_If_Ending_Time_Is_Previous_Than_Starting_Time() - { - var scheduleDate = DateTime.Today.AddDays(-1); - var startingTime = TimeSpan.FromHours(18); - var endingTime = TimeSpan.FromHours(16); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest(services); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new ImportStreamingModel - { - EndingTime = endingTime, - HostingChannelUrl = "https://www.twitch.tv/albx87", - ScheduleDate = scheduleDate, - Slug = "test-import", - StartingTime = startingTime, - StreamingAbstract = "test", - Title = "test import", - YoutubeVideoUrl = "youtube.com/test" - }; - - var response = await client.PostAsJsonAsync("/api/console/streamings/import", model); - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } - - [Fact] - public async Task ImportStreaming_Should_Save_Schedule_Time_Correctly() - { - var scheduleDate = DateTime.Now.AddDays(-1); - var startingTime = TimeSpan.FromHours(16); - var endingTime = TimeSpan.FromHours(18); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest(services); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new ImportStreamingModel - { - EndingTime = endingTime, - HostingChannelUrl = "https://www.twitch.tv/albx87", - ScheduleDate = scheduleDate, - Slug = "test-import", - StartingTime = startingTime, - StreamingAbstract = "test", - Title = "test import", - YoutubeVideoUrl = "youtube.com/test" - }; - - var response = await client.PostAsJsonAsync("/api/console/streamings/import", model); - var modelFromResponse = await response.Content.ReadFromJsonAsync(); - - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - - Assert.Equal(startingTime, modelFromResponse!.StartingTime); - Assert.Equal(endingTime, modelFromResponse!.EndingTime); - } - #endregion - - #region UpdateStreaming tests - [Fact] - public async Task UpdateStreaming_Should_Return_Not_Found_If_Streaming_Id_Is_An_Empty_Guid() - { - var streamingId = Guid.Empty; - var scheduleDate = DateOnly.FromDateTime(DateTime.Today.AddDays(-1)); - var startingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(16)); - var endingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(18)); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest(services); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new StreamingDetailModel - { - Id = streamingId, - EndingTime = endingTime, - HostingChannelUrl = "https://www.twitch.tv/albx87", - ScheduleDate = scheduleDate, - StartingTime = startingTime, - Slug = "test", - StreamingAbstract = "my abstract", - Title = "test", - YoutubeVideoUrl = "https://www.youtube.com/test" - }; - - var response = await client.PutAsJsonAsync($"/api/console/streamings/{streamingId}", model); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - public async Task UpdateStreaming_Should_Return_Bad_Request_If_Model_Is_Invalid() - { - var streamingId = Guid.Empty; - var scheduleDate = DateOnly.FromDateTime(DateTime.Today.AddDays(1)); - var startingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(16)); - var endingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(18)); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest( - services, - context => - { - var streaming = Streaming.Schedule("test", "test", "albx87", scheduleDate, startingTime, endingTime, "albx87", userId); - context.Streamings.Add(streaming); - streamingId = streaming.Id; - - context.SaveChanges(); - }); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new StreamingDetailModel { Id = streamingId }; - - var response = await client.PutAsJsonAsync($"/api/console/streamings/{streamingId}", model); - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } - - [Fact] - public async Task UpdateStreaming_Should_Return_NoContent_Status_Code_As_Expected() - { - var streamingId = Guid.Empty; - var scheduleDate = DateOnly.FromDateTime(DateTime.Today.AddDays(1)); - var startingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(16)); - var endingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(18)); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest( - services, - context => - { - var streaming = Streaming.Schedule("test", "test", "albx87", scheduleDate, startingTime, endingTime, "albx87", userId); - context.Streamings.Add(streaming); - streamingId = streaming.Id; - - context.SaveChanges(); - }); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new StreamingDetailModel - { - Id = streamingId, - EndingTime = endingTime, - HostingChannelUrl = "https://www.twitch.tv/albx87", - ScheduleDate = scheduleDate, - StartingTime = startingTime, - Slug = "test", - StreamingAbstract = "my abstract", - Title = "test", - YoutubeVideoUrl = "https://www.youtube.com/test" - }; - - var response = await client.PutAsJsonAsync($"/api/console/streamings/{streamingId}", model); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } - - [Fact] - public async Task UpdateStreaming_Should_Return_NoContent_Status_Code_Even_If_Streaming_Has_A_Past_Schedule_Date() - { - var streamingId = Guid.Empty; - var scheduleDate = DateOnly.FromDateTime(DateTime.Today.AddMonths(-1)); - var startingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(16)); - var endingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(18)); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest( - services, - context => - { - var streaming = Streaming.Import("test", "test", "albx87", scheduleDate, startingTime, endingTime, "albx87", "youtube", "my abstract", userId); - context.Streamings.Add(streaming); - streamingId = streaming.Id; - - context.SaveChanges(); - }); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var model = new StreamingDetailModel - { - Id = streamingId, - EndingTime = endingTime, - HostingChannelUrl = "https://www.twitch.tv/albx87", - ScheduleDate = scheduleDate, - StartingTime = new(16, 30), - Slug = "test", - StreamingAbstract = "my abstract", - Title = "test", - YoutubeVideoUrl = "https://www.youtube.com/test" - }; - - var response = await client.PutAsJsonAsync($"/api/console/streamings/{streamingId}", model); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } - #endregion - - #region DeleteStreaming tests - [Fact] - public async Task DeleteStreaming_Should_Return_NoContent_Status_Code_As_Expected() - { - var streamingId = Guid.Empty; - var scheduleDate = DateOnly.FromDateTime(DateTime.Today.AddDays(1)); - var startingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(16)); - var endingTime = TimeOnly.FromTimeSpan(TimeSpan.FromHours(18)); - var userId = TestAuthenticationHandler.UserId; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - - builder.ConfigureServices(services => - { - DataHelper.PrepareDataForTest( - services, - context => - { - var streaming = Streaming.Schedule("test", "test", "albx87", scheduleDate, startingTime, endingTime, "albx87", userId); - context.Streamings.Add(streaming); - streamingId = streaming.Id; - - context.SaveChanges(); - }); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var response = await client.DeleteAsync($"/api/console/streamings/{streamingId}"); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } - - [Fact] - public async Task DeleteStreaming_Should_Return_Not_Found_If_Streaming_Id_Is_An_Empty_Guid() - { - var streamingId = Guid.Empty; - - var client = this.factory - .WithWebHostBuilder(builder => - { - builder.ConfigureTestServices(services => - { - services.AddTestAuthentication(); - }); - }) - .CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - var response = await client.DeleteAsync($"/api/console/streamings/{streamingId}"); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - #endregion -} diff --git a/tests/LemonBot.Web.Test.Integration/DataHelper.cs b/tests/LemonBot.Web.Test.Integration/DataHelper.cs deleted file mode 100644 index 1df1861..0000000 --- a/tests/LemonBot.Web.Test.Integration/DataHelper.cs +++ /dev/null @@ -1,37 +0,0 @@ -using KITT.Core.Models; -using KITT.Core.Persistence; - -namespace LemonBot.Web.Test.Integration; - -internal static class DataHelper -{ - public static void PrepareDataForTest(IServiceCollection services, Action? customDataPreparation = null) - { - var provider = services.BuildServiceProvider(); - - using var scope = provider.CreateScope(); - var scopedServices = scope.ServiceProvider; - - var shouldSave = false; - - var context = scopedServices.GetRequiredService(); - if (!context.Settings.Any()) - { - context.Settings.Add(Settings.CreateNew(TestAuthenticationHandler.UserId, "albx87")); - shouldSave = true; - } - - if (context.Streamings.Any()) - { - context.Streamings.RemoveRange(context.Streamings); - shouldSave = true; - } - - if (shouldSave) - { - context.SaveChanges(); - } - - customDataPreparation?.Invoke(context); - } -} diff --git a/tests/LemonBot.Web.Test.Integration/Fixtures/KittWebApplicationFactory.cs b/tests/LemonBot.Web.Test.Integration/Fixtures/KittWebApplicationFactory.cs deleted file mode 100644 index d03b618..0000000 --- a/tests/LemonBot.Web.Test.Integration/Fixtures/KittWebApplicationFactory.cs +++ /dev/null @@ -1,46 +0,0 @@ -using KITT.Core.Persistence; -using KITT.Telegram.Messages; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.EntityFrameworkCore; -using Moq; - -namespace LemonBot.Web.Test.Integration.Fixtures; - -public class KittWebApplicationFactory : WebApplicationFactory -{ - private Mock _messageBusMock = new Mock(); - - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - builder.ConfigureServices(services => - { - RemoveDbContextDescriptor(services); - services.AddDbContext(options => options.UseInMemoryDatabase("Kitt-InMemory-Test")); - - RemoveMessageBusDescriptor(services); - services.AddSingleton(_messageBusMock.Object); - }); - } - - private void RemoveMessageBusDescriptor(IServiceCollection services) - { - var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IMessageBus)); - if (descriptor is not null) - { - services.Remove(descriptor); - } - } - - private void RemoveDbContextDescriptor(IServiceCollection services) - { - var descriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(DbContextOptions)); - - if (descriptor is not null) - { - services.Remove(descriptor); - } - } - - -} diff --git a/tests/LemonBot.Web.Test.Integration/LemonBot.Web.Test.Integration.csproj b/tests/LemonBot.Web.Test.Integration/LemonBot.Web.Test.Integration.csproj deleted file mode 100644 index 2e92ad6..0000000 --- a/tests/LemonBot.Web.Test.Integration/LemonBot.Web.Test.Integration.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - net8.0 - enable - enable - - false - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/tests/LemonBot.Web.Test.Integration/ServiceCollectionExtensions.cs b/tests/LemonBot.Web.Test.Integration/ServiceCollectionExtensions.cs deleted file mode 100644 index 15d8db2..0000000 --- a/tests/LemonBot.Web.Test.Integration/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Authentication; - -namespace LemonBot.Web.Test.Integration; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddTestAuthentication(this IServiceCollection services) - { - services.AddAuthentication("Test") - .AddScheme("Test", options => { }); - - return services; - } -} diff --git a/tests/LemonBot.Web.Test.Integration/TestAuthenticationHandler.cs b/tests/LemonBot.Web.Test.Integration/TestAuthenticationHandler.cs deleted file mode 100644 index 6a428ca..0000000 --- a/tests/LemonBot.Web.Test.Integration/TestAuthenticationHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Options; -using System.Security.Claims; -using System.Text.Encodings.Web; - -namespace LemonBot.Web.Test.Integration; - -public class TestAuthenticationHandler : AuthenticationHandler -{ - public static string UserId { get; } = Guid.NewGuid().ToString(); - - public TestAuthenticationHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder) : base(options, logger, encoder) - { - } - - protected override Task HandleAuthenticateAsync() - { - var claims = new[] { new Claim(ClaimTypes.Name, "Test user"), new Claim(ClaimTypes.NameIdentifier, UserId) }; - var identity = new ClaimsIdentity(claims, "Test"); - var principal = new ClaimsPrincipal(identity); - var ticket = new AuthenticationTicket(principal, "Test"); - - var result = AuthenticateResult.Success(ticket); - - return Task.FromResult(result); - } -}