diff --git a/.github/workflows/develop-api.yaml b/.github/workflows/develop-api.yaml index 9a2d3ed01..8851e1a9b 100644 --- a/.github/workflows/develop-api.yaml +++ b/.github/workflows/develop-api.yaml @@ -3,7 +3,13 @@ on: workflow_dispatch: push: paths: - - 'backend/**' + - 'backend/LexBoxApi/**' + - 'backend/LexCore/**' + - 'backend/LexData/**' + - 'backend/LfClassicData/**' + - 'backend/MiniLcm/**' + - 'backend/SyncReverserProxy/**' + - 'backend/Testing/**' - '.github/workflows/lexbox-api.yaml' - '.github/workflows/deploy.yaml' - 'deployment/lexbox-deployment.yaml' @@ -11,7 +17,13 @@ on: - develop pull_request: paths: - - 'backend/**' + - 'backend/LexBoxApi/**' + - 'backend/LexCore/**' + - 'backend/LexData/**' + - 'backend/LfClassicData/**' + - 'backend/MiniLcm/**' + - 'backend/SyncReverserProxy/**' + - 'backend/Testing/**' - '.github/workflows/lexbox-api.yaml' - '.github/workflows/deploy.yaml' - 'deployment/lexbox-deployment.yaml' diff --git a/.github/workflows/fw-lite.yaml b/.github/workflows/fw-lite.yaml new file mode 100644 index 000000000..70cc22320 --- /dev/null +++ b/.github/workflows/fw-lite.yaml @@ -0,0 +1,77 @@ +name: FieldWorks Lite +on: + workflow_dispatch: + push: + paths: + - 'backend/FwDataMiniLcmBridge/**' + - 'backend/harmony/**' + - 'backend/LcmCrdt/**' + - 'backend/LocalWebApp/**' + - 'backend/MiniLcm/**' + - 'frontend/viewer/**' + - '.github/workflows/fw-lite.yaml' + branches: + - develop + pull_request: + paths: + - 'backend/FwDataMiniLcmBridge/**' + - 'backend/harmony/**' + - 'backend/LcmCrdt/**' + - 'backend/LocalWebApp/**' + - 'backend/MiniLcm/**' + - 'frontend/viewer/**' + - '.github/workflows/fw-lite.yaml' + branches: + - develop + +jobs: + build-app: + name: Build FW Lite + timeout-minutes: 20 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.x' + - uses: actions/setup-node@v4 + with: + node-version-file: './frontend/package.json' + - name: Dotnet build + working-directory: backend/LocalWebApp + run: dotnet build --configuration Release + - name: Dotnet test + working-directory: backend/LcmCrdt.Tests + run: dotnet test --configuration Release --logger:"xunit;LogFileName={assembly}.results.xml" --results-directory ./test-results + - name: Publish test results + uses: EnricoMi/publish-unit-test-result-action@8885e273a4343cd7b48eaa72428dea0c3067ea98 # v2.14.0 + if: always() + with: + check_name: FW Lite Unit Tests + files: ./backend/LcmCrdt.Tests/test-results/*.xml + - name: Build viewer + working-directory: frontend/viewer + run: | + corepack enable + pnpm install + pnpm run build-app + + - name: Publish linux + working-directory: backend/LocalWebApp + run: dotnet publish -r linux-x64 -o ./publish/linux + - name: Publish osx + working-directory: backend/LocalWebApp + run: dotnet publish -r osx-x64 -o ./publish/osx + - name: Publish win + working-directory: backend/LocalWebApp + run: dotnet publish -r win-x64 -o ./publish/win + - name: Upload artifacts + # uploading in one artifact as there's a lot of duplication between builds so compression goes far + uses: actions/upload-artifact@v4 + with: + name: fw-lite + if-no-files-found: error + path: backend/LocalWebApp/publish/* diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml index 990c008b0..cf8afe3d9 100644 --- a/.github/workflows/integration-test.yaml +++ b/.github/workflows/integration-test.yaml @@ -12,10 +12,15 @@ on: required: true type: string hg-version: - description: 'The version of hg to test (3 or 6, default 3)' + description: 'The version of hg to test (3 or 6, default 6)' required: false - default: '3' + default: '6' type: string + run-dotnet: + description: 'Whether to run dotnet tests' + required: false + default: true + type: boolean run-playwright: description: 'Whether to run Playwright tests' required: false @@ -32,20 +37,31 @@ on: required: true type: string hg-version: - description: 'The version of hg to test (3 or 6, default 3)' + description: 'The version of hg to test (3 or 6, default 6)' required: false - default: '3' + default: '6' type: string + run-dotnet: + description: 'Whether to run dotnet tests' + required: false + default: true + type: boolean run-playwright: description: 'Whether to run Playwright tests' required: false default: true type: boolean +# TODO: Set up caching for /home/runner/.cache/ms-playwright +# TODO: Ditto for /home/runner/.pnpm-store/v3 +# TODO: Ditto for /home/runner/.nuget/packages +# TODO: Ditto for /var/cache/apt/archives/ + jobs: - test: + dotnet-test: + if: ${{ inputs.run-dotnet }} timeout-minutes: 120 - name: Test ${{ inputs.runs-on }} for Mercurial ${{ inputs.hg-version }} on ${{ inputs.environment }} + name: Dotnet tests on ${{ inputs.runs-on }} for Mercurial ${{ inputs.hg-version }} on ${{ inputs.environment }} permissions: checks: write environment: @@ -68,23 +84,6 @@ jobs: sudo apt-get install -f rm powershell_7.4.1-1.deb_amd64.deb pwsh #validate that powershell installed correctly - # First we need to setup Node... - - uses: actions/setup-node@v4 - if: ${{ inputs.run-playwright }} - with: - node-version-file: 'frontend/package.json' - # Then we can set up pnpm... - - uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 - if: ${{ inputs.run-playwright }} - with: - package_json_file: 'frontend/package.json' - # Then we can have Noede set up package caching - - uses: actions/setup-node@v4 - if: ${{ inputs.run-playwright }} - with: - node-version-file: 'frontend/package.json' - cache: 'pnpm' - cache-dependency-path: 'frontend/pnpm-lock.yaml' - uses: actions/setup-dotnet@v4 env: DOTNET_INSTALL_DIR: ${{ inputs.runs-on == 'self-hosted' && '/opt/hostedtoolcache/dotnet' || '' }} #poor man's conditional @@ -104,25 +103,12 @@ jobs: - name: Dotnet build # limit cpu count for self-hosted due to memory constraints run: dotnet build ${{ inputs.runs-on == 'self-hosted' && '-maxcpucount:2' || '' }} - # TODO: Set up caching for /home/runner/.cache/ms-playwright so this doesn't take as long - # TODO: Ditto for /home/runner/.pnpm-store/v3 - # TODO: Ditto for /home/runner/.nuget/packages - # TODO: Ditto for /var/cache/apt/archives/ - - name: Playwright setup - working-directory: frontend - if: ${{ inputs.run-playwright }} - run: pnpm install && pnpm pretest - - name: Set up Playwright dependencies - working-directory: frontend - if: ${{ inputs.run-playwright && inputs.runs-on == 'self-hosted' }} - run: sudo pnpm exec playwright install-deps - name: Build for tests run: "dotnet build /p:MercurialVersion=$MERCURIAL_VERSION" env: MERCURIAL_VERSION: ${{ inputs.hg-version }} - - name: Integration tests (Playwright) - if: ${{ inputs.run-playwright }} + - name: Integration tests (.NET) env: TEST_SERVER_HOSTNAME: ${{ vars.TEST_SERVER_HOSTNAME }} # this is not a typo, we need to use the lf domain because it has a cert that hg will validate @@ -130,11 +116,70 @@ jobs: TEST_RESUMABLE_HG_HOSTNAME: ${{ vars.TEST_RESUMABLE_HG_HOSTNAME }} TEST_PROJECT_CODE: 'sena-3' TEST_DEFAULT_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }} - working-directory: frontend - run: pnpm test + # 1.5gb max heap size, must be in hex https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector#heap-limit + # the self hosted runner only has 2gb so this should prevent it from running out of memory + DOTNET_GCHeapHardLimit: '0x2CB417800' - - name: Integration tests (.NET) + run: dotnet test --output ./bin --logger trx --results-directory ./test-results --filter Category=Integration + - name: Publish unit test results + uses: EnricoMi/publish-unit-test-result-action/composite@8885e273a4343cd7b48eaa72428dea0c3067ea98 # v2.14.0 + if: ${{ always() && !env.act }} + with: + check_name: Integration Tests ${{ inputs.runs-on }} for Mercurial ${{ inputs.hg-version }} + files: ./test-results/*.trx + - name: Publish results to testspace if: always() + run: testspace "[.Net Integration/${{ inputs.runs-on }} HG ${{ inputs.hg-version }}]./test-results/*.trx" + + playwright-test: + if: ${{ inputs.run-playwright }} + timeout-minutes: 120 + name: Playwright tests on ${{ inputs.runs-on }} for Mercurial ${{ inputs.hg-version }} on ${{ inputs.environment }} + permissions: + checks: write + environment: + name: ${{ inputs.environment }} + runs-on: ${{ inputs.runs-on }} + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - uses: testspace-com/setup-testspace@v1.0.6 + with: + domain: ${{ github.repository_owner }} + - name: Setup self-hosted dependencies + if: ${{ inputs.runs-on == 'self-hosted' }} + run: | + sudo apt-get update + sudo apt-get install -y p7zip-full iputils-ping python3-venv wget zlib1g-dev libssl-dev #needed by setup python + wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.4.1/powershell_7.4.1-1.deb_amd64.deb + sudo dpkg -i powershell_7.4.1-1.deb_amd64.deb + sudo apt-get install -f + rm powershell_7.4.1-1.deb_amd64.deb + pwsh #validate that powershell installed correctly + # First we need to setup Node... + - uses: actions/setup-node@v4 + with: + node-version-file: 'frontend/package.json' + # Then we can set up pnpm... + - uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0 + with: + package_json_file: 'frontend/package.json' + # Then we can have Node set up package caching + - uses: actions/setup-node@v4 + with: + node-version-file: 'frontend/package.json' + cache: 'pnpm' + cache-dependency-path: 'frontend/pnpm-lock.yaml' + - name: Playwright setup + working-directory: frontend + run: pnpm install && pnpm pretest + - name: Set up Playwright dependencies + working-directory: frontend + if: ${{ inputs.runs-on == 'self-hosted' }} + run: sudo pnpm exec playwright install-deps + + - name: Integration tests (Playwright) env: TEST_SERVER_HOSTNAME: ${{ vars.TEST_SERVER_HOSTNAME }} # this is not a typo, we need to use the lf domain because it has a cert that hg will validate @@ -142,28 +187,18 @@ jobs: TEST_RESUMABLE_HG_HOSTNAME: ${{ vars.TEST_RESUMABLE_HG_HOSTNAME }} TEST_PROJECT_CODE: 'sena-3' TEST_DEFAULT_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }} - # 1.5gb max heap size, must be in hex https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector#heap-limit - # the self hosted runner only has 2gb so this should prevent it from running out of memory - DOTNET_GCHeapHardLimit: '0x2CB417800' - - run: dotnet test --output ./bin --logger trx --results-directory ./test-results --filter Category=Integration - + working-directory: frontend + run: pnpm test - name: Password protect Playwright traces id: password_protect_test_results - if: ${{ always() && inputs.run-playwright }} + if: ${{ always() }} shell: bash env: ZIP_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }} run: 7z a ./playwright-traces.7z -mx=0 -mmt=off ./frontend/test-results -p"$ZIP_PASSWORD" - - name: Publish unit test results - uses: EnricoMi/publish-unit-test-result-action/composite@8885e273a4343cd7b48eaa72428dea0c3067ea98 # v2.14.0 - if: ${{ always() && !env.act }} - with: - check_name: Integration Tests ${{ inputs.runs-on }} for Mercurial ${{ inputs.hg-version }} - files: ./test-results/*.trx - name: Publish results to testspace if: always() - run: testspace "[.Net Integration]./test-results/*.trx" "[Playwright]./frontend/test-results/*.xml" + run: testspace "[Playwright]./frontend/test-results/*.xml" - name: Upload playwright results if: ${{ always() && steps.password_protect_test_results.outcome == 'success' }} uses: actions/upload-artifact@v4 diff --git a/.github/workflows/lexbox-api.yaml b/.github/workflows/lexbox-api.yaml index aecbedc84..372ab865e 100644 --- a/.github/workflows/lexbox-api.yaml +++ b/.github/workflows/lexbox-api.yaml @@ -53,13 +53,14 @@ jobs: - name: Dotnet build run: dotnet build - name: Unit tests + working-directory: backend/Testing run: dotnet test --logger:"xunit;LogFileName={assembly}.results.xml" --results-directory ./test-results --filter "Category!=Integration" --blame-hang-timeout 10m - name: Publish unit test results uses: EnricoMi/publish-unit-test-result-action@8885e273a4343cd7b48eaa72428dea0c3067ea98 # v2.14.0 if: always() with: check_name: C# Unit Tests - files: ./test-results/*.xml + files: ./backend/Testing/test-results/*.xml - name: Upload test results if: always() uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index 2bea0cd70..32010732f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store bin/ obj/ +/data/ /packages/ riderModule.iml /_ReSharper.Caches/ @@ -17,3 +18,4 @@ dump.sql test-results/ **/*.sqlite **/*.sqlite-* +msal.cache diff --git a/.gitmodules b/.gitmodules index ff0f22e8f..59dd7daf6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "backend/harmony"] path = backend/harmony url = https://github.com/hahn-kev/harmony.git - branch = add-crdt + branch = chore/performance-pass diff --git a/.idea/.idea.LexBox/.idea/dataSources.xml b/.idea/.idea.LexBox/.idea/dataSources.xml index b6ad8028d..5661a55a2 100644 --- a/.idea/.idea.LexBox/.idea/dataSources.xml +++ b/.idea/.idea.LexBox/.idea/dataSources.xml @@ -86,5 +86,19 @@ + + postgresql + true + false + org.postgresql.Driver + jdbc:postgresql://localhost:5436/lexbox + + + + + + + $ProjectFileDir$ + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index cfb75b038..50341537b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,5 +29,6 @@ }, "files.associations": { "**/frontend/**/i18n/**/*.json": "json5", // Supports json5 syntax: https://svelte-intl-precompile.com/en/docs/getting-started - } + }, + "dotnet.defaultSolution": "LexBox.sln" } diff --git a/LexBox.sln b/LexBox.sln index e27567273..6ec82a3cc 100644 --- a/LexBox.sln +++ b/LexBox.sln @@ -37,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crdt", "backend\harmony\src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Crdt.Core", "backend\harmony\src\Crdt.Core\Crdt.Core.csproj", "{8B54FFB5-0BDF-403E-83CC-A3B3861EC507}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FwDataMiniLcmBridge", "backend\FwDataMiniLcmBridge\FwDataMiniLcmBridge.csproj", "{279197B6-EC06-4DE0-94F8-625379C3AD83}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +97,10 @@ Global {8B54FFB5-0BDF-403E-83CC-A3B3861EC507}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B54FFB5-0BDF-403E-83CC-A3B3861EC507}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B54FFB5-0BDF-403E-83CC-A3B3861EC507}.Release|Any CPU.Build.0 = Release|Any CPU + {279197B6-EC06-4DE0-94F8-625379C3AD83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {279197B6-EC06-4DE0-94F8-625379C3AD83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {279197B6-EC06-4DE0-94F8-625379C3AD83}.Release|Any CPU.ActiveCfg = Release|Any CPU + {279197B6-EC06-4DE0-94F8-625379C3AD83}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {E8BB768B-C3DC-4BE6-9B9F-82319E05AF86} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} @@ -104,5 +110,6 @@ Global {7D874D9B-1CA9-49E9-8B03-91B5C324E938} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} {740C8FF5-8006-4047-8C52-53873C2DD7C4} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} {8B54FFB5-0BDF-403E-83CC-A3B3861EC507} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} + {279197B6-EC06-4DE0-94F8-625379C3AD83} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090} EndGlobalSection EndGlobal diff --git a/Taskfile.yml b/Taskfile.yml index 42c02a7a6..63ac89079 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -5,6 +5,7 @@ version: '3' vars: TESTING_DIR: './backend/Testing' HG_REPO_DIR: './hgweb/repos' + DATA_DIR: './data' includes: k8s: taskfile: ./deployment/Taskfile.yml @@ -35,24 +36,18 @@ tasks: - echo "GOOGLE_OAUTH_CLIENT_ID=__REPLACE__.apps.googleusercontent.com" >> deployment/local-dev/local.env - echo "GOOGLE_OAUTH_CLIENT_SECRET=__REPLACE__" >> deployment/local-dev/local.env - kubectl --context=docker-desktop apply -f deployment/setup/namespace.yaml + - kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.0/cert-manager.yaml + - docker build -t local-dev-init data/ setup-win: platforms: [ windows ] cmds: - - cmd: powershell "if (test-path {{.HG_REPO_DIR}}/sena-3) { rm -r {{.HG_REPO_DIR}}/sena-3 }" - ignore_error: true - silent: true - - powershell -Command "Invoke-WebRequest 'https://drive.google.com/uc?export=download&id=1I-hwc0RHoQqW774gbS5qR-GHa1E7BlsS' -OutFile sena-3.zip" - - powershell -Command "Expand-Archive sena-3.zip -DestinationPath {{.HG_REPO_DIR}}" - - powershell rm sena-3.zip + - powershell -Command "Invoke-WebRequest 'https://drive.google.com/uc?export=download&id=1I-hwc0RHoQqW774gbS5qR-GHa1E7BlsS' -OutFile {{.DATA_DIR}}/sena-3.zip -Resume" + - powershell -Command "Invoke-WebRequest 'https://drive.usercontent.google.com/download?export=download&id=1Jk-eSDho8ATBMS-Kmfatwi-MWQth26ro&confirm=t' -OutFile {{.DATA_DIR}}/elawa.zip -Resume" setup-unix: platforms: [ linux, darwin ] cmds: - - cmd: rm -rf {{.HG_REPO_DIR}}/sena-3 - ignore_error: true - silent: true - - wget -O sena-3.zip 'https://drive.google.com/uc?export=download&id=1I-hwc0RHoQqW774gbS5qR-GHa1E7BlsS' - - unzip -q sena-3.zip -d {{.HG_REPO_DIR}}/ - - rm sena-3.zip + - wget -c -O {{.DATA_DIR}}/sena-3.zip 'https://drive.google.com/uc?export=download&id=1I-hwc0RHoQqW774gbS5qR-GHa1E7BlsS' + - wget -c -O {{.DATA_DIR}}/elawa.zip 'https://drive.usercontent.google.com/download?export=download&id=1Jk-eSDho8ATBMS-Kmfatwi-MWQth26ro&confirm=t' # k8s up: @@ -84,5 +79,8 @@ tasks: deps: [ infra-up, api:only, k8s:infra-forward ] interactive: true + local-web-app-for-develop: + deps: [ ui:viewer-dev, api:local-web-app-for-develop, ui:https-oauth-authority ] + local-web-app: - deps: [ ui:viewer-dev, api:local-web-app ] + deps: [ ui:viewer-dev, api:local-web-app, ui:https-oauth-authority ] diff --git a/backend/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs new file mode 100644 index 000000000..2f862442c --- /dev/null +++ b/backend/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -0,0 +1,465 @@ +using FwDataMiniLcmBridge.Api.UpdateProxy; +using Microsoft.Extensions.Logging; +using MiniLcm; +using SIL.LCModel; +using SIL.LCModel.Core.KernelInterfaces; +using SIL.LCModel.Core.Text; +using SIL.LCModel.Core.WritingSystems; +using SIL.LCModel.DomainServices; +using SIL.LCModel.Infrastructure; + +namespace FwDataMiniLcmBridge.Api; + +public class FwDataMiniLcmApi(LcmCache cache, bool onCloseSave, ILogger logger, FwDataProject project) : ILexboxApi, IDisposable +{ + public FwDataProject Project { get; } = project; + + private readonly IWritingSystemContainer _writingSystemContainer = + cache.ServiceLocator.WritingSystems; + + private readonly ILexEntryRepository _entriesRepository = + cache.ServiceLocator.GetInstance(); + + private readonly IRepository _senseRepository = + cache.ServiceLocator.GetInstance>(); + + private readonly IRepository _exampleSentenceRepository = + cache.ServiceLocator.GetInstance>(); + + private readonly ILexEntryFactory _lexEntryFactory = cache.ServiceLocator.GetInstance(); + private readonly ILexSenseFactory _lexSenseFactory = cache.ServiceLocator.GetInstance(); + + private readonly ILexExampleSentenceFactory _lexExampleSentenceFactory = + cache.ServiceLocator.GetInstance(); + + private readonly IMoMorphTypeRepository _morphTypeRepository = + cache.ServiceLocator.GetInstance(); + + private readonly ICmTranslationFactory _cmTranslationFactory = + cache.ServiceLocator.GetInstance(); + + private readonly ICmPossibilityRepository _cmPossibilityRepository = + cache.ServiceLocator.GetInstance(); + + public void Dispose() + { + if (onCloseSave) + { + Save(); + } + } + + public void Save() + { + logger.LogInformation("Saving FW data file {Name}", cache.ProjectId.Name); + cache.ActionHandlerAccessor.Commit(); + } + + public int EntryCount => _entriesRepository.Count; + + internal WritingSystemId GetWritingSystemId(int ws) + { + return cache.ServiceLocator.WritingSystemManager.Get(ws).Id; + } + + internal int GetWritingSystemHandle(WritingSystemId ws, WritingSystemType? type = null) + { + var lcmWs = GetLcmWritingSystem(ws, type) ?? throw new NullReferenceException($"Unable to find writing system with id {ws}"); + return lcmWs.Handle; + } + + internal CoreWritingSystemDefinition? GetLcmWritingSystem(WritingSystemId ws, WritingSystemType? type = null) + { + if (ws == "default") + { + return type switch + { + WritingSystemType.Analysis => _writingSystemContainer.DefaultAnalysisWritingSystem, + WritingSystemType.Vernacular => _writingSystemContainer.DefaultVernacularWritingSystem, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } + + var lcmWs = cache.ServiceLocator.WritingSystemManager.Get(ws.Code); + if (lcmWs is not null && type is not null) + { + var validWs = type switch + { + WritingSystemType.Analysis => _writingSystemContainer.AnalysisWritingSystems, + WritingSystemType.Vernacular => _writingSystemContainer.VernacularWritingSystems, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + if (!validWs.Contains(lcmWs)) + { + throw new InvalidOperationException($"Writing system {ws} is not of the requested type: {type}."); + } + } + return lcmWs; + } + + public Task GetWritingSystems() + { + var currentVernacularWs = _writingSystemContainer + .CurrentVernacularWritingSystems + .Select(ws => ws.Id).ToHashSet(); + var currentAnalysisWs = _writingSystemContainer + .CurrentAnalysisWritingSystems + .Select(ws => ws.Id).ToHashSet(); + var writingSystems = new WritingSystems + { + Vernacular = cache.ServiceLocator.WritingSystems.VernacularWritingSystems.Select(ws => new WritingSystem + { + //todo determine current and create a property for that. + Id = ws.Id, + Name = ws.LanguageTag, + Abbreviation = ws.Abbreviation, + Font = ws.DefaultFontName, + Exemplars = ws.CharacterSets.FirstOrDefault(s => s.Type == "index")?.Characters.ToArray() ?? [] + }).ToArray(), + Analysis = cache.ServiceLocator.WritingSystems.AnalysisWritingSystems.Select(ws => new WritingSystem + { + Id = ws.Id, + Name = ws.LanguageTag, + Abbreviation = ws.Abbreviation, + Font = ws.DefaultFontName, + Exemplars = ws.CharacterSets.FirstOrDefault(s => s.Type == "index")?.Characters.ToArray() ?? [] + }).ToArray() + }; + CompleteExemplars(writingSystems); + return Task.FromResult(writingSystems); + } + + internal void CompleteExemplars(WritingSystems writingSystems) + { + var wsExemplars = writingSystems.Vernacular.Concat(writingSystems.Analysis) + .Distinct() + .ToDictionary(ws => ws, ws => ws.Exemplars.ToHashSet()); + var wsExemplarsByHandle = wsExemplars.ToDictionary(kv => GetWritingSystemHandle(kv.Key.Id), kv => kv.Value); + + foreach (var entry in _entriesRepository.AllInstances()) + { + LcmHelpers.ContributeExemplars(entry.CitationForm, wsExemplarsByHandle); + LcmHelpers.ContributeExemplars(entry.LexemeFormOA.Form, wsExemplarsByHandle); + } + + foreach (var ws in wsExemplars.Keys) + { + ws.Exemplars = [.. wsExemplars[ws].Order()]; + } + } + + public Task CreateWritingSystem(WritingSystemType type, WritingSystem writingSystem) + { + throw new NotImplementedException(); + } + + public Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update) + { + throw new NotImplementedException(); + } + + private Entry FromLexEntry(ILexEntry entry) + { + return new Entry + { + Id = entry.Guid, + Note = FromLcmMultiString(entry.Comment), + LexemeForm = FromLcmMultiString(entry.LexemeFormOA.Form), + CitationForm = FromLcmMultiString(entry.CitationForm), + LiteralMeaning = FromLcmMultiString(entry.LiteralMeaning), + Senses = entry.AllSenses.Select(FromLexSense).ToList() + }; + } + + private Sense FromLexSense(ILexSense sense) + { + return new Sense + { + Id = sense.Guid, + Gloss = FromLcmMultiString(sense.Gloss), + Definition = FromLcmMultiString(sense.Definition), + PartOfSpeech = sense.SenseTypeRA?.Name.BestAnalysisVernacularAlternative.Text ?? string.Empty, + SemanticDomain = sense.SemanticDomainsRC.Select(s => s.OcmCodes).ToList(), + ExampleSentences = sense.ExamplesOS.Select(FromLexExampleSentence).ToList() + }; + } + + private ExampleSentence FromLexExampleSentence(ILexExampleSentence sentence) + { + var translation = sentence.TranslationsOC.FirstOrDefault()?.Translation; + return new ExampleSentence + { + Id = sentence.Guid, + Sentence = FromLcmMultiString(sentence.Example), + Reference = sentence.Reference.Text, + Translation = translation is null ? new MultiString() : FromLcmMultiString(translation), + }; + } + + private MultiString FromLcmMultiString(ITsMultiString multiString) + { + var result = new MultiString(); + for (var i = 0; i < multiString.StringCount; i++) + { + var tsString = multiString.GetStringFromIndex(i, out var ws); + result.Values.Add(GetWritingSystemId(ws), tsString.Text); + } + + return result; + } + + public async IAsyncEnumerable GetEntries(QueryOptions? options = null) + { + await foreach (var entry in GetEntries(null, options)) + { + yield return entry; + } + } + + public async IAsyncEnumerable GetEntries( + Func? predicate = null, QueryOptions? options = null) + { + var entries = _entriesRepository.AllInstances(); + + options ??= QueryOptions.Default; + if (predicate is not null) entries = entries.Where(e => predicate(e)); + + if (options.Exemplar is not null) + { + var ws = GetWritingSystemHandle(options.Exemplar.WritingSystem, WritingSystemType.Vernacular); + entries = entries.Where(e => (e.CitationForm.get_String(ws).Text ?? e.LexemeFormOA.Form.get_String(ws).Text)? + .Trim(LcmHelpers.WhitespaceAndFormattingChars) + .StartsWith(options.Exemplar.Value, StringComparison.InvariantCultureIgnoreCase) ?? false); + } + + var sortWs = GetWritingSystemHandle(options.Order.WritingSystem, WritingSystemType.Vernacular); + entries = entries.OrderBy(e => (e.CitationForm.get_String(sortWs).Text ?? e.LexemeFormOA.Form.get_String(sortWs).Text).Trim(LcmHelpers.WhitespaceChars)) + .Skip(options.Offset) + .Take(options.Count); + + foreach (var entry in entries) + { + yield return FromLexEntry(entry); + } + } + + public IAsyncEnumerable SearchEntries(string query, QueryOptions? options = null) + { + var entries = GetEntries(e => + e.CitationForm.SearchValue(query) || + e.LexemeFormOA.Form.SearchValue(query) || + e.SensesOS.Any(s => s.Gloss.SearchValue(query)), options); + return entries; + } + + public Task GetEntry(Guid id) + { + return Task.FromResult(FromLexEntry(_entriesRepository.GetObject(id))); + } + + public async Task CreateEntry(Entry entry) + { + // TODO: The API requires a value and the UI assumes it has a value. How should we handle this? + // if (entry.Id != default) throw new NotSupportedException("Id must be empty"); + Guid entryId = default; + UndoableUnitOfWorkHelper.Do("Create Entry", + "Remove entry", + cache.ServiceLocator.ActionHandler, + () => + { + var rootMorphType = _morphTypeRepository.GetObject(MoMorphTypeTags.kguidMorphRoot); + var firstSense = entry.Senses.FirstOrDefault(); + var lexEntry = _lexEntryFactory.Create(new LexEntryComponents + { + MorphType = rootMorphType, + LexemeFormAlternatives = MultiStringToTsStrings(entry.LexemeForm), + GlossAlternatives = MultiStringToTsStrings(firstSense?.Gloss), + GlossFeatures = [], + MSA = null + }); + UpdateLcmMultiString(lexEntry.CitationForm, entry.CitationForm); + UpdateLcmMultiString(lexEntry.LiteralMeaning, entry.LiteralMeaning); + UpdateLcmMultiString(lexEntry.Comment, entry.Note); + if (firstSense is not null) + { + var lexSense = lexEntry.SensesOS.First(); + ApplySenseToLexSense(firstSense, lexSense); + } + + //first sense is already created + foreach (var sense in entry.Senses.Skip(1)) + { + CreateSense(lexEntry, sense); + } + + entryId = lexEntry.Guid; + }); + if (entryId == default) throw new InvalidOperationException("Entry was not created"); + + return await GetEntry(entryId) ?? throw new InvalidOperationException("Entry was not found"); + } + + private IList MultiStringToTsStrings(MultiString? multiString) + { + if (multiString is null) return []; + var result = new List(multiString.Values.Count); + foreach (var (ws, value) in multiString.Values) + { + result.Add(TsStringUtils.MakeString(value, GetWritingSystemHandle(ws))); + } + + return result; + } + + private void UpdateLcmMultiString(ITsMultiString multiString, MultiString newMultiString) + { + foreach (var (ws, value) in newMultiString.Values) + { + var writingSystemHandle = GetWritingSystemHandle(ws); + multiString.set_String(writingSystemHandle, TsStringUtils.MakeString(value, writingSystemHandle)); + } + } + + public Task UpdateEntry(Guid id, UpdateObjectInput update) + { + var lexEntry = _entriesRepository.GetObject(id); + UndoableUnitOfWorkHelper.Do("Update Entry", + "Revert entry", + cache.ServiceLocator.ActionHandler, + () => + { + var updateProxy = new UpdateEntryProxy(lexEntry, this); + update.Apply(updateProxy); + }); + return Task.FromResult(FromLexEntry(lexEntry)); + } + + public Task DeleteEntry(Guid id) + { + UndoableUnitOfWorkHelper.Do("Delete Entry", + "Revert delete", + cache.ServiceLocator.ActionHandler, + () => + { + _entriesRepository.GetObject(id).Delete(); + }); + return Task.CompletedTask; + } + + internal void CreateSense(ILexEntry lexEntry, Sense sense) + { + var lexSense = _lexSenseFactory.Create(sense.Id, lexEntry); + ApplySenseToLexSense(sense, lexSense); + } + + private void ApplySenseToLexSense(Sense sense, ILexSense lexSense) + { + UpdateLcmMultiString(lexSense.Gloss, sense.Gloss); + UpdateLcmMultiString(lexSense.Definition, sense.Definition); + foreach (var exampleSentence in sense.ExampleSentences) + { + CreateExampleSentence(lexSense, exampleSentence); + } + } + + public Task CreateSense(Guid entryId, Sense sense) + { + if (sense.Id != default) sense.Id = Guid.NewGuid(); + if (!_entriesRepository.TryGetObject(entryId, out var lexEntry)) + throw new InvalidOperationException("Entry not found"); + UndoableUnitOfWorkHelper.Do("Create Sense", + "Remove sense", + cache.ServiceLocator.ActionHandler, + () => CreateSense(lexEntry, sense)); + return Task.FromResult(FromLexSense(_senseRepository.GetObject(sense.Id))); + } + + public Task UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) + { + var lexSense = _senseRepository.GetObject(senseId); + if (lexSense.Owner.Guid != entryId) throw new InvalidOperationException("Sense does not belong to entry"); + UndoableUnitOfWorkHelper.Do("Update Sense", + "Revert sense", + cache.ServiceLocator.ActionHandler, + () => + { + var updateProxy = new UpdateSenseProxy(lexSense, this); + update.Apply(updateProxy); + }); + return Task.FromResult(FromLexSense(lexSense)); + } + + public Task DeleteSense(Guid entryId, Guid senseId) + { + var lexSense = _senseRepository.GetObject(senseId); + if (lexSense.Owner.Guid != entryId) throw new InvalidOperationException("Sense does not belong to entry"); + UndoableUnitOfWorkHelper.Do("Delete Sense", + "Revert delete", + cache.ServiceLocator.ActionHandler, + () => lexSense.Delete()); + return Task.CompletedTask; + } + + internal void CreateExampleSentence(ILexSense lexSense, ExampleSentence exampleSentence) + { + var lexExampleSentence = _lexExampleSentenceFactory.Create(exampleSentence.Id, lexSense); + UpdateLcmMultiString(lexExampleSentence.Example, exampleSentence.Sentence); + var freeTranslationType = _cmPossibilityRepository.GetObject(CmPossibilityTags.kguidTranFreeTranslation); + var translation = _cmTranslationFactory.Create(lexExampleSentence, freeTranslationType); + UpdateLcmMultiString(translation.Translation, exampleSentence.Translation); + lexExampleSentence.Reference = TsStringUtils.MakeString(exampleSentence.Reference, + lexExampleSentence.Reference.get_WritingSystem(0)); + } + + public Task CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence) + { + if (exampleSentence.Id != default) exampleSentence.Id = Guid.NewGuid(); + if (!_senseRepository.TryGetObject(senseId, out var lexSense)) + throw new InvalidOperationException("Sense not found"); + UndoableUnitOfWorkHelper.Do("Create Example Sentence", + "Remove example sentence", + cache.ServiceLocator.ActionHandler, + () => CreateExampleSentence(lexSense, exampleSentence)); + return Task.FromResult(FromLexExampleSentence(_exampleSentenceRepository.GetObject(exampleSentence.Id))); + } + + public Task UpdateExampleSentence(Guid entryId, + Guid senseId, + Guid exampleSentenceId, + UpdateObjectInput update) + { + var lexExampleSentence = _exampleSentenceRepository.GetObject(exampleSentenceId); + if (lexExampleSentence.Owner.Guid != senseId) + throw new InvalidOperationException("Example sentence does not belong to sense"); + if (lexExampleSentence.Owner.Owner.Guid != entryId) + throw new InvalidOperationException("Example sentence does not belong to entry"); + UndoableUnitOfWorkHelper.Do("Update Example Sentence", + "Revert example sentence", + cache.ServiceLocator.ActionHandler, + () => + { + var updateProxy = new UpdateExampleSentenceProxy(lexExampleSentence, this); + update.Apply(updateProxy); + }); + return Task.FromResult(FromLexExampleSentence(lexExampleSentence)); + } + + public Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId) + { + var lexExampleSentence = _exampleSentenceRepository.GetObject(exampleSentenceId); + if (lexExampleSentence.Owner.Guid != senseId) + throw new InvalidOperationException("Example sentence does not belong to sense"); + if (lexExampleSentence.Owner.Owner.Guid != entryId) + throw new InvalidOperationException("Example sentence does not belong to entry"); + UndoableUnitOfWorkHelper.Do("Delete Example Sentence", + "Revert delete", + cache.ServiceLocator.ActionHandler, + () => lexExampleSentence.Delete()); + return Task.CompletedTask; + } + + public UpdateBuilder CreateUpdateBuilder() where T : class + { + return new UpdateBuilder(); + } +} diff --git a/backend/FwDataMiniLcmBridge/Api/LcmHelpers.cs b/backend/FwDataMiniLcmBridge/Api/LcmHelpers.cs new file mode 100644 index 000000000..351022e8c --- /dev/null +++ b/backend/FwDataMiniLcmBridge/Api/LcmHelpers.cs @@ -0,0 +1,68 @@ +using SIL.LCModel.Core.KernelInterfaces; + +namespace FwDataMiniLcmBridge.Api; + +internal static class LcmHelpers +{ + internal static bool SearchValue(this ITsMultiString multiString, string value) + { + var valueLower = value.ToLowerInvariant(); + for (var i = 0; i < multiString.StringCount; i++) + { + var tsString = multiString.GetStringFromIndex(i, out var _); + if (tsString.Text?.ToLowerInvariant().Contains(valueLower) is true) + { + return true; + } + } + return false; + } + + internal static readonly char[] WhitespaceChars = + [ + '\u0009', // Tab + '\u000A', // Line Feed + '\u000D', // Carriage Return + '\u0020', // Space + '\u00A0', // Non-breaking Space + '\u2002', // En Space + '\u2003', // Em Space + '\u2004', // Three-Per-Em Space + '\u2005', // Four-Per-Em Space + '\u2006', // Six-Per-Em Space + '\u2007', // Figure Space + '\u2008', // Punctuation Space + '\u2009', // Thin Space + '\u200A', // Hair Space + '\u200B', // Zero Width Space + '\u200C', // Zero Width Non-Joiner + '\u200D', // Zero Width Joiner + '\u200E', // Left-to-Right Mark + '\u200F', // Right-to-Left Mark + '\u2028', // Line Separator + '\u2029', // Paragraph Separator + '\u202F', // Narrow No-Break Space + '\u205F', // Medium Mathematical Space + '\u3000', // Ideographic Space + '\uFEFF', // Zero Width No-Break Space / BOM + ]; + + internal static readonly char[] WhitespaceAndFormattingChars = + [ + .. WhitespaceChars, + '\u0640', // Arabic Tatweel + ]; + + internal static void ContributeExemplars(ITsMultiString multiString, Dictionary> wsExemplars) + { + for (var i = 0; i < multiString.StringCount; i++) + { + var tsString = multiString.GetStringFromIndex(i, out var ws); + var value = tsString.Text?.Trim(WhitespaceAndFormattingChars); + if (value?.Any() is true && wsExemplars.TryGetValue(ws, out var exemplars)) + { + exemplars.Add(value.First().ToString()); + } + } + } +} diff --git a/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateDictionaryProxy.cs b/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateDictionaryProxy.cs new file mode 100644 index 000000000..9060ecf82 --- /dev/null +++ b/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateDictionaryProxy.cs @@ -0,0 +1,165 @@ +using System.Collections; +using MiniLcm; +using SIL.LCModel.Core.KernelInterfaces; +using SIL.LCModel.Core.Text; + +namespace FwDataMiniLcmBridge.Api.UpdateProxy; + +public class UpdateDictionaryProxy(ITsMultiString multiString, FwDataMiniLcmApi lexboxLcmApi) + : IDictionary, IDictionary +{ + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public void Add(WritingSystemId key, string value) + { + var writingSystemHandle = lexboxLcmApi.GetWritingSystemHandle(key); + multiString.set_String(writingSystemHandle, TsStringUtils.MakeString(value, writingSystemHandle)); + } + + public bool ContainsKey(WritingSystemId key) + { + if (multiString.StringCount == 0) return false; + var tsString = multiString.get_String(lexboxLcmApi.GetWritingSystemHandle(key)); + return tsString.Length > 0; + } + + public void Add(object key, object? value) + { + var valStr = value as string ?? throw new ArgumentException("unable to convert value to string", nameof(value)); + if (key is WritingSystemId keyWs) + { + Add(keyWs, valStr); + } + else if (key is string keyStr) + { + Add(keyStr, valStr); + } + else + { + throw new ArgumentException("unable to convert key to writing system id", nameof(key)); + } + } + + public bool Contains(object key) + { + return ContainsKey(key as WritingSystemId? ?? key as string ?? + throw new ArgumentException("unable to convert key to writing system id", nameof(key))); + } + + + public string this[WritingSystemId key] + { + get + { + var tsString = multiString.get_String(lexboxLcmApi.GetWritingSystemHandle(key)); + return tsString.Text; + } + set + { + var writingSystemHandle = lexboxLcmApi.GetWritingSystemHandle(key); + multiString.set_String(writingSystemHandle, TsStringUtils.MakeString(value, writingSystemHandle)); + } + } + + public object? this[object key] + { + get => + key switch + { + WritingSystemId keyWs => this[keyWs], + string keyStr => this[keyStr], + _ => throw new ArgumentException("unable to convert key to writing system id", nameof(key)) + }; + set + { + var valStr = value as string ?? + throw new ArgumentException("unable to convert value to string", nameof(value)); + if (key is WritingSystemId keyWs) + { + this[keyWs] = valStr; + } + else if (key is string keyStr) + { + this[keyStr] = valStr; + } + else + { + throw new ArgumentException("unable to convert key to writing system id", nameof(key)); + } + } + } + + IDictionaryEnumerator IDictionary.GetEnumerator() + { + throw new NotSupportedException(); + } + + public void Remove(object key) + { + } + + public bool IsFixedSize => false; + + public IEnumerator> GetEnumerator() + { + throw new NotSupportedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Clear() + { + throw new NotSupportedException(); + } + + public bool Contains(KeyValuePair item) + { + throw new NotSupportedException(); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotSupportedException(); + } + + public bool Remove(KeyValuePair item) + { + throw new NotSupportedException(); + } + + public void CopyTo(Array array, int index) + { + } + + public int Count => throw new NotSupportedException(); + + public bool IsSynchronized => false; + + public object SyncRoot => this; + + public bool IsReadOnly => false; + + public bool Remove(WritingSystemId key) + { + throw new NotSupportedException(); + } + + public bool TryGetValue(WritingSystemId key, out string value) + { + throw new NotSupportedException(); + } + + public ICollection Keys => []; + + ICollection IDictionary.Values => Array.Empty(); + + ICollection IDictionary.Keys => Array.Empty(); + + public ICollection Values => []; +} diff --git a/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs b/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs new file mode 100644 index 000000000..f48470ca1 --- /dev/null +++ b/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs @@ -0,0 +1,59 @@ +using MiniLcm; +using SIL.LCModel; +using SIL.LCModel.Core.KernelInterfaces; + +namespace FwDataMiniLcmBridge.Api.UpdateProxy; + +public class UpdateEntryProxy(ILexEntry lcmEntry, FwDataMiniLcmApi lexboxLcmApi) : Entry +{ + public override Guid Id + { + get => lcmEntry.Guid; + set => throw new NotImplementedException(); + } + + public override MultiString LexemeForm + { + get => new UpdateMultiStringProxy(lcmEntry.LexemeFormOA.Form, lexboxLcmApi); + set => throw new NotImplementedException(); + } + + public override MultiString CitationForm + { + get => new UpdateMultiStringProxy(lcmEntry.CitationForm, lexboxLcmApi); + set => throw new NotImplementedException(); + } + + public override MultiString LiteralMeaning + { + get => new UpdateMultiStringProxy(lcmEntry.LiteralMeaning, lexboxLcmApi); + set => throw new NotImplementedException(); + } + + public override IList Senses + { + get => + new UpdateListProxy( + sense => lexboxLcmApi.CreateSense(lcmEntry, sense), + sense => lexboxLcmApi.DeleteSense(Id, sense.Id), + i => new UpdateSenseProxy(lcmEntry.SensesOS[i], lexboxLcmApi) + ); + set => throw new NotImplementedException(); + } + + public override MultiString Note + { + get => new UpdateMultiStringProxy(lcmEntry.Comment, lexboxLcmApi); + set => throw new NotImplementedException(); + } +} + +public class UpdateMultiStringProxy(ITsMultiString multiString, FwDataMiniLcmApi lexboxLcmApi) : MultiString +{ + public override IDictionary Values { get; } = new UpdateDictionaryProxy(multiString, lexboxLcmApi); + + public override MultiString Copy() + { + return new UpdateMultiStringProxy(multiString, lexboxLcmApi); + } +} diff --git a/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateExampleSentenceProxy.cs b/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateExampleSentenceProxy.cs new file mode 100644 index 000000000..c764d2fae --- /dev/null +++ b/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateExampleSentenceProxy.cs @@ -0,0 +1,36 @@ +using MiniLcm; +using SIL.LCModel; +using SIL.LCModel.Core.Text; + +namespace FwDataMiniLcmBridge.Api.UpdateProxy; + +public class UpdateExampleSentenceProxy(ILexExampleSentence sentence, FwDataMiniLcmApi lexboxLcmApi): ExampleSentence +{ + public override Guid Id + { + get => sentence.Guid; + set => throw new NotImplementedException(); + } + + public override MultiString Sentence + { + get => new UpdateMultiStringProxy(sentence.Example, lexboxLcmApi); + set => throw new NotImplementedException(); + } + + public override MultiString Translation + { + get + { + var firstTranslation = sentence.TranslationsOC.FirstOrDefault()?.Translation; + return firstTranslation is null ? new MultiString() : new UpdateMultiStringProxy(firstTranslation, lexboxLcmApi); + } + set => throw new NotImplementedException(); + } + + public override string? Reference + { + get => throw new NotImplementedException(); + set => sentence.Reference = TsStringUtils.MakeString(value, sentence.Reference.get_WritingSystem(0)); + } +} diff --git a/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateListProxy.cs b/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateListProxy.cs new file mode 100644 index 000000000..dc21c62bf --- /dev/null +++ b/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateListProxy.cs @@ -0,0 +1,74 @@ +using System.Collections; + +namespace FwDataMiniLcmBridge.Api.UpdateProxy; + +public class UpdateListProxy( + Action add, + Action remove, + Func getAt) : IList +{ + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + + public void Add(T item) + { + add(item); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(T item) + { + throw new NotImplementedException(); + } + + public void CopyTo(T[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public bool Remove(T item) + { + remove(item); + return false; + } + + public int Count => throw new NotImplementedException(); + + public bool IsReadOnly => false; + + public int IndexOf(T item) + { + throw new NotImplementedException(); + } + + public void Insert(int index, T item) + { + add(item); + } + + public void RemoveAt(int index) + { + Remove(getAt(index)); + } + + public T this[int index] + { + get => getAt(index); + set + { + RemoveAt(index); + Insert(index, value); + } + } +} \ No newline at end of file diff --git a/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs b/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs new file mode 100644 index 000000000..83239bc88 --- /dev/null +++ b/backend/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs @@ -0,0 +1,48 @@ +using MiniLcm; +using SIL.LCModel; + +namespace FwDataMiniLcmBridge.Api.UpdateProxy; + +public class UpdateSenseProxy(ILexSense sense, FwDataMiniLcmApi lexboxLcmApi) : Sense +{ + public override Guid Id + { + get => sense.Guid; + set => throw new NotImplementedException(); + } + + public override MultiString Definition + { + get => new UpdateMultiStringProxy(sense.Definition, lexboxLcmApi); + set => throw new NotImplementedException(); + } + + public override MultiString Gloss + { + get => new UpdateMultiStringProxy(sense.Gloss, lexboxLcmApi); + set => throw new NotImplementedException(); + } + + public override string PartOfSpeech + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public override IList SemanticDomain + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public override IList ExampleSentences + { + get => + new UpdateListProxy( + sentence => lexboxLcmApi.CreateExampleSentence(sense, sentence), + sentence => lexboxLcmApi.DeleteExampleSentence(sense.Owner.Guid, Id, sentence.Id), + i => new UpdateExampleSentenceProxy(sense.ExamplesOS[i], lexboxLcmApi) + ); + set => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/backend/FwDataMiniLcmBridge/FieldWorksProjectList.cs b/backend/FwDataMiniLcmBridge/FieldWorksProjectList.cs new file mode 100644 index 000000000..a62439796 --- /dev/null +++ b/backend/FwDataMiniLcmBridge/FieldWorksProjectList.cs @@ -0,0 +1,23 @@ +using FwDataMiniLcmBridge.LcmUtils; +using MiniLcm; + +namespace FwDataMiniLcmBridge; + +public class FieldWorksProjectList +{ + public static IEnumerable EnumerateProjects() + { + foreach (var directory in Directory.EnumerateDirectories(ProjectLoader.ProjectFolder)) + { + var projectName = Path.GetFileName(directory); + if (string.IsNullOrEmpty(projectName)) continue; + if (!File.Exists(Path.Combine(directory, projectName + ".fwdata"))) continue; + yield return new FwDataProject(projectName, projectName + ".fwdata"); + } + } + + public static FwDataProject? GetProject(string name) + { + return EnumerateProjects().OfType().FirstOrDefault(p => p.Name == name); + } +} diff --git a/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs b/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs new file mode 100644 index 000000000..59d8de9ac --- /dev/null +++ b/backend/FwDataMiniLcmBridge/FwDataBridgeKernel.cs @@ -0,0 +1,20 @@ +using FwDataMiniLcmBridge.LcmUtils; +using Microsoft.Extensions.DependencyInjection; +using MiniLcm; + +namespace FwDataMiniLcmBridge; + +public static class FwDataBridgeKernel +{ + public const string FwDataApiKey = "FwDataApiKey"; + public static IServiceCollection AddFwDataBridge(this IServiceCollection services) + { + services.AddMemoryCache(); + services.AddSingleton(); + //todo since this is scoped it gets created on each request (or hub method call), which opens the project file on each request + //this is not ideal since opening the project file can be slow. It should be done once per hub connection. + services.AddKeyedScoped(FwDataApiKey, (provider, o) => provider.GetRequiredService().GetCurrentFwDataMiniLcmApi(true)); + services.AddSingleton(); + return services; + } +} diff --git a/backend/FwDataMiniLcmBridge/FwDataFactory.cs b/backend/FwDataMiniLcmBridge/FwDataFactory.cs new file mode 100644 index 000000000..16497de43 --- /dev/null +++ b/backend/FwDataMiniLcmBridge/FwDataFactory.cs @@ -0,0 +1,88 @@ +using FwDataMiniLcmBridge.Api; +using FwDataMiniLcmBridge.LcmUtils; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using SIL.LCModel; + +namespace FwDataMiniLcmBridge; + +public class FwDataFactory(FwDataProjectContext context, ILogger fwdataLogger, IMemoryCache cache, ILogger logger): IDisposable +{ + public FwDataMiniLcmApi GetFwDataMiniLcmApi(string projectName, bool saveOnDispose) + { + var project = FieldWorksProjectList.GetProject(projectName) ?? throw new InvalidOperationException($"Project {projectName} not found."); + return GetFwDataMiniLcmApi(project, saveOnDispose); + } + + private string CacheKey(FwDataProject project) => $"{nameof(FwDataFactory)}|{project.FileName}"; + + public FwDataMiniLcmApi GetFwDataMiniLcmApi(FwDataProject project, bool saveOnDispose) + { + var projectService = GetProjectServiceCached(project); + return new FwDataMiniLcmApi(projectService, saveOnDispose, fwdataLogger, project); + } + + private HashSet _projects = []; + private LcmCache GetProjectServiceCached(FwDataProject project) + { + var key = CacheKey(project); + var projectService = cache.GetOrCreate(key, + entry => + { + entry.SlidingExpiration = TimeSpan.FromMinutes(30); + entry.RegisterPostEvictionCallback(OnLcmProjectCacheEviction, (logger, _projects)); + logger.LogInformation("Loading project {ProjectFileName}", project.FileName); + var projectService = ProjectLoader.LoadCache(project.FileName); + logger.LogInformation("Project {ProjectFileName} loaded", project.FileName); + _projects.Add((string)entry.Key); + return projectService; + }); + if (projectService is null) + { + throw new InvalidOperationException("Project service is null"); + } + if (projectService.IsDisposed) + { + throw new InvalidOperationException("Project service is disposed"); + } + + return projectService; + } + + private static void OnLcmProjectCacheEviction(object key, object? value, EvictionReason reason, object? state) + { + if (value is null) return; + // todo this could trigger when the service is still referenced elsewhere, for example in a long running task. + // disposing of the service while it's still in use would be bad. + // one way around this would be to return a lease object, only after a timeout and no more references to the lease object would the service be disposed. + var lcmCache = (LcmCache)value; + var (logger, projects) = ((ILogger, HashSet))state!; + var name = lcmCache.ProjectId.Name; + logger.LogInformation("Evicting project {ProjectFileName} from cache", name); + lcmCache.Dispose(); + logger.LogInformation("FW Data Project {ProjectFileName} disposed", name); + projects.Remove((string)key); + } + + public void Dispose() + { + foreach (var project in _projects) + { + var lcmCache = cache.Get(project); + if (lcmCache is null) continue; + var name = lcmCache.ProjectId.Name; + lcmCache.Dispose();//need to explicitly call dispose as that blocks, just removing from the cache does not block, meaning it will not finish disposing before the program exits. + logger.LogInformation("FW Data Project {ProjectFileName} disposed", name); + } + } + + public FwDataMiniLcmApi GetCurrentFwDataMiniLcmApi(bool saveOnDispose) + { + var fwDataProject = context.Project; + if (fwDataProject is null) + { + throw new InvalidOperationException("No project is set in the context."); + } + return GetFwDataMiniLcmApi(fwDataProject, true); + } +} diff --git a/backend/FwDataMiniLcmBridge/FwDataMiniLcmBridge.csproj b/backend/FwDataMiniLcmBridge/FwDataMiniLcmBridge.csproj new file mode 100644 index 000000000..f7b18e4ae --- /dev/null +++ b/backend/FwDataMiniLcmBridge/FwDataMiniLcmBridge.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + diff --git a/backend/FwDataMiniLcmBridge/FwDataProject.cs b/backend/FwDataMiniLcmBridge/FwDataProject.cs new file mode 100644 index 000000000..68dd25fd1 --- /dev/null +++ b/backend/FwDataMiniLcmBridge/FwDataProject.cs @@ -0,0 +1,10 @@ +using MiniLcm; + +namespace FwDataMiniLcmBridge; + +public class FwDataProject(string name, string fileName) : IProjectIdentifier +{ + public string Name { get; } = name; + public string FileName { get; } = fileName; + public string Origin { get; } = "FieldWorks"; +} diff --git a/backend/FwDataMiniLcmBridge/FwDataProjectContext.cs b/backend/FwDataMiniLcmBridge/FwDataProjectContext.cs new file mode 100644 index 000000000..b40d03407 --- /dev/null +++ b/backend/FwDataMiniLcmBridge/FwDataProjectContext.cs @@ -0,0 +1,32 @@ +namespace FwDataMiniLcmBridge; + +public class FwDataProjectContext +{ + private sealed class ProjectHolder + { + public FwDataProject? Project; + } + + private static readonly AsyncLocal _projectHolder = new(); + + public virtual FwDataProject? Project + { + get => _projectHolder.Value?.Project; + set + { + var holder = _projectHolder.Value; + if (holder != null) + { + // Clear current Project trapped in the AsyncLocals, as its done. + holder.Project = null; + } + + if (value is not null) + { + // Use an object indirection to hold the Project in the AsyncLocal, + // so it can be cleared in all ExecutionContexts when its cleared above. + _projectHolder.Value = new ProjectHolder { Project = value }; + } + } + } +} diff --git a/backend/FwDataMiniLcmBridge/LcmUtils/LcmDirectories.cs b/backend/FwDataMiniLcmBridge/LcmUtils/LcmDirectories.cs new file mode 100644 index 000000000..15df281d8 --- /dev/null +++ b/backend/FwDataMiniLcmBridge/LcmUtils/LcmDirectories.cs @@ -0,0 +1,5 @@ +using SIL.LCModel; + +namespace FwDataMiniLcmBridge.LcmUtils; + +public record LcmDirectories(string ProjectsDirectory, string TemplateDirectory) : ILcmDirectories; diff --git a/backend/FwDataMiniLcmBridge/LcmUtils/LcmThreadedProgress.cs b/backend/FwDataMiniLcmBridge/LcmUtils/LcmThreadedProgress.cs new file mode 100644 index 000000000..fe7466b00 --- /dev/null +++ b/backend/FwDataMiniLcmBridge/LcmUtils/LcmThreadedProgress.cs @@ -0,0 +1,52 @@ +using System.ComponentModel; +using SIL.LCModel.Utils; + +namespace FwDataMiniLcmBridge.LcmUtils; + +public class LcmThreadedProgress : IThreadedProgress +{ + private SingleThreadedSynchronizeInvoke _synchronizeInvoke = new(); + + public event CancelEventHandler? Canceling; // this is part of the interface + + public void Step(int amount) + { + } + + public string? Title { get; set; } + public string? Message { get; set; } + public int Position { get; set; } + public int StepSize { get; set; } + public int Minimum { get; set; } + public int Maximum { get; set; } + + public ISynchronizeInvoke SynchronizeInvoke + { + get { return _synchronizeInvoke; } + } + + public bool IsIndeterminate { get; set; } + public bool AllowCancel { get; set; } + + public object RunTask(Func backgroundTask, params object[] parameters) + { + return backgroundTask(this, parameters); + } + + public object RunTask(bool fDisplayUi, + Func backgroundTask, + params object[] parameters) + { + return backgroundTask(this, parameters); + } + + public bool Canceled + { + get { return false; } + } + + public bool IsCanceling + { + get { return false; } + } +} diff --git a/backend/FwDataMiniLcmBridge/LcmUtils/LcmUi.cs b/backend/FwDataMiniLcmBridge/LcmUtils/LcmUi.cs new file mode 100644 index 000000000..04aef60cb --- /dev/null +++ b/backend/FwDataMiniLcmBridge/LcmUtils/LcmUi.cs @@ -0,0 +1,74 @@ +using System.ComponentModel; +using SIL.LCModel; + +namespace FwDataMiniLcmBridge.LcmUtils; + +public class LfLcmUi(ISynchronizeInvoke synchronizeInvoke) : ILcmUI +{ + public void DisplayCircularRefBreakerReport(string msg, string caption) + { + Console.WriteLine(msg); + } + + public bool ConflictingSave() + { + Console.WriteLine("ConsoleLcmUI.ConflictingSave..."); + // Revert to saved state + return true; + } + + public bool ConnectionLost() + { + throw new NotImplementedException(); + } + + public FileSelection ChooseFilesToUse() + { + throw new NotImplementedException(); + } + + public bool RestoreLinkedFilesInProjectFolder() + { + throw new NotImplementedException(); + } + + public YesNoCancel CannotRestoreLinkedFilesToOriginalLocation() + { + throw new NotImplementedException(); + } + + public void DisplayMessage(MessageType type, string message, string caption, string helpTopic) + { + Console.WriteLine("{0}: {1}", type, message); + } + + public void ReportException(Exception error, bool isLethal) + { + Console.WriteLine("Got exception: {0}: {1}\n{2}", error.GetType(), error.Message, error); + } + + public void ReportDuplicateGuids(string errorText) + { + Console.WriteLine("Duplicate GUIDs: " + errorText); + } + + public bool Retry(string msg, string caption) + { + Console.WriteLine(msg); + return true; + } + + public bool OfferToRestore(string projectPath, string backupPath) + { + return false; + } + + public void Exit() + { + Console.WriteLine("Exiting"); + } + + public ISynchronizeInvoke SynchronizeInvoke => synchronizeInvoke; + + public DateTime LastActivityTime => DateTime.Now; +} diff --git a/backend/FwDataMiniLcmBridge/LcmUtils/ProjectLoader.cs b/backend/FwDataMiniLcmBridge/LcmUtils/ProjectLoader.cs new file mode 100644 index 000000000..9ab99f34b --- /dev/null +++ b/backend/FwDataMiniLcmBridge/LcmUtils/ProjectLoader.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +using SIL.LCModel; +using SIL.WritingSystems; + +namespace FwDataMiniLcmBridge.LcmUtils; + +public class ProjectLoader +{ + public const string ProjectFolder = @"C:\ProgramData\SIL\FieldWorks\Projects"; + private static string TemplatesFolder { get; } = @"C:\ProgramData\SIL\FieldWorks\Templates"; + private static bool _init; + + public static void Init() + { + if (_init) + { + return; + } + + Icu.Wrapper.Init(); + Debug.Assert(Icu.Wrapper.IcuVersion == "72.1.0.3"); + Sldr.Initialize(); + _init = true; + } + + + /// + /// loads a fwdata file that lives in the project folder C:\ProgramData\SIL\FieldWorks\Projects + /// + /// could be the full path or just the file name, the path will be ignored, must include the extension + /// + public static LcmCache LoadCache(string fileName) + { + Init(); + fileName = Path.GetFileName(fileName); + var projectFilePath = Path.Combine(ProjectFolder, Path.GetFileNameWithoutExtension(fileName), fileName); + var lcmDirectories = new LcmDirectories(ProjectFolder, TemplatesFolder); + var progress = new LcmThreadedProgress(); + var cache = LcmCache.CreateCacheFromLocalProjectFile(projectFilePath, + null, + new LfLcmUi(progress.SynchronizeInvoke), + lcmDirectories, + new LcmSettings(), + progress + ); + return cache; + } +} diff --git a/backend/LcmCrdt.Tests/LexboxApiTests.cs b/backend/LcmCrdt.Tests/LexboxApiTests.cs index 2a9943583..8f1f862f9 100644 --- a/backend/LcmCrdt.Tests/LexboxApiTests.cs +++ b/backend/LcmCrdt.Tests/LexboxApiTests.cs @@ -17,10 +17,9 @@ public class BasicApiTests : IAsyncLifetime private Guid _entry1Id = new Guid("a3f5aa5a-578f-4181-8f38-eaaf27f01f1c"); private Guid _entry2Id = new Guid("2de6c334-58fa-4844-b0fd-0bc2ce4ef835"); - protected readonly IServiceScope _services; + protected readonly AsyncServiceScope _services; public DataModel DataModel = null!; - private CrdtDbContext _crdtDbContext; - private ProjectsService _projectsService; + private readonly CrdtDbContext _crdtDbContext; public BasicApiTests() { @@ -29,15 +28,16 @@ public BasicApiTests() .RemoveAll(typeof(ProjectContext)) .AddSingleton(new MockProjectContext(new CrdtProject("sena-3", ":memory:"))) .BuildServiceProvider(); - _projectsService = services.GetRequiredService(); - _services = services.CreateScope(); + _services = services.CreateAsyncScope(); _crdtDbContext = _services.ServiceProvider.GetRequiredService(); } public virtual async Task InitializeAsync() { await _crdtDbContext.Database.OpenConnectionAsync(); - await _projectsService.CreateProject("Sena 3", sqliteFile: ":memory:", db: _crdtDbContext); + //can't use ProjectsService.CreateProject because it opens and closes the db context, this would wipe out the in memory db. + await ProjectsService.InitProjectDb(_crdtDbContext, new ProjectData("Sena 3", Guid.NewGuid(), null, Guid.NewGuid())); + await _services.ServiceProvider.GetRequiredService().PopulateProjectDataCache(); DataModel = _services.ServiceProvider.GetRequiredService(); _api = ActivatorUtilities.CreateInstance(_services.ServiceProvider); await _api.CreateWritingSystem(WritingSystemType.Analysis, @@ -49,6 +49,15 @@ await _api.CreateWritingSystem(WritingSystemType.Analysis, Font = "Arial", Exemplars = [] }); + await _api.CreateWritingSystem(WritingSystemType.Vernacular, + new WritingSystem() + { + Id = "en", + Name = "English", + Abbreviation = "En", + Font = "Arial", + Exemplars = [] + }); await _api.CreateEntry(new Entry { Id = _entry1Id, @@ -124,10 +133,9 @@ await _api.CreateEntry(new() }); } - public Task DisposeAsync() + public async Task DisposeAsync() { - _services.Dispose(); - return Task.CompletedTask; + await _services.DisposeAsync(); } [Fact] diff --git a/backend/LcmCrdt/CrdtLexboxApi.cs b/backend/LcmCrdt/CrdtLexboxApi.cs index f491b4cf2..18b5d846a 100644 --- a/backend/LcmCrdt/CrdtLexboxApi.cs +++ b/backend/LcmCrdt/CrdtLexboxApi.cs @@ -12,16 +12,13 @@ namespace LcmCrdt; public class CrdtLexboxApi(DataModel dataModel, JsonSerializerOptions jsonOptions, IHybridDateTimeProvider timeProvider, CurrentProjectService projectService) : ILexboxApi { - private async ValueTask GetClientId() - { - return (await projectService.GetProjectData()).ClientId; - } + private Guid ClientId { get; } = projectService.ProjectData.ClientId; - private IQueryable Entries => dataModel.GetLatestObjects().ToLinqToDB(); - private IQueryable Senses => dataModel.GetLatestObjects().ToLinqToDB(); - private IQueryable ExampleSentences => dataModel.GetLatestObjects().ToLinqToDB(); - private IQueryable WritingSystems => dataModel.GetLatestObjects().ToLinqToDB(); + private IQueryable Entries => dataModel.GetLatestObjects(); + private IQueryable Senses => dataModel.GetLatestObjects(); + private IQueryable ExampleSentences => dataModel.GetLatestObjects(); + private IQueryable WritingSystems => dataModel.GetLatestObjects(); public async Task GetWritingSystems() { @@ -39,7 +36,7 @@ public async Task GetWritingSystems() { var entityId = Guid.NewGuid(); var wsCount = await WritingSystems.CountAsync(ws => ws.Type == type); - await dataModel.AddChange(await GetClientId(), new CreateWritingSystemChange(writingSystem, type, entityId, wsCount)); + await dataModel.AddChange(ClientId, new CreateWritingSystemChange(writingSystem, type, entityId, wsCount)); return await dataModel.GetLatest(entityId) ?? throw new NullReferenceException(); } @@ -48,7 +45,7 @@ public async Task GetWritingSystems() var ws = await GetWritingSystem(id, type); if (ws is null) throw new NullReferenceException($"unable to find writing system with id {id}"); var patchChange = new JsonPatchChange(ws.Id, update.Patch, jsonOptions); - await dataModel.AddChange(await GetClientId(), patchChange); + await dataModel.AddChange(ClientId, patchChange); return await dataModel.GetLatest(ws.Id) ?? throw new NullReferenceException(); } @@ -100,13 +97,13 @@ public async Task GetWritingSystems() if (predicate is not null) queryable = queryable.Where(predicate); if (options.Exemplar is not null) { - var ws = (await GetWritingSystem(options.Exemplar.WritingSystem, WritingSystemType.Analysis))?.WsId; + var ws = (await GetWritingSystem(options.Exemplar.WritingSystem, WritingSystemType.Vernacular))?.WsId; if (ws is null) throw new NullReferenceException($"writing system {options.Exemplar.WritingSystem} not found"); queryable = queryable.Where(e => e.Headword(ws.Value).StartsWith(options.Exemplar.Value)); } - var sortWs = (await GetWritingSystem(options.Order.WritingSystem, WritingSystemType.Analysis))?.WsId; + var sortWs = (await GetWritingSystem(options.Order.WritingSystem, WritingSystemType.Vernacular))?.WsId; if (sortWs is null) throw new NullReferenceException($"sort writing system {options.Order.WritingSystem} not found"); queryable = queryable.OrderBy(e => e.Headword(sortWs.Value)) @@ -123,13 +120,13 @@ private async Task LoadSenses(Entry[] entries) { var allSenses = (await Senses .Where(s => entries.Select(e => e.Id).Contains(s.EntryId)) - .ToArrayAsync()) + .ToArrayAsyncEF()) .ToLookup(s => s.EntryId) .ToDictionary(g => g.Key, g => g.ToArray()); var allSenseIds = allSenses.Values.SelectMany(s => s, (_, sense) => sense.Id); var allExampleSentences = (await ExampleSentences .Where(e => allSenseIds.Contains(e.SenseId)) - .ToArrayAsync()) + .ToArrayAsyncEF()) .ToLookup(s => s.SenseId) .ToDictionary(g => g.Key, g => g.ToArray()); foreach (var entry in entries) @@ -146,12 +143,12 @@ private async Task LoadSenses(Entry[] entries) public async Task GetEntry(Guid id) { - var entry = await dataModel.GetLatest(id); + var entry = await Entries.SingleOrDefaultAsync(e => e.Id == id); if (entry is null) return null; var senses = await Senses .Where(s => s.EntryId == id).ToArrayAsyncLinqToDB(); var exampleSentences = (await ExampleSentences - .Where(e => senses.Select(s => s.Id).Contains(e.SenseId)).ToArrayAsyncLinqToDB()) + .Where(e => senses.Select(s => s.Id).Contains(e.SenseId)).ToArrayAsyncEF()) .ToLookup(e => e.SenseId) .ToDictionary(g => g.Key, g => g.ToArray()); entry.Senses = senses; @@ -163,9 +160,24 @@ private async Task LoadSenses(Entry[] entries) return entry; } + /// + /// does not return the newly created entry, used for importing a large amount of data + /// + /// + public async Task CreateEntryLite(MiniLcm.Entry entry) + { + await dataModel.AddChanges(ClientId, + [ + new CreateEntryChange(entry), + ..entry.Senses.Select(s => new CreateSenseChange(s, entry.Id)), + ..entry.Senses.SelectMany(s => s.ExampleSentences, + (sense, sentence) => new CreateExampleSentenceChange(sentence, sense.Id)) + ], deferCommit: true); + } + public async Task CreateEntry(MiniLcm.Entry entry) { - await dataModel.AddChanges(await GetClientId(), + await dataModel.AddChanges(ClientId, [ new CreateEntryChange(entry), ..entry.Senses.Select(s => new CreateSenseChange(s, entry.Id)), @@ -179,18 +191,18 @@ await dataModel.AddChanges(await GetClientId(), UpdateObjectInput update) { var patchChange = new JsonPatchChange(id, update.Patch, jsonOptions); - await dataModel.AddChange(await GetClientId(), patchChange); + await dataModel.AddChange(ClientId, patchChange); return await GetEntry(id) ?? throw new NullReferenceException(); } public async Task DeleteEntry(Guid id) { - await dataModel.AddChange(await GetClientId(), new DeleteChange(id)); + await dataModel.AddChange(ClientId, new DeleteChange(id)); } public async Task CreateSense(Guid entryId, MiniLcm.Sense sense) { - await dataModel.AddChanges(await GetClientId(), + await dataModel.AddChanges(ClientId, [ new CreateSenseChange(sense, entryId), ..sense.ExampleSentences.Select(sentence => @@ -204,20 +216,20 @@ await dataModel.AddChanges(await GetClientId(), UpdateObjectInput update) { var patchChange = new JsonPatchChange(senseId, update.Patch, jsonOptions); - await dataModel.AddChange(await GetClientId(), patchChange); + await dataModel.AddChange(ClientId, patchChange); return await dataModel.GetLatest(senseId) ?? throw new NullReferenceException(); } public async Task DeleteSense(Guid entryId, Guid senseId) { - await dataModel.AddChange(await GetClientId(), new DeleteChange(senseId)); + await dataModel.AddChange(ClientId, new DeleteChange(senseId)); } public async Task CreateExampleSentence(Guid entryId, Guid senseId, MiniLcm.ExampleSentence exampleSentence) { - await dataModel.AddChange(await GetClientId(), new CreateExampleSentenceChange(exampleSentence, senseId)); + await dataModel.AddChange(ClientId, new CreateExampleSentenceChange(exampleSentence, senseId)); return await dataModel.GetLatest(exampleSentence.Id) ?? throw new NullReferenceException(); } @@ -228,13 +240,13 @@ public async Task DeleteSense(Guid entryId, Guid senseId) { var jsonPatch = update.Patch; var patchChange = new JsonPatchChange(exampleSentenceId, jsonPatch, jsonOptions); - await dataModel.AddChange(await GetClientId(), patchChange); + await dataModel.AddChange(ClientId, patchChange); return await dataModel.GetLatest(exampleSentenceId) ?? throw new NullReferenceException(); } public async Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId) { - await dataModel.AddChange(await GetClientId(), new DeleteChange(exampleSentenceId)); + await dataModel.AddChange(ClientId, new DeleteChange(exampleSentenceId)); } public UpdateBuilder CreateUpdateBuilder() where T : class diff --git a/backend/LcmCrdt/CrdtProject.cs b/backend/LcmCrdt/CrdtProject.cs index 9c4795d36..2c5020e9e 100644 --- a/backend/LcmCrdt/CrdtProject.cs +++ b/backend/LcmCrdt/CrdtProject.cs @@ -1,4 +1,5 @@ -using MiniLcm; +using System.Diagnostics.CodeAnalysis; +using MiniLcm; namespace LcmCrdt; @@ -9,4 +10,10 @@ public class CrdtProject(string name, string dbPath) : IProjectIdentifier public string DbPath { get; } = dbPath; } -public record ProjectData(string Name, Guid Id, string? OriginDomain, Guid ClientId); +public record ProjectData(string Name, Guid Id, string? OriginDomain, Guid ClientId) +{ + public static string? GetOriginDomain(Uri? uri) + { + return uri?.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped); + } +} diff --git a/backend/LcmCrdt/CurrentProjectService.cs b/backend/LcmCrdt/CurrentProjectService.cs index f3e75ec1f..1828a1601 100644 --- a/backend/LcmCrdt/CurrentProjectService.cs +++ b/backend/LcmCrdt/CurrentProjectService.cs @@ -9,9 +9,12 @@ public class CurrentProjectService(CrdtDbContext dbContext, ProjectContext proje public CrdtProject Project => projectContext.Project ?? throw new NullReferenceException("Not in the context of a project"); + //only works because PopulateProjectDataCache is called first in the request pipeline + public ProjectData ProjectData => memoryCache.Get(CacheKey(Project)) ?? throw new InvalidOperationException("Project data not found"); + public async ValueTask GetProjectData() { - var key = Project.Name + "|ProjectData"; + var key = CacheKey(Project); if (!memoryCache.TryGetValue(key, out object? result)) { using var entry = memoryCache.CreateEntry(key); @@ -23,4 +26,39 @@ public async ValueTask GetProjectData() return (ProjectData)result; } + + private static string CacheKey(CrdtProject project) + { + return project.Name + "|ProjectData"; + } + + public async ValueTask PopulateProjectDataCache() + { + var projectData = await GetProjectData(); + return projectData; + } + + private void RemoveProjectDataCache() + { + memoryCache.Remove(CacheKey(Project)); + } + + public async Task SetProjectSyncOrigin(Uri domain, Guid? id) + { + var originDomain = ProjectData.GetOriginDomain(domain); + if (id is null) + { + await dbContext.Set() + .ExecuteUpdateAsync(calls => calls.SetProperty(p => p.OriginDomain, originDomain)); + } + else + { + await dbContext.Set() + .ExecuteUpdateAsync(calls => calls.SetProperty(p => p.OriginDomain, originDomain) + .SetProperty(p => p.Id, id)); + } + + RemoveProjectDataCache(); + await PopulateProjectDataCache(); + } } diff --git a/backend/LcmCrdt/LcmCrdt.csproj b/backend/LcmCrdt/LcmCrdt.csproj index 7671ff2f9..da7f89f2a 100644 --- a/backend/LcmCrdt/LcmCrdt.csproj +++ b/backend/LcmCrdt/LcmCrdt.csproj @@ -6,6 +6,10 @@ enable + + + + diff --git a/backend/LcmCrdt/ProjectContext.cs b/backend/LcmCrdt/ProjectContext.cs index f5e1ed7d7..30bfb3c58 100644 --- a/backend/LcmCrdt/ProjectContext.cs +++ b/backend/LcmCrdt/ProjectContext.cs @@ -14,6 +14,8 @@ public virtual CrdtProject? Project get => _projectHolder.Value?.Project; set { + //don't change existing if they are the same, that will break nested scopes + if (Project == value) return; var holder = _projectHolder.Value; if (holder != null) { diff --git a/backend/LcmCrdt/ProjectsService.cs b/backend/LcmCrdt/ProjectsService.cs index 50861798b..bf567f84b 100644 --- a/backend/LcmCrdt/ProjectsService.cs +++ b/backend/LcmCrdt/ProjectsService.cs @@ -1,5 +1,6 @@ using Crdt.Db; using Microsoft.Extensions.DependencyInjection; +using MiniLcm; namespace LcmCrdt; @@ -28,26 +29,30 @@ public bool ProjectExists(string name) public async Task CreateProject(string name, Guid? id = null, - string? domain = null, - Func? afterCreate = null, - string? sqliteFile = null, - CrdtDbContext? db = null) + Uri? domain = null, + Func? afterCreate = null) { - sqliteFile ??= $"{name}.sqlite"; + var sqliteFile = $"{name}.sqlite"; if (File.Exists(sqliteFile)) throw new InvalidOperationException("Project already exists"); var crdtProject = new CrdtProject(name, sqliteFile); - using var serviceScope = CreateProjectScope(crdtProject); - db ??= serviceScope.ServiceProvider.GetRequiredService(); - await db.Database.EnsureCreatedAsync(); - db.Set().Add(new ProjectData(name, id ?? Guid.NewGuid(), domain, Guid.NewGuid())); - await db.SaveChangesAsync(); + await using var serviceScope = CreateProjectScope(crdtProject); + var db = serviceScope.ServiceProvider.GetRequiredService(); + await InitProjectDb(db, new ProjectData(name, id ?? Guid.NewGuid(), ProjectData.GetOriginDomain(domain), Guid.NewGuid())); + await serviceScope.ServiceProvider.GetRequiredService().PopulateProjectDataCache(); await (afterCreate?.Invoke(serviceScope.ServiceProvider, crdtProject) ?? Task.CompletedTask); return crdtProject; } - public IServiceScope CreateProjectScope(CrdtProject crdtProject) + internal static async Task InitProjectDb(CrdtDbContext db, ProjectData data) + { + await db.Database.EnsureCreatedAsync(); + db.Set().Add(data); + await db.SaveChangesAsync(); + } + + public AsyncServiceScope CreateProjectScope(CrdtProject crdtProject) { - var serviceScope = provider.CreateScope(); + var serviceScope = provider.CreateAsyncScope(); SetProjectScope(crdtProject); return serviceScope; } diff --git a/backend/LexBoxApi/Auth/AuthKernel.cs b/backend/LexBoxApi/Auth/AuthKernel.cs index 69065a040..4cdd1193d 100644 --- a/backend/LexBoxApi/Auth/AuthKernel.cs +++ b/backend/LexBoxApi/Auth/AuthKernel.cs @@ -1,6 +1,6 @@ -using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; +using System.Security.Cryptography.X509Certificates; using System.Text; using LexBoxApi.Auth.Attributes; using LexBoxApi.Auth.Requirements; @@ -242,13 +242,13 @@ private static void AddOpenId(IServiceCollection services, IWebHostEnvironment e options.SetUserinfoEndpointUris("api/oauth/userinfo"); options.Configure(serverOptions => serverOptions.Handlers.Add(ScopeRequestFixer.Descriptor)); + options.SetAccessTokenLifetime(TimeSpan.FromHours(1)); + options.SetRefreshTokenLifetime(TimeSpan.FromDays(14)); options.AllowAuthorizationCodeFlow() .AllowRefreshTokenFlow(); options.RequireProofKeyForCodeExchange();//best practice to use PKCE with auth code flow and no implicit flow - options.IgnoreResponseTypePermissions(); - options.IgnoreScopePermissions(); if (environment.IsDevelopment()) { options.AddDevelopmentEncryptionCertificate(); @@ -257,7 +257,19 @@ private static void AddOpenId(IServiceCollection services, IWebHostEnvironment e else { //see docs: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html - throw new NotImplementedException("need to implement loading keys from a file"); + //todo, handle certificate rotation, right now these certs will be replaced 15 days before they expire + //however we need to start signing with the new cert before the old one expires + //this means we need 2 certs for each use case, an old one for tokens that have not expired yet, and a new one to sign all new tokens + var encryptionCert = X509Certificate2.CreateFromPemFile( + "/oauth-certs/encryption/tls.crt", + "/oauth-certs/encryption/tls.key"); + options.AddEncryptionCertificate(encryptionCert); + + var signingCert = X509Certificate2.CreateFromPemFile( + "/oauth-certs/signing/tls.crt", + "/oauth-certs/signing/tls.key"); + + options.AddSigningCertificate(signingCert); } var aspnetCoreBuilder = options.UseAspNetCore() diff --git a/backend/LexBoxApi/Auth/LexAuthService.cs b/backend/LexBoxApi/Auth/LexAuthService.cs index 5782455e9..84b830f78 100644 --- a/backend/LexBoxApi/Auth/LexAuthService.cs +++ b/backend/LexBoxApi/Auth/LexAuthService.cs @@ -60,7 +60,11 @@ private static SymmetricSecurityKey GetSigningKey(JwtOptions jwtOptions) return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Secret)); } - + public async Task CanUserLogin(Guid id) + { + var user = await _lexBoxDbContext.Users.FindAsync(id); + return user?.CanLogin() ?? false; + } public async Task<(LexAuthUser?, LoginError?)> Login(LoginRequest loginRequest) { diff --git a/backend/LexBoxApi/Controllers/AdminController.cs b/backend/LexBoxApi/Controllers/AdminController.cs index ac6b1f0d4..a4e121c4f 100644 --- a/backend/LexBoxApi/Controllers/AdminController.cs +++ b/backend/LexBoxApi/Controllers/AdminController.cs @@ -2,6 +2,7 @@ using LexBoxApi.Auth; using LexBoxApi.Auth.Attributes; using LexBoxApi.Services; +using LexBoxApi.Services.Email; using LexCore; using LexData; using Microsoft.AspNetCore.Mvc; @@ -16,12 +17,12 @@ public class AdminController : ControllerBase private readonly LexBoxDbContext _lexBoxDbContext; private readonly LoggedInContext _loggedInContext; private readonly UserService _userService; - private readonly EmailService _emailService; + private readonly IEmailService _emailService; public AdminController(LexBoxDbContext lexBoxDbContext, LoggedInContext loggedInContext, UserService userService, - EmailService emailService + IEmailService emailService ) { _lexBoxDbContext = lexBoxDbContext; diff --git a/backend/LexBoxApi/Controllers/CrdtController.cs b/backend/LexBoxApi/Controllers/CrdtController.cs new file mode 100644 index 000000000..5fb268925 --- /dev/null +++ b/backend/LexBoxApi/Controllers/CrdtController.cs @@ -0,0 +1,63 @@ +using Crdt.Core; +using LexBoxApi.Auth.Attributes; +using LexData; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LexBoxApi.Controllers; + +[ApiController] +[Route("/api/crdt")] +[AdminRequired] +public class CrdtController(LexBoxDbContext dbContext): ControllerBase +{ + [HttpGet("{projectId}/get")] + public async Task> GetSyncState(Guid projectId) + { + return await dbContext.Set().Where(c => c.ProjectId == projectId).GetSyncState(); + } + + [HttpPost("{projectId}/add")] + public async Task Add(Guid projectId, [FromBody] ServerCommit[] commits) + { + foreach (var commit in commits) + { + commit.ProjectId = projectId; + dbContext.Add(commit); //todo should only add if not exists, based on commit id + } + + await dbContext.SaveChangesAsync(); + return Ok(); + } + + [HttpPost("{projectId}/changes")] + public async Task>> Changes(Guid projectId, [FromBody] SyncState clientHeads) + { + var commits = dbContext.Set().Where(c => c.ProjectId == projectId); + return await commits.GetChanges(clientHeads); + } + + public record LexboxCrdtProject(Guid Id, string Name); + [HttpGet("listProjects")] + public async Task> ListProjects() + { + return await dbContext.Projects + .Where(p => dbContext.Set().Any(c => c.ProjectId == p.Id)) + .Select(p => new LexboxCrdtProject(p.Id, p.Code)) + .ToArrayAsync(); + } + + [HttpGet("lookupProjectId")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesDefaultResponseType] + public async Task> GetProjectId(string code) + { + var project = await dbContext.Projects.FirstOrDefaultAsync(p => p.Code == code); + if (project == null) + { + return NotFound(); + } + return Ok(project.Id); + } +} diff --git a/backend/LexBoxApi/Controllers/LoginController.cs b/backend/LexBoxApi/Controllers/LoginController.cs index 34466ab10..b60cb0ca3 100644 --- a/backend/LexBoxApi/Controllers/LoginController.cs +++ b/backend/LexBoxApi/Controllers/LoginController.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using System.Security.Claims; using Microsoft.AspNetCore.Authentication.Google; +using LexBoxApi.Services.Email; namespace LexBoxApi.Controllers; @@ -22,7 +23,7 @@ public class LoginController( LexAuthService lexAuthService, LexBoxDbContext lexBoxDbContext, LoggedInContext loggedInContext, - EmailService emailService, + IEmailService emailService, UserService userService, TurnstileService turnstileService) : ControllerBase diff --git a/backend/LexBoxApi/Controllers/OauthController.cs b/backend/LexBoxApi/Controllers/OauthController.cs index b7024a295..ceb8861b2 100644 --- a/backend/LexBoxApi/Controllers/OauthController.cs +++ b/backend/LexBoxApi/Controllers/OauthController.cs @@ -1,7 +1,9 @@ -using System.Security.Claims; +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; using System.Text.Json; using LexBoxApi.Auth; using LexCore.Auth; +using LexData; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; @@ -17,11 +19,19 @@ namespace LexBoxApi.Controllers; [ApiController] [Route("/api/oauth")] public class OauthController( - LoggedInContext loggedInContext, - IOpenIddictApplicationManager applicationManager, - IOpenIddictAuthorizationManager authorizationManager + LexAuthService lexAuthService, + IOpenIddictApplicationManager? applicationManager = null, + IOpenIddictAuthorizationManager? authorizationManager = null ) : ControllerBase { + private IOpenIddictApplicationManager? applicationManager = applicationManager; + private IOpenIddictAuthorizationManager? authorizationManager = authorizationManager; + + [MemberNotNull(nameof(applicationManager), nameof(authorizationManager))] + private void AssertOAuthSetup() { + if (applicationManager is null) throw new InvalidOperationException("applicationManager is null"); + if (authorizationManager is null) throw new InvalidOperationException("authorizationManager is null"); + } [HttpGet("open-id-auth")] [HttpPost("open-id-auth")] @@ -29,6 +39,7 @@ IOpenIddictAuthorizationManager authorizationManager [ProducesDefaultResponseType] public async Task Authorize() { + AssertOAuthSetup(); var request = HttpContext.GetOpenIddictServerRequest(); if (request is null) { @@ -37,15 +48,14 @@ public async Task Authorize() if (IsAcceptRequest()) { - return await FinishSignIn(loggedInContext.User, request); + return await FinishSignIn(User, request); } // Retrieve the user principal stored in the authentication cookie. // If the user principal can't be extracted or the cookie is too old, redirect the user to the login page. var result = await HttpContext.AuthenticateAsync(); - var lexAuthUser = result.Succeeded ? LexAuthUser.FromClaimsPrincipal(result.Principal) : null; if (!result.Succeeded || - lexAuthUser is null || + result.Principal.Identity?.IsAuthenticated is not true || request.HasPrompt(OpenIddictConstants.Prompts.Login) || IsExpired(request, result)) { @@ -82,7 +92,9 @@ lexAuthUser is null || }); } - var userId = lexAuthUser.Id.ToString(); + var user = result.Principal; + ArgumentNullException.ThrowIfNull(user); + var requestClientId = request.ClientId; ArgumentException.ThrowIfNullOrEmpty(requestClientId); var application = await applicationManager.FindByClientIdAsync(requestClientId) ?? @@ -93,7 +105,7 @@ lexAuthUser is null || // Retrieve the permanent authorizations associated with the user and the calling client application. var authorizations = await authorizationManager.FindAsync( - subject: userId, + subject: GetUserId(user).ToString(), client: applicationId, status: OpenIddictConstants.Statuses.Valid, type: OpenIddictConstants.AuthorizationTypes.Permanent, @@ -107,7 +119,7 @@ lexAuthUser is null || case OpenIddictConstants.ConsentTypes.External when authorizations.Count is not 0: case OpenIddictConstants.ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(OpenIddictConstants.Prompts.Consent): - return await FinishSignIn(lexAuthUser, request, applicationId, authorizations); + return await FinishSignIn(user, request, applicationId, authorizations); // If the consent is external (e.g when authorizations are granted by a sysadmin), // immediately return an error if no authorization can be found in the database. @@ -165,12 +177,12 @@ private bool IsAcceptRequest() [AllowAnonymous] public async Task Exchange() { + AssertOAuthSetup(); var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); // Retrieve the claims principal stored in the authorization code/refresh token. var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - var lexAuthUser = result.Succeeded ? LexAuthUser.FromClaimsPrincipal(result.Principal) : null; - if (!result.Succeeded || lexAuthUser is null) + if (!result.Succeeded || result.Principal.Identity?.IsAuthenticated is not true) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, @@ -182,11 +194,12 @@ public async Task Exchange() })); } - return await FinishSignIn(lexAuthUser, request); + return await FinishSignIn(result.Principal, request); } - private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddictRequest request) + private async Task FinishSignIn(ClaimsPrincipal user, OpenIddictRequest request) { + AssertOAuthSetup(); var requestClientId = request.ClientId; ArgumentException.ThrowIfNullOrEmpty(requestClientId); var application = await applicationManager.FindByClientIdAsync(requestClientId) ?? @@ -195,7 +208,7 @@ private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddic // Retrieve the permanent authorizations associated with the user and the calling client application. var applicationId = await applicationManager.GetIdAsync(application) ?? throw new InvalidOperationException("The calling client application could not be found."); var authorizations = await authorizationManager.FindAsync( - subject: lexAuthUser.Id.ToString(), + subject: GetUserId(user).ToString(), client: applicationId, status: OpenIddictConstants.Statuses.Valid, type: OpenIddictConstants.AuthorizationTypes.Permanent, @@ -204,7 +217,9 @@ private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddic //allow cors response for redirect hosts var redirectUrisAsync = await applicationManager.GetRedirectUrisAsync(application); Response.Headers.AccessControlAllowOrigin = redirectUrisAsync - .Select(uri => new Uri(uri).GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)).ToArray(); + .Select(uri => new Uri(uri)) + .Where(uri => request.RedirectUri is not null && uri.Host == new Uri(request.RedirectUri).Host) + .Select(uri => uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)).ToArray(); // Note: this check is here to ensure a malicious user can't abuse this POST-only endpoint and // force it to return a valid response without the external authorization. @@ -221,27 +236,37 @@ await applicationManager.HasConsentTypeAsync(application, OpenIddictConstants.Co })); } - return await FinishSignIn(lexAuthUser, request, applicationId, authorizations); + if (!await lexAuthService.CanUserLogin(GetUserId(user))) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "User account is locked." + })); + } + + return await FinishSignIn(user, request, applicationId, authorizations); } - private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddictRequest request, string applicationId, List authorizations) + private async Task FinishSignIn(ClaimsPrincipal user, OpenIddictRequest request, string applicationId, List authorizations) { - var userId = lexAuthUser.Id.ToString(); + AssertOAuthSetup(); + var userId = GetUserId(user); // Create the claims-based identity that will be used by OpenIddict to generate tokens. var identity = new ClaimsIdentity( authenticationType: TokenValidationParameters.DefaultAuthenticationType, + claims: user.Claims, nameType: OpenIddictConstants.Claims.Name, roleType: OpenIddictConstants.Claims.Role); - // Add the claims that will be persisted in the tokens. - identity.SetClaim(OpenIddictConstants.Claims.Subject, userId) - .SetClaim(OpenIddictConstants.Claims.Email, lexAuthUser.Email) - .SetClaim(OpenIddictConstants.Claims.Name, lexAuthUser.Name) - .SetClaim(OpenIddictConstants.Claims.Role, lexAuthUser.Role.ToString()); - // Note: in this sample, the granted scopes match the requested scope // but you may want to allow the user to uncheck specific scopes. // For that, simply restrict the list of scopes before calling SetScopes. - identity.SetScopes(request.GetScopes()); + // request scope may be null when exchanging tokens, so we want to keep the existing scopes + if (!string.IsNullOrEmpty(request.Scope)) + identity.SetScopes(request.GetScopes()); identity.SetAudiences(LexboxAudience.LexboxApi.ToString()); // identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync()); @@ -250,7 +275,7 @@ private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddic var authorization = authorizations.LastOrDefault(); authorization ??= await authorizationManager.CreateAsync( identity: identity, - subject : userId, + subject : userId.ToString(), client : applicationId, type : OpenIddictConstants.AuthorizationTypes.Permanent, scopes : identity.GetScopes()); @@ -262,6 +287,16 @@ private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddic return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } + private static Guid GetUserId(ClaimsPrincipal? principal) + { + var id = principal?.FindFirst(LexAuthConstants.IdClaimType)?.Value; + if (id is null or []) + { + throw new InvalidOperationException("The user identifier cannot be found."); + } + return Guid.Parse(id); + } + private static IEnumerable GetDestinations(Claim claim) { // Note: by default, claims are NOT automatically included in the access and identity tokens. @@ -275,6 +310,13 @@ private static IEnumerable GetDestinations(Claim claim) case OpenIddictConstants.Claims.Name: yield return OpenIddictConstants.Destinations.AccessToken; + if (claimsIdentity.HasScope(OpenIddictConstants.Scopes.Profile)) + yield return OpenIddictConstants.Destinations.IdentityToken; + + yield break; + case OpenIddictConstants.Claims.Username: + yield return OpenIddictConstants.Destinations.AccessToken; + if (claimsIdentity.HasScope(OpenIddictConstants.Scopes.Profile)) yield return OpenIddictConstants.Destinations.IdentityToken; diff --git a/backend/LexBoxApi/Controllers/TestingController.cs b/backend/LexBoxApi/Controllers/TestingController.cs index 9d6db7eda..bccf075be 100644 --- a/backend/LexBoxApi/Controllers/TestingController.cs +++ b/backend/LexBoxApi/Controllers/TestingController.cs @@ -36,6 +36,12 @@ public async Task> MakeJwt(string usernameOrEmail, return token; } + [HttpGet("claims")] + public Dictionary Claims() + { + return User.Claims.ToLookup(c => c.Type, c => c.Value).ToDictionary(k => k.Key, v => string.Join(";", v)); + } + [HttpPost("cleanupSeedData")] public async Task CleanupSeedData() { diff --git a/backend/LexBoxApi/Controllers/UserController.cs b/backend/LexBoxApi/Controllers/UserController.cs index 92b6c7020..14075b167 100644 --- a/backend/LexBoxApi/Controllers/UserController.cs +++ b/backend/LexBoxApi/Controllers/UserController.cs @@ -4,6 +4,7 @@ using LexBoxApi.Models; using LexBoxApi.Otel; using LexBoxApi.Services; +using LexBoxApi.Services.Email; using LexCore; using LexCore.Auth; using LexCore.Entities; @@ -24,14 +25,14 @@ public class UserController : ControllerBase private readonly LexBoxDbContext _lexBoxDbContext; private readonly TurnstileService _turnstileService; private readonly LoggedInContext _loggedInContext; - private readonly EmailService _emailService; + private readonly IEmailService _emailService; private readonly LexAuthService _lexAuthService; public UserController( LexBoxDbContext lexBoxDbContext, TurnstileService turnstileService, LoggedInContext loggedInContext, - EmailService emailService, + IEmailService emailService, LexAuthService lexAuthService ) { diff --git a/backend/LexBoxApi/GraphQL/CustomTypes/OrgGqlConfiguration.cs b/backend/LexBoxApi/GraphQL/CustomTypes/OrgGqlConfiguration.cs new file mode 100644 index 000000000..1d77fe834 --- /dev/null +++ b/backend/LexBoxApi/GraphQL/CustomTypes/OrgGqlConfiguration.cs @@ -0,0 +1,12 @@ +using LexCore.Entities; + +namespace LexBoxApi.GraphQL.CustomTypes; + +[ObjectType] +public class OrgGqlConfiguration : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(o => o.CreatedDate).IsProjected(); + } +} diff --git a/backend/LexBoxApi/GraphQL/CustomTypes/OrgMembersGqlConfiguration.cs b/backend/LexBoxApi/GraphQL/CustomTypes/OrgMembersGqlConfiguration.cs new file mode 100644 index 000000000..65b81bede --- /dev/null +++ b/backend/LexBoxApi/GraphQL/CustomTypes/OrgMembersGqlConfiguration.cs @@ -0,0 +1,13 @@ +using LexCore.Entities; + +namespace LexBoxApi.GraphQL.CustomTypes; + +[ObjectType] +public class OrgMembersGqlConfiguration : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(f => f.User).Type>(); + descriptor.Field(f => f.Organization).Type>(); + } +} diff --git a/backend/LexBoxApi/GraphQL/CustomTypes/OrgProjectsGqlConfiguration.cs b/backend/LexBoxApi/GraphQL/CustomTypes/OrgProjectsGqlConfiguration.cs new file mode 100644 index 000000000..cbe188a96 --- /dev/null +++ b/backend/LexBoxApi/GraphQL/CustomTypes/OrgProjectsGqlConfiguration.cs @@ -0,0 +1,13 @@ +using LexCore.Entities; + +namespace LexBoxApi.GraphQL.CustomTypes; + +[ObjectType] +public class OrgProjectsGqlConfiguration : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(op => op.Org).Type>(); + descriptor.Field(op => op.Project).Type>(); + } +} diff --git a/backend/LexBoxApi/GraphQL/GraphQlSetupKernel.cs b/backend/LexBoxApi/GraphQL/GraphQlSetupKernel.cs index 252da0584..1f55fc6ea 100644 --- a/backend/LexBoxApi/GraphQL/GraphQlSetupKernel.cs +++ b/backend/LexBoxApi/GraphQL/GraphQlSetupKernel.cs @@ -3,6 +3,7 @@ using LexBoxApi.Auth; using LexBoxApi.GraphQL.CustomFilters; using LexBoxApi.Services; +using LexBoxApi.Services.Email; using LexCore.ServiceInterfaces; using LexData; @@ -22,7 +23,7 @@ public static void AddLexGraphQL(this IServiceCollection services, IHostEnvironm .RegisterService() .RegisterService() .RegisterService() - .RegisterService() + .RegisterService() .RegisterService() .RegisterService() .AddDataAnnotationsValidator() diff --git a/backend/LexBoxApi/GraphQL/LexQueries.cs b/backend/LexBoxApi/GraphQL/LexQueries.cs index 82a53e305..8607d0524 100644 --- a/backend/LexBoxApi/GraphQL/LexQueries.cs +++ b/backend/LexBoxApi/GraphQL/LexQueries.cs @@ -36,6 +36,14 @@ public IQueryable Projects(LexBoxDbContext context, bool withDeleted = } } + [UseProjection] + [UseSorting] + public IQueryable MyDraftProjects(LoggedInContext loggedInContext, LexBoxDbContext context) + { + var userId = loggedInContext.User.Id; + return context.DraftProjects.Where(p => p.ProjectManagerId == userId); + } + [UseProjection] [UseFiltering] [UseSorting] diff --git a/backend/LexBoxApi/GraphQL/OrgMutations.cs b/backend/LexBoxApi/GraphQL/OrgMutations.cs index abd50d789..6d317f653 100644 --- a/backend/LexBoxApi/GraphQL/OrgMutations.cs +++ b/backend/LexBoxApi/GraphQL/OrgMutations.cs @@ -1,4 +1,6 @@ using LexBoxApi.Auth; +using LexBoxApi.Auth.Attributes; +using LexBoxApi.Models.Org; using LexCore.Entities; using LexCore.Exceptions; using LexCore.ServiceInterfaces; @@ -30,12 +32,90 @@ public async Task> CreateOrganization(string name, Members = [ new OrgMember() { Role = OrgRole.Admin, UserId = userId } - ] + ], + Projects = [] }); await dbContext.SaveChangesAsync(); return dbContext.Orgs.Where(o => o.Id == orgId); } + [Error] + [UseMutationConvention] + [AdminRequired] + public async Task DeleteOrg(Guid orgId, + LexBoxDbContext dbContext) + { + var org = await dbContext.Orgs.Include(o => o.Members).FirstOrDefaultAsync(o => o.Id == orgId); + NotFoundException.ThrowIfNull(org); + + dbContext.Remove(org); + await dbContext.SaveChangesAsync(); + return org; + } + + [Error] + [Error] + [UseMutationConvention] + [UseFirstOrDefault] + [UseProjection] + public async Task> AddProjectToOrg( + LexBoxDbContext dbContext, + IPermissionService permissionService, + Guid orgId, + Guid projectId) + { + var org = await dbContext.Orgs.FindAsync(orgId); + NotFoundException.ThrowIfNull(org); + permissionService.AssertCanAddProjectToOrg(org); + var project = await dbContext.Projects.Where(p => p.Id == projectId) + .Include(p => p.Organizations) + .SingleOrDefaultAsync(); + NotFoundException.ThrowIfNull(project); + permissionService.AssertCanManageProject(projectId); + + if (project.Organizations.Exists(o => o.Id == orgId)) + { + // No error since we're already in desired state; just return early + return dbContext.Orgs.Where(o => o.Id == orgId); + } + project.Organizations.Add(org); + project.UpdateUpdatedDate(); + org.UpdateUpdatedDate(); + await dbContext.SaveChangesAsync(); + return dbContext.Orgs.Where(o => o.Id == orgId); + } + + [Error] + [Error] + [UseMutationConvention] + [UseFirstOrDefault] + [UseProjection] + public async Task> RemoveProjectFromOrg( + LexBoxDbContext dbContext, + IPermissionService permissionService, + Guid orgId, + Guid projectId) + { + var org = await dbContext.Orgs.FindAsync(orgId); + NotFoundException.ThrowIfNull(org); + permissionService.AssertCanAddProjectToOrg(org); + var project = await dbContext.Projects.Where(p => p.Id == projectId) + .Include(p => p.Organizations) + .SingleOrDefaultAsync(); + NotFoundException.ThrowIfNull(project); + permissionService.AssertCanManageProject(projectId); + var foundOrg = project.Organizations.FirstOrDefault(o => o.Id == orgId); + if (foundOrg is not null) + { + project.Organizations.Remove(foundOrg); + project.UpdateUpdatedDate(); + org.UpdateUpdatedDate(); + await dbContext.SaveChangesAsync(); + } + // If org did not own project, return with no error + return dbContext.Orgs.Where(o => o.Id == orgId); + } + /// /// set the role of a member in an organization, if the member does not exist it will be created /// @@ -55,14 +135,39 @@ public async Task> SetOrgMemberRole( Guid orgId, OrgRole? role, string emailOrUsername) + { + var user = await dbContext.Users.FindByEmailOrUsername(emailOrUsername); + NotFoundException.ThrowIfNull(user); // TODO: Implement inviting user + return await ChangeOrgMemberRole(dbContext, permissionService, orgId, user.Id, role); + } + + /// + /// Change the role of an existing member in an organization + /// + /// + /// + /// + /// ID (GUID) of the user whose membership should be updated + /// set to null to remove the member + [Error] + [Error] + [UseMutationConvention] + [UseFirstOrDefault] + [UseProjection] + public async Task> ChangeOrgMemberRole( + LexBoxDbContext dbContext, + IPermissionService permissionService, + Guid orgId, + Guid userId, + OrgRole? role) { var org = await dbContext.Orgs.Include(o => o.Members).FirstOrDefaultAsync(o => o.Id == orgId); NotFoundException.ThrowIfNull(org); - var user = await dbContext.Users.FindByEmailOrUsername(emailOrUsername); - NotFoundException.ThrowIfNull(user); permissionService.AssertCanEditOrg(org); - await UpdateOrgMemberRole(dbContext, org, role, user.Id); + var user = await dbContext.Users.FindAsync(userId); + NotFoundException.ThrowIfNull(user); + await UpdateOrgMemberRole(dbContext, org, role, userId); return dbContext.Orgs.Where(o => o.Id == orgId); } @@ -85,4 +190,26 @@ private async Task UpdateOrgMemberRole(LexBoxDbContext dbContext, Organization o await dbContext.SaveChangesAsync(); } + + [Error] + [Error] + [Error] + [UseMutationConvention] + [UseFirstOrDefault] + [UseProjection] + public async Task> ChangeOrgName(ChangeOrgNameInput input, + IPermissionService permissionService, + LexBoxDbContext dbContext) + { + if (string.IsNullOrEmpty(input.Name)) throw new RequiredException("Org name cannot be empty"); + + var org = await dbContext.Orgs.FindAsync(input.OrgId); + NotFoundException.ThrowIfNull(org); + permissionService.AssertCanEditOrg(org); + + org.Name = input.Name; + org.UpdateUpdatedDate(); + await dbContext.SaveChangesAsync(); + return dbContext.Orgs.Where(o => o.Id == input.OrgId); + } } diff --git a/backend/LexBoxApi/GraphQL/ProjectMutations.cs b/backend/LexBoxApi/GraphQL/ProjectMutations.cs index 9f4c87bd0..717f28fb5 100644 --- a/backend/LexBoxApi/GraphQL/ProjectMutations.cs +++ b/backend/LexBoxApi/GraphQL/ProjectMutations.cs @@ -14,6 +14,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using System.Net.Mail; +using LexBoxApi.Services.Email; namespace LexBoxApi.GraphQL; @@ -38,7 +39,7 @@ public record CreateProjectResponse(Guid? Id, CreateProjectResult Result); IPermissionService permissionService, CreateProjectInput input, [Service] ProjectService projectService, - [Service] EmailService emailService) + [Service] IEmailService emailService) { if (!loggedInContext.User.IsAdmin) { @@ -54,7 +55,6 @@ public record CreateProjectResponse(Guid? Id, CreateProjectResult Result); await emailService.SendCreateProjectRequestEmail(loggedInContext.User, input); return new CreateProjectResponse(draftProjectId, CreateProjectResult.Requested); } - var projectId = await projectService.CreateProject(input); return new CreateProjectResponse(projectId, CreateProjectResult.Created); } @@ -73,7 +73,7 @@ public async Task> AddProjectMember(IPermissionService permi LoggedInContext loggedInContext, AddProjectMemberInput input, LexBoxDbContext dbContext, - [Service] EmailService emailService) + [Service] IEmailService emailService) { permissionService.AssertCanManageProject(input.ProjectId); var project = await dbContext.Projects.FindAsync(input.ProjectId); @@ -106,6 +106,7 @@ public async Task> AddProjectMember(IPermissionService permi user.UpdateUpdatedDate(); project.UpdateUpdatedDate(); await dbContext.SaveChangesAsync(); + await emailService.SendUserAddedEmail(user, project.Name, project.Code); return dbContext.Projects.Where(p => p.Id == input.ProjectId); } diff --git a/backend/LexBoxApi/GraphQL/UserMutations.cs b/backend/LexBoxApi/GraphQL/UserMutations.cs index aff7f6108..230b9021a 100644 --- a/backend/LexBoxApi/GraphQL/UserMutations.cs +++ b/backend/LexBoxApi/GraphQL/UserMutations.cs @@ -6,6 +6,7 @@ using LexBoxApi.Models.Project; using LexBoxApi.Otel; using LexBoxApi.Services; +using LexBoxApi.Services.Email; using LexCore; using LexCore.Auth; using LexCore.Entities; @@ -45,7 +46,7 @@ public async Task ChangeUserAccountBySelf( IPermissionService permissionService, ChangeUserAccountBySelfInput input, LexBoxDbContext dbContext, - EmailService emailService + IEmailService emailService ) { if (loggedInContext.User.Id != input.UserId) throw new UnauthorizedAccessException(); @@ -68,7 +69,7 @@ public Task ChangeUserAccountByAdmin( IPermissionService permissionService, ChangeUserAccountByAdminInput input, LexBoxDbContext dbContext, - EmailService emailService + IEmailService emailService ) { return UpdateUser(loggedInContext, permissionService, input, dbContext, emailService); @@ -83,7 +84,7 @@ public async Task CreateGuestUserByAdmin( LoggedInContext loggedInContext, CreateGuestUserByAdminInput input, LexBoxDbContext dbContext, - EmailService emailService + IEmailService emailService ) { using var createGuestUserActivity = LexBoxActivitySource.Get().StartActivity("CreateGuestUser"); @@ -128,7 +129,7 @@ private static async Task UpdateUser( IPermissionService permissionService, ChangeUserAccountDataInput input, LexBoxDbContext dbContext, - EmailService emailService + IEmailService emailService ) { var user = await dbContext.Users.FindAsync(input.UserId); diff --git a/backend/LexBoxApi/Jobs/RetryEmailJob.cs b/backend/LexBoxApi/Jobs/RetryEmailJob.cs index f2bf4c729..864bbda48 100644 --- a/backend/LexBoxApi/Jobs/RetryEmailJob.cs +++ b/backend/LexBoxApi/Jobs/RetryEmailJob.cs @@ -1,10 +1,10 @@ -using LexBoxApi.Services; +using LexBoxApi.Services.Email; using MimeKit; using Quartz; namespace LexBoxApi.Jobs; -public class RetryEmailJob(EmailService emailService) : LexJob +public class RetryEmailJob(IEmailService emailService) : LexJob { public static async Task Queue(ISchedulerFactory schedulerFactory, MimeMessage email, diff --git a/backend/LexBoxApi/LexBoxKernel.cs b/backend/LexBoxApi/LexBoxKernel.cs index 47f9a946a..cc336503a 100644 --- a/backend/LexBoxApi/LexBoxKernel.cs +++ b/backend/LexBoxApi/LexBoxKernel.cs @@ -3,6 +3,7 @@ using LexBoxApi.GraphQL; using LexBoxApi.GraphQL.CustomTypes; using LexBoxApi.Services; +using LexBoxApi.Services.Email; using LexCore.Config; using LexCore.ServiceInterfaces; using LexSyncReverseProxy; @@ -50,7 +51,7 @@ public static void AddLexBoxApi(this IServiceCollection services, services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/backend/LexBoxApi/Models/Org/ChangeOrgInputs.cs b/backend/LexBoxApi/Models/Org/ChangeOrgInputs.cs new file mode 100644 index 000000000..646803106 --- /dev/null +++ b/backend/LexBoxApi/Models/Org/ChangeOrgInputs.cs @@ -0,0 +1,3 @@ +namespace LexBoxApi.Models.Org; + +public record ChangeOrgNameInput(Guid OrgId, string Name); diff --git a/backend/LexBoxApi/Models/Project/CreateProjectInput.cs b/backend/LexBoxApi/Models/Project/CreateProjectInput.cs index e97c6720d..9e942925f 100644 --- a/backend/LexBoxApi/Models/Project/CreateProjectInput.cs +++ b/backend/LexBoxApi/Models/Project/CreateProjectInput.cs @@ -12,5 +12,6 @@ public record CreateProjectInput( ProjectType Type, RetentionPolicy RetentionPolicy, bool IsConfidential, + Guid? OwningOrgId, Guid? ProjectManagerId ); diff --git a/backend/LexBoxApi/Program.cs b/backend/LexBoxApi/Program.cs index 889b02293..c867153ca 100644 --- a/backend/LexBoxApi/Program.cs +++ b/backend/LexBoxApi/Program.cs @@ -162,7 +162,6 @@ app.MapQuartzUI("/api/quartz").RequireAuthorization(new AdminRequiredAttribute()); app.MapControllers(); app.MapLfClassicApi().RequireAuthorization(new AdminRequiredAttribute()).WithOpenApi(); -app.MapSyncApi().WithOpenApi(); app.MapTus("/api/tus-test", async context => await context.RequestServices.GetRequiredService().GetTestConfig(context)) .RequireAuthorization(new AdminRequiredAttribute()); diff --git a/backend/LexBoxApi/Services/CrdtSyncRoutes.cs b/backend/LexBoxApi/Services/CrdtSyncRoutes.cs deleted file mode 100644 index c3bb92f1c..000000000 --- a/backend/LexBoxApi/Services/CrdtSyncRoutes.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Crdt.Core; -using LexBoxApi.Auth.Attributes; -using LexData; -using LexData.Entities; - -namespace LexBoxApi.Services; - -public static class CrdtSyncRoutes -{ - public static IEndpointConventionBuilder MapSyncApi(this IEndpointRouteBuilder endpoints, - string path = "/api/sync/{id}") - { - //todo determine if the user has permission to access the project, for now lock down to admin only - var group = endpoints.MapGroup(path).RequireAuthorization(new AdminRequiredAttribute()); - group.MapGet("/get", - async (Guid id, LexBoxDbContext dbContext) => - { - return await dbContext.Set().Where(c => c.ProjectId == id).GetSyncState(); - }); - group.MapPost("/add", - async (Guid id, ServerCommit[] commits, LexBoxDbContext dbContext) => - { - foreach (var commit in commits) - { - commit.ProjectId = id; - dbContext.Add(commit);//todo should only add if not exists, based on commit id - } - - await dbContext.SaveChangesAsync(); - }); - group.MapPost("/changes", - async (Guid id, SyncState clientHeads, LexBoxDbContext dbContext) => - { - var commits = dbContext.Set().Where(c => c.ProjectId == id); - return await commits.GetChanges(clientHeads); - }); - - return group; - } -} diff --git a/backend/LexBoxApi/Services/Email/EmailTemplates.cs b/backend/LexBoxApi/Services/Email/EmailTemplates.cs index b23db8d8f..6d7684f52 100644 --- a/backend/LexBoxApi/Services/Email/EmailTemplates.cs +++ b/backend/LexBoxApi/Services/Email/EmailTemplates.cs @@ -18,7 +18,9 @@ public enum EmailTemplate VerifyEmailAddress, PasswordChanged, CreateAccountRequest, - CreateProjectRequest + CreateProjectRequest, + ApproveProjectRequest, + UserAdded, } public record ForgotPasswordEmail(string Name, string ResetUrl, TimeSpan lifetime) : EmailTemplateBase(EmailTemplate.ForgotPassword); @@ -32,4 +34,6 @@ public record ProjectInviteEmail(string Email, string ProjectId, string ManagerN public record PasswordChangedEmail(string Name) : EmailTemplateBase(EmailTemplate.PasswordChanged); public record CreateProjectRequestUser(string Name, string Email); -public record CreateProjectRequestEmail(string Name, CreateProjectRequestUser User, CreateProjectInput Project): EmailTemplateBase(EmailTemplate.CreateProjectRequest); +public record CreateProjectRequestEmail(string Name, CreateProjectRequestUser User, CreateProjectInput Project) : EmailTemplateBase(EmailTemplate.CreateProjectRequest); +public record ApproveProjectRequestEmail(string Name, CreateProjectRequestUser User, CreateProjectInput Project) : EmailTemplateBase(EmailTemplate.ApproveProjectRequest); +public record UserAddedEmail(string Name, string Email, string ProjectName, string ProjectCode) : EmailTemplateBase(EmailTemplate.UserAdded); diff --git a/backend/LexBoxApi/Services/Email/IEmailService.cs b/backend/LexBoxApi/Services/Email/IEmailService.cs new file mode 100644 index 000000000..fce76a58e --- /dev/null +++ b/backend/LexBoxApi/Services/Email/IEmailService.cs @@ -0,0 +1,44 @@ +using LexBoxApi.Models.Project; +using LexCore.Auth; +using LexCore.Entities; +using MimeKit; + +namespace LexBoxApi.Services.Email; + +public interface IEmailService +{ + public Task SendForgotPasswordEmail(string emailAddress); + + public Task SendNewAdminEmail(IAsyncEnumerable admins, string newAdminName, string newAdminEmail); + + /// + /// Sends a verification email to the user for their email address. + /// + /// The user to verify the email address for. + /// + /// If the user is trying to change their address, this is the new email address. + /// If null, the verification email will be sent to the current email address of the user. + /// + public Task SendVerifyAddressEmail(User user, string? newEmail = null); + + /// + /// Sends a project invitation email to a new user, whose account will be created when they accept. + /// + /// The name (real name, NOT username) of user to invite. + /// The email address to send the invitation to + /// The GUID of the project the user is being invited to + /// The language in which the invitation email should be sent (default English) + public Task SendCreateAccountEmail(string emailAddress, + Guid projectId, + ProjectRole role, + string managerName, + string projectName, + string? language = null); + + public Task SendPasswordChangedEmail(User user); + + public Task SendCreateProjectRequestEmail(LexAuthUser user, CreateProjectInput projectInput); + public Task SendApproveProjectRequestEmail(User user, CreateProjectInput projectInput); + public Task SendUserAddedEmail(User user, string projectName, string projectCode); + public Task SendEmailAsync(MimeMessage message); +} diff --git a/backend/LexBoxApi/Services/EmailService.cs b/backend/LexBoxApi/Services/EmailService.cs index af8c46a93..6ba247889 100644 --- a/backend/LexBoxApi/Services/EmailService.cs +++ b/backend/LexBoxApi/Services/EmailService.cs @@ -25,7 +25,7 @@ public class EmailService( LexboxLinkGenerator linkGenerator, IHttpContextAccessor httpContextAccessor, Quartz.ISchedulerFactory schedulerFactory, - LexAuthService lexAuthService) + LexAuthService lexAuthService) : IEmailService { private readonly EmailConfig _emailConfig = emailConfig.Value; private readonly LinkGenerator _linkGenerator = linkGenerator; @@ -156,7 +156,19 @@ await RenderEmail(email, new CreateProjectRequestEmail("Admin", new CreateProjectRequestUser(user.Name, user.Email), projectInput), "en"); await SendEmailWithRetriesAsync(email); } - + public async Task SendApproveProjectRequestEmail(User user, CreateProjectInput projectInput) + { + var email = StartUserEmail(user) ?? throw new ArgumentNullException("emailAddress"); + await RenderEmail(email, + new ApproveProjectRequestEmail(user.Name, new CreateProjectRequestUser(user.Name, user.Email!), projectInput), user.LocalizationCode); + await SendEmailWithRetriesAsync(email); + } + public async Task SendUserAddedEmail(User user, string projectName, string projectCode) + { + var email = StartUserEmail(user) ?? throw new ArgumentNullException("emailAddress"); + await RenderEmail(email, new UserAddedEmail(user.Name, user.Email!, projectName, projectCode), user.LocalizationCode); + await SendEmailWithRetriesAsync(email); + } public async Task SendEmailAsync(MimeMessage message) { message.From.Add(MailboxAddress.Parse(_emailConfig.From)); @@ -181,7 +193,7 @@ public async Task SendEmailAsync(MimeMessage message) throw; } } - private async Task SendEmailWithRetriesAsync(MimeMessage message, int retryCount = 3, int retryWaitSeconds = 5 * 60) + private async Task SendEmailWithRetriesAsync(MimeMessage message, int retryCount = 3, int retryWaitSeconds = 5 * 60) { try { diff --git a/backend/LexBoxApi/Services/PermissionService.cs b/backend/LexBoxApi/Services/PermissionService.cs index e394c6eab..9f0994f12 100644 --- a/backend/LexBoxApi/Services/PermissionService.cs +++ b/backend/LexBoxApi/Services/PermissionService.cs @@ -108,4 +108,12 @@ public void AssertCanEditOrg(Organization org) if (org.Members.Any(m => m.UserId == User.Id && m.Role == OrgRole.Admin)) return; throw new UnauthorizedAccessException(); } + + public void AssertCanAddProjectToOrg(Organization org) + { + if (User is null) throw new UnauthorizedAccessException(); + if (User.Role == UserRole.admin) return; + if (org.Members.Any(m => m.UserId == User.Id)) return; + throw new UnauthorizedAccessException(); + } } diff --git a/backend/LexBoxApi/Services/ProjectService.cs b/backend/LexBoxApi/Services/ProjectService.cs index 5e351fc38..ccb3bae3d 100644 --- a/backend/LexBoxApi/Services/ProjectService.cs +++ b/backend/LexBoxApi/Services/ProjectService.cs @@ -1,5 +1,6 @@ using System.Data.Common; using LexBoxApi.Models.Project; +using LexBoxApi.Services.Email; using LexCore.Config; using LexCore.Entities; using LexCore.Exceptions; @@ -11,7 +12,7 @@ namespace LexBoxApi.Services; -public class ProjectService(LexBoxDbContext dbContext, IHgService hgService, IOptions hgConfig, IMemoryCache memoryCache) +public class ProjectService(LexBoxDbContext dbContext, IHgService hgService, IOptions hgConfig, IMemoryCache memoryCache, IEmailService emailService) { public async Task CreateProject(CreateProjectInput input) { @@ -34,6 +35,7 @@ public async Task CreateProject(CreateProjectInput input) LastCommit = null, RetentionPolicy = input.RetentionPolicy, IsConfidential = isConfidentialIsUntrustworthy ? null : input.IsConfidential, + Organizations = [], Users = input.ProjectManagerId.HasValue ? [new() { UserId = input.ProjectManagerId.Value, Role = ProjectRole.Manager }] : [], }); // Also delete draft project, if any @@ -42,7 +44,16 @@ public async Task CreateProject(CreateProjectInput input) { var manager = await dbContext.Users.FindAsync(input.ProjectManagerId.Value); manager?.UpdateCreateProjectsPermission(ProjectRole.Manager); - + if (draftProject != null && manager != null) + { + await emailService.SendApproveProjectRequestEmail(manager, input); + } + } + if (input.OwningOrgId.HasValue) + { + dbContext.OrgProjects.Add( + new OrgProjects { ProjectId = projectId, OrgId = input.OwningOrgId.Value } + ); } await dbContext.SaveChangesAsync(); await hgService.InitRepo(input.Code); diff --git a/backend/LexBoxApi/Services/UserService.cs b/backend/LexBoxApi/Services/UserService.cs index ed7f12c66..3fa44c10e 100644 --- a/backend/LexBoxApi/Services/UserService.cs +++ b/backend/LexBoxApi/Services/UserService.cs @@ -1,10 +1,11 @@ using LexBoxApi.Auth; +using LexBoxApi.Services.Email; using LexData; using Microsoft.EntityFrameworkCore; namespace LexBoxApi.Services; -public class UserService(LexBoxDbContext dbContext, EmailService emailService, LexAuthService lexAuthService) +public class UserService(LexBoxDbContext dbContext, IEmailService emailService, LexAuthService lexAuthService) { public async Task ForgotPassword(string email) { diff --git a/backend/LexCore/Entities/OrgProjects.cs b/backend/LexCore/Entities/OrgProjects.cs new file mode 100644 index 000000000..e465fe895 --- /dev/null +++ b/backend/LexCore/Entities/OrgProjects.cs @@ -0,0 +1,9 @@ +namespace LexCore.Entities; + +public class OrgProjects : EntityBase +{ + public Guid OrgId { get; set; } + public Guid ProjectId { get; set; } + public Organization? Org { get; set; } + public Project? Project { get; set; } +} diff --git a/backend/LexCore/Entities/Organization.cs b/backend/LexCore/Entities/Organization.cs index b80404fcf..6956608a2 100644 --- a/backend/LexCore/Entities/Organization.cs +++ b/backend/LexCore/Entities/Organization.cs @@ -1,9 +1,24 @@ -namespace LexCore.Entities; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq.Expressions; +using EntityFrameworkCore.Projectables; + +namespace LexCore.Entities; public class Organization : EntityBase { public required string Name { get; set; } public required List Members { get; set; } + public required List Projects { get; set; } + + [NotMapped] + [Projectable(UseMemberBody = nameof(SqlMemberCount))] + public int MemberCount { get; set; } + private static Expression> SqlMemberCount => org => org.Members.Count; + + [NotMapped] + [Projectable(UseMemberBody = nameof(SqlProjectCount))] + public int ProjectCount { get; set; } + private static Expression> SqlProjectCount => org => org.Projects.Count; } public class OrgMember : EntityBase diff --git a/backend/LexCore/Entities/Project.cs b/backend/LexCore/Entities/Project.cs index 37f2e9b32..0d6114d7b 100644 --- a/backend/LexCore/Entities/Project.cs +++ b/backend/LexCore/Entities/Project.cs @@ -18,6 +18,7 @@ public class Project : EntityBase public required bool? IsConfidential { get; set; } public FlexProjectMetadata? FlexProjectMetadata { get; set; } public required List Users { get; set; } + public required List Organizations { get; set; } public required DateTimeOffset? LastCommit { get; set; } public DateTimeOffset? DeletedDate { get; set; } public ResetStatus ResetStatus { get; set; } = ResetStatus.None; diff --git a/backend/LexCore/ServiceInterfaces/IPermissionService.cs b/backend/LexCore/ServiceInterfaces/IPermissionService.cs index c23718d45..ffe8c136c 100644 --- a/backend/LexCore/ServiceInterfaces/IPermissionService.cs +++ b/backend/LexCore/ServiceInterfaces/IPermissionService.cs @@ -20,4 +20,5 @@ public interface IPermissionService void AssertCanLockOrUnlockUser(Guid userId); void AssertCanCreateOrg(); void AssertCanEditOrg(Organization org); + void AssertCanAddProjectToOrg(Organization org); } diff --git a/backend/LexData/DbStartupService.cs b/backend/LexData/DbStartupService.cs index 5ac000d2e..316ded684 100644 --- a/backend/LexData/DbStartupService.cs +++ b/backend/LexData/DbStartupService.cs @@ -45,9 +45,14 @@ public async Task StartAsync(CancellationToken cancellationToken) var dbContext = serviceScope.ServiceProvider.GetRequiredService(); await dbContext.Database.MigrateAsync(cancellationToken); var environment = serviceScope.ServiceProvider.GetRequiredService(); + var seedingData = serviceScope.ServiceProvider.GetRequiredService(); if (environment.IsDevelopment() || environment.IsStaging()) { - await serviceScope.ServiceProvider.GetRequiredService().SeedIfNoUsers(cancellationToken); + await seedingData.SeedIfNoUsers(cancellationToken); + } + else + { + await seedingData.SeedOAuth(cancellationToken); } _migrateExecuted = true; diff --git a/backend/LexData/Entities/OrgProjectsEntityConfiguration.cs b/backend/LexData/Entities/OrgProjectsEntityConfiguration.cs new file mode 100644 index 000000000..d74888a68 --- /dev/null +++ b/backend/LexData/Entities/OrgProjectsEntityConfiguration.cs @@ -0,0 +1,15 @@ +using LexCore.Entities; +using LexData.Configuration; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace LexData.Entities; + +public class OrgProjectsEntityConfiguration : EntityBaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + builder.HasIndex(op => new { op.OrgId, op.ProjectId }).IsUnique(); + builder.HasQueryFilter(op => op.Project!.DeletedDate == null); + } +} diff --git a/backend/LexData/Entities/OrganizationEntityConfiguration.cs b/backend/LexData/Entities/OrganizationEntityConfiguration.cs index 9ac666c8d..b8eda4636 100644 --- a/backend/LexData/Entities/OrganizationEntityConfiguration.cs +++ b/backend/LexData/Entities/OrganizationEntityConfiguration.cs @@ -16,5 +16,11 @@ public override void Configure(EntityTypeBuilder builder) .WithOne(m => m.Organization) .HasForeignKey(m => m.OrgId) .OnDelete(DeleteBehavior.Cascade); + builder.HasMany(o => o.Projects) + .WithMany(p => p.Organizations) + .UsingEntity( + op => op.HasOne(op => op.Project).WithMany().HasForeignKey(op => op.ProjectId), + op => op.HasOne(op => op.Org).WithMany().HasForeignKey(op => op.OrgId) + ); } } diff --git a/backend/LexData/Entities/ProjectEntityConfiguration.cs b/backend/LexData/Entities/ProjectEntityConfiguration.cs index add56a126..730152cb3 100644 --- a/backend/LexData/Entities/ProjectEntityConfiguration.cs +++ b/backend/LexData/Entities/ProjectEntityConfiguration.cs @@ -24,6 +24,12 @@ public override void Configure(EntityTypeBuilder builder) .WithOne(projectUser => projectUser.Project) .HasForeignKey(projectUser => projectUser.ProjectId) .OnDelete(DeleteBehavior.Cascade); + builder.HasMany(p => p.Organizations) + .WithMany(o => o.Projects) + .UsingEntity( + op => op.HasOne(op => op.Org).WithMany().HasForeignKey(op => op.OrgId), + op => op.HasOne(op => op.Project).WithMany().HasForeignKey(op => op.ProjectId) + ); builder.HasQueryFilter(p => p.DeletedDate == null); } } diff --git a/backend/LexData/LexBoxDbContext.cs b/backend/LexData/LexBoxDbContext.cs index 35ac21548..27489c257 100644 --- a/backend/LexData/LexBoxDbContext.cs +++ b/backend/LexData/LexBoxDbContext.cs @@ -29,6 +29,7 @@ protected override void ConfigureConventions(ModelConfigurationBuilder builder) public DbSet ProjectUsers => Set(); public DbSet DraftProjects => Set(); public DbSet Orgs => Set(); + public DbSet OrgProjects => Set(); public async Task HeathCheck(CancellationToken cancellationToken) { diff --git a/backend/LexData/Migrations/20240603231034_AllowCommitHashToBeNull.Designer.cs b/backend/LexData/Migrations/20240603231034_AllowCommitHashToBeNull.Designer.cs new file mode 100644 index 000000000..0cd47c8a0 --- /dev/null +++ b/backend/LexData/Migrations/20240603231034_AllowCommitHashToBeNull.Designer.cs @@ -0,0 +1,1273 @@ +// +using System; +using System.Collections.Generic; +using LexData; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LexData.Migrations +{ + [DbContext(typeof(LexBoxDbContext))] + [Migration("20240603231034_AllowCommitHashToBeNull")] + partial class AllowCommitHashToBeNull + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "und-u-ks-level2,und-u-ks-level2,icu,False") + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_blob_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("qrtz_calendars", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_cron_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_j_req_recovery"); + + b.ToTable("qrtz_job_details", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("qrtz_locks", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("qrtz_paused_trigger_grps", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("qrtz_scheduler_state", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simprop_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simple_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_qrtz_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_qrtz_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_qrtz_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("qrtz_triggers", "quartz"); + }); + + modelBuilder.Entity("Crdt.Core.ServerCommit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("Hash") + .HasColumnType("text"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentHash") + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.ComplexProperty>("HybridDateTime", "Crdt.Core.ServerCommit.HybridDateTime#HybridDateTime", b1 => + { + b1.IsRequired(); + + b1.Property("Counter") + .HasColumnType("bigint"); + + b1.Property("DateTime") + .HasColumnType("timestamp with time zone"); + }); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("CrdtCommits", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectManagerId") + .HasColumnType("uuid"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ProjectManagerId"); + + b.ToTable("DraftProjects"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("LexEntryCount") + .HasColumnType("integer"); + + b.HasKey("ProjectId"); + + b.ToTable("FlexProjectMetadata"); + }); + + modelBuilder.Entity("LexCore.Entities.OrgMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrgId") + .HasColumnType("uuid"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrgId"); + + b.HasIndex("UserId", "OrgId") + .IsUnique(); + + b.ToTable("OrgMembers", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Orgs", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("LastCommit") + .HasColumnType("timestamp with time zone"); + + b.Property("MigratedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("ProjectOrigin") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ResetStatus") + .HasColumnType("integer"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ParentId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId", "ProjectId") + .IsUnique(); + + b.ToTable("ProjectUsers"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanCreateProjects") + .HasColumnType("boolean"); + + b.Property("CreatedById") + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("GoogleId") + .HasColumnType("text"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("LastActive") + .HasColumnType("timestamp with time zone"); + + b.Property("LocalizationCode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("en"); + + b.Property("Locked") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordStrength") + .HasColumnType("integer"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Username") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Crdt.Core.ServerCommit", b => + { + b.HasOne("LexCore.Entities.FlexProjectMetadata", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Crdt.Core.ChangeEntity", "ChangeEntities", b1 => + { + b1.Property("ServerCommitId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b1.Property("Change") + .HasColumnType("text"); + + b1.Property("CommitId") + .HasColumnType("uuid"); + + b1.Property("EntityId") + .HasColumnType("uuid"); + + b1.Property("Index") + .HasColumnType("integer"); + + b1.HasKey("ServerCommitId", "Id"); + + b1.ToTable("CrdtCommits"); + + b1.ToJson("ChangeEntities"); + + b1.WithOwner() + .HasForeignKey("ServerCommitId"); + }); + + b.Navigation("ChangeEntities"); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.HasOne("LexCore.Entities.User", "ProjectManager") + .WithMany() + .HasForeignKey("ProjectManagerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("ProjectManager"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithOne("FlexProjectMetadata") + .HasForeignKey("LexCore.Entities.FlexProjectMetadata", "ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LexCore.Entities.OrgMember", b => + { + b.HasOne("LexCore.Entities.Organization", "Organization") + .WithMany("Members") + .HasForeignKey("OrgId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.User", "User") + .WithMany("Organizations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.HasOne("LexCore.Entities.Project", "Project") + .WithMany("Users") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.User", "User") + .WithMany("Projects") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.HasOne("LexCore.Entities.User", "CreatedBy") + .WithMany("UsersICreated") + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); + + modelBuilder.Entity("LexCore.Entities.Organization", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Navigation("FlexProjectMetadata"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Navigation("Organizations"); + + b.Navigation("Projects"); + + b.Navigation("UsersICreated"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/LexData/Migrations/20240603231034_AllowCommitHashToBeNull.cs b/backend/LexData/Migrations/20240603231034_AllowCommitHashToBeNull.cs new file mode 100644 index 000000000..f27ff5b26 --- /dev/null +++ b/backend/LexData/Migrations/20240603231034_AllowCommitHashToBeNull.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LexData.Migrations +{ + /// + public partial class AllowCommitHashToBeNull : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ParentHash", + table: "CrdtCommits", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Hash", + table: "CrdtCommits", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ParentHash", + table: "CrdtCommits", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Hash", + table: "CrdtCommits", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + } +} diff --git a/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.Designer.cs b/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.Designer.cs new file mode 100644 index 000000000..42c80a2df --- /dev/null +++ b/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.Designer.cs @@ -0,0 +1,1328 @@ +// +using System; +using System.Collections.Generic; +using LexData; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LexData.Migrations +{ + [DbContext(typeof(LexBoxDbContext))] + [Migration("20240606084230_AddOrgProjectsTable")] + partial class AddOrgProjectsTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "und-u-ks-level2,und-u-ks-level2,icu,False") + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_blob_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("qrtz_calendars", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_cron_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_j_req_recovery"); + + b.ToTable("qrtz_job_details", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("qrtz_locks", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("qrtz_paused_trigger_grps", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("qrtz_scheduler_state", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simprop_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simple_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_qrtz_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_qrtz_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_qrtz_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("qrtz_triggers", "quartz"); + }); + + modelBuilder.Entity("Crdt.Core.ServerCommit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.ComplexProperty>("HybridDateTime", "Crdt.Core.ServerCommit.HybridDateTime#HybridDateTime", b1 => + { + b1.IsRequired(); + + b1.Property("Counter") + .HasColumnType("bigint"); + + b1.Property("DateTime") + .HasColumnType("timestamp with time zone"); + }); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("CrdtCommits", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectManagerId") + .HasColumnType("uuid"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ProjectManagerId"); + + b.ToTable("DraftProjects"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("LexEntryCount") + .HasColumnType("integer"); + + b.HasKey("ProjectId"); + + b.ToTable("FlexProjectMetadata"); + }); + + modelBuilder.Entity("LexCore.Entities.OrgMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrgId") + .HasColumnType("uuid"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrgId"); + + b.HasIndex("UserId", "OrgId") + .IsUnique(); + + b.ToTable("OrgMembers", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.OrgProjects", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrgId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrgId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("OrgId", "ProjectId") + .IsUnique(); + + b.ToTable("OrgProjects"); + }); + + modelBuilder.Entity("LexCore.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Orgs", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("LastCommit") + .HasColumnType("timestamp with time zone"); + + b.Property("MigratedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("ProjectOrigin") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ResetStatus") + .HasColumnType("integer"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ParentId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId", "ProjectId") + .IsUnique(); + + b.ToTable("ProjectUsers"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanCreateProjects") + .HasColumnType("boolean"); + + b.Property("CreatedById") + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("GoogleId") + .HasColumnType("text"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("LastActive") + .HasColumnType("timestamp with time zone"); + + b.Property("LocalizationCode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("en"); + + b.Property("Locked") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordStrength") + .HasColumnType("integer"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Username") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Crdt.Core.ServerCommit", b => + { + b.HasOne("LexCore.Entities.FlexProjectMetadata", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Crdt.Core.ChangeEntity", "ChangeEntities", b1 => + { + b1.Property("ServerCommitId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b1.Property("Change") + .HasColumnType("text"); + + b1.Property("CommitId") + .HasColumnType("uuid"); + + b1.Property("EntityId") + .HasColumnType("uuid"); + + b1.Property("Index") + .HasColumnType("integer"); + + b1.HasKey("ServerCommitId", "Id"); + + b1.ToTable("CrdtCommits"); + + b1.ToJson("ChangeEntities"); + + b1.WithOwner() + .HasForeignKey("ServerCommitId"); + }); + + b.Navigation("ChangeEntities"); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.HasOne("LexCore.Entities.User", "ProjectManager") + .WithMany() + .HasForeignKey("ProjectManagerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("ProjectManager"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithOne("FlexProjectMetadata") + .HasForeignKey("LexCore.Entities.FlexProjectMetadata", "ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LexCore.Entities.OrgMember", b => + { + b.HasOne("LexCore.Entities.Organization", "Organization") + .WithMany("Members") + .HasForeignKey("OrgId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.User", "User") + .WithMany("Organizations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LexCore.Entities.OrgProjects", b => + { + b.HasOne("LexCore.Entities.Organization", "Org") + .WithMany() + .HasForeignKey("OrgId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Org"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.HasOne("LexCore.Entities.Project", "Project") + .WithMany("Users") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.User", "User") + .WithMany("Projects") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.HasOne("LexCore.Entities.User", "CreatedBy") + .WithMany("UsersICreated") + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); + + modelBuilder.Entity("LexCore.Entities.Organization", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Navigation("FlexProjectMetadata"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Navigation("Organizations"); + + b.Navigation("Projects"); + + b.Navigation("UsersICreated"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.cs b/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.cs new file mode 100644 index 000000000..41023967b --- /dev/null +++ b/backend/LexData/Migrations/20240606084230_AddOrgProjectsTable.cs @@ -0,0 +1,65 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LexData.Migrations +{ + /// + public partial class AddOrgProjectsTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrgProjects", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrgId = table.Column(type: "uuid", nullable: false), + ProjectId = table.Column(type: "uuid", nullable: false), + CreatedDate = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + UpdatedDate = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()") + }, + constraints: table => + { + table.PrimaryKey("PK_OrgProjects", x => x.Id); + table.ForeignKey( + name: "FK_OrgProjects_Orgs_OrgId", + column: x => x.OrgId, + principalTable: "Orgs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrgProjects_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrgProjects_OrgId_ProjectId", + table: "OrgProjects", + columns: new[] { "OrgId", "ProjectId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OrgProjects_OrgId", + table: "OrgProjects", + column: "OrgId"); + + migrationBuilder.CreateIndex( + name: "IX_OrgProjects_ProjectId", + table: "OrgProjects", + column: "ProjectId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrgProjects"); + } + } +} diff --git a/backend/LexData/Migrations/20240611215238_RemoveCrdtHashFields.Designer.cs b/backend/LexData/Migrations/20240611215238_RemoveCrdtHashFields.Designer.cs new file mode 100644 index 000000000..0589338a9 --- /dev/null +++ b/backend/LexData/Migrations/20240611215238_RemoveCrdtHashFields.Designer.cs @@ -0,0 +1,1267 @@ +// +using System; +using System.Collections.Generic; +using LexData; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LexData.Migrations +{ + [DbContext(typeof(LexBoxDbContext))] + [Migration("20240611215238_RemoveCrdtHashFields")] + partial class RemoveCrdtHashFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "und-u-ks-level2,und-u-ks-level2,icu,False") + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_blob_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("qrtz_calendars", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_cron_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_j_req_recovery"); + + b.ToTable("qrtz_job_details", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("qrtz_locks", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("qrtz_paused_trigger_grps", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("qrtz_scheduler_state", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simprop_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simple_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_qrtz_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_qrtz_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_qrtz_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("qrtz_triggers", "quartz"); + }); + + modelBuilder.Entity("Crdt.Core.ServerCommit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.ComplexProperty>("HybridDateTime", "Crdt.Core.ServerCommit.HybridDateTime#HybridDateTime", b1 => + { + b1.IsRequired(); + + b1.Property("Counter") + .HasColumnType("bigint"); + + b1.Property("DateTime") + .HasColumnType("timestamp with time zone"); + }); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("CrdtCommits", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectManagerId") + .HasColumnType("uuid"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ProjectManagerId"); + + b.ToTable("DraftProjects"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("LexEntryCount") + .HasColumnType("integer"); + + b.HasKey("ProjectId"); + + b.ToTable("FlexProjectMetadata"); + }); + + modelBuilder.Entity("LexCore.Entities.OrgMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrgId") + .HasColumnType("uuid"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrgId"); + + b.HasIndex("UserId", "OrgId") + .IsUnique(); + + b.ToTable("OrgMembers", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Orgs", (string)null); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("LastCommit") + .HasColumnType("timestamp with time zone"); + + b.Property("MigratedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("ProjectOrigin") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ResetStatus") + .HasColumnType("integer"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ParentId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId", "ProjectId") + .IsUnique(); + + b.ToTable("ProjectUsers"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanCreateProjects") + .HasColumnType("boolean"); + + b.Property("CreatedById") + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("GoogleId") + .HasColumnType("text"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("LastActive") + .HasColumnType("timestamp with time zone"); + + b.Property("LocalizationCode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("en"); + + b.Property("Locked") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordStrength") + .HasColumnType("integer"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Username") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("Crdt.Core.ServerCommit", b => + { + b.HasOne("LexCore.Entities.FlexProjectMetadata", null) + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Crdt.Core.ChangeEntity", "ChangeEntities", b1 => + { + b1.Property("ServerCommitId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b1.Property("Change") + .HasColumnType("text"); + + b1.Property("CommitId") + .HasColumnType("uuid"); + + b1.Property("EntityId") + .HasColumnType("uuid"); + + b1.Property("Index") + .HasColumnType("integer"); + + b1.HasKey("ServerCommitId", "Id"); + + b1.ToTable("CrdtCommits"); + + b1.ToJson("ChangeEntities"); + + b1.WithOwner() + .HasForeignKey("ServerCommitId"); + }); + + b.Navigation("ChangeEntities"); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.HasOne("LexCore.Entities.User", "ProjectManager") + .WithMany() + .HasForeignKey("ProjectManagerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("ProjectManager"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithOne("FlexProjectMetadata") + .HasForeignKey("LexCore.Entities.FlexProjectMetadata", "ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LexCore.Entities.OrgMember", b => + { + b.HasOne("LexCore.Entities.Organization", "Organization") + .WithMany("Members") + .HasForeignKey("OrgId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.User", "User") + .WithMany("Organizations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.HasOne("LexCore.Entities.Project", "Project") + .WithMany("Users") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.User", "User") + .WithMany("Projects") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.HasOne("LexCore.Entities.User", "CreatedBy") + .WithMany("UsersICreated") + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); + + modelBuilder.Entity("LexCore.Entities.Organization", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Navigation("FlexProjectMetadata"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Navigation("Organizations"); + + b.Navigation("Projects"); + + b.Navigation("UsersICreated"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/LexData/Migrations/20240611215238_RemoveCrdtHashFields.cs b/backend/LexData/Migrations/20240611215238_RemoveCrdtHashFields.cs new file mode 100644 index 000000000..412b215de --- /dev/null +++ b/backend/LexData/Migrations/20240611215238_RemoveCrdtHashFields.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LexData.Migrations +{ + /// + public partial class RemoveCrdtHashFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Hash", + table: "CrdtCommits"); + + migrationBuilder.DropColumn( + name: "ParentHash", + table: "CrdtCommits"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Hash", + table: "CrdtCommits", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "ParentHash", + table: "CrdtCommits", + type: "text", + nullable: true); + } + } +} diff --git a/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs b/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs index 0b5f4e5e6..21f188264 100644 --- a/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs +++ b/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs @@ -476,18 +476,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ClientId") .HasColumnType("uuid"); - b.Property("Hash") - .IsRequired() - .HasColumnType("text"); - b.Property("Metadata") .IsRequired() .HasColumnType("text"); - b.Property("ParentHash") - .IsRequired() - .HasColumnType("text"); - b.Property("ProjectId") .HasColumnType("uuid"); @@ -607,6 +599,40 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("OrgMembers", (string)null); }); + modelBuilder.Entity("LexCore.Entities.OrgProjects", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrgId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrgId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("OrgId", "ProjectId") + .IsUnique(); + + b.ToTable("OrgProjects"); + }); + modelBuilder.Entity("LexCore.Entities.Organization", b => { b.Property("Id") @@ -1158,6 +1184,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("LexCore.Entities.OrgProjects", b => + { + b.HasOne("LexCore.Entities.Organization", "Org") + .WithMany() + .HasForeignKey("OrgId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Org"); + + b.Navigation("Project"); + }); + modelBuilder.Entity("LexCore.Entities.Project", b => { b.HasOne("LexCore.Entities.Project", null) diff --git a/backend/LexData/SeedingData.cs b/backend/LexData/SeedingData.cs index 0d4bb4994..abea3ece3 100644 --- a/backend/LexData/SeedingData.cs +++ b/backend/LexData/SeedingData.cs @@ -1,23 +1,34 @@ +using System.Collections.Immutable; using LexCore; using LexCore.Entities; using LexData.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; +using OpenIddict.EntityFrameworkCore.Models; namespace LexData; -public class SeedingData(LexBoxDbContext lexBoxDbContext, IOptions dbConfig, IHostEnvironment environment, IOpenIddictApplicationManager? applicationManager = null) +public class SeedingData( + LexBoxDbContext lexBoxDbContext, + IOptions dbConfig, + IHostEnvironment environment, + ILogger logger, + IOpenIddictApplicationManager? applicationManager = null) { public static readonly Guid TestAdminId = new("cf430ec9-e721-450a-b6a1-9a853212590b"); public static readonly Guid QaAdminId = new("99b00c58-0dc7-4fe4-b6f2-c27b828811e0"); private static readonly Guid MangerId = new Guid("703701a8-005c-4747-91f2-ac7650455118"); private static readonly Guid EditorId = new Guid("6dc9965b-4021-4606-92df-133fcce75fcb"); + private static readonly Guid TestOrgId = new Guid("292c80e6-a815-4cd1-9ea2-34bd01274de6"); + private static readonly Guid SecondTestOrgId = new Guid("a748bd8b-6348-4980-8dee-6de8b63e4a39"); + private static readonly Guid Sena3ProjId = new Guid("0ebc5976-058d-4447-aaa7-297f8569f968"); public async Task SeedIfNoUsers(CancellationToken cancellationToken = default) { - await SeedOpenId(cancellationToken); + await SeedOAuth(cancellationToken); if (await lexBoxDbContext.Users.CountAsync(cancellationToken) > 0) { return; @@ -28,7 +39,7 @@ public async Task SeedIfNoUsers(CancellationToken cancellationToken = default) public async Task SeedDatabase(CancellationToken cancellationToken = default) { - await SeedOpenId(cancellationToken); + await SeedOAuth(cancellationToken); await SeedUserData(cancellationToken); } @@ -85,7 +96,7 @@ private async Task SeedUserData(CancellationToken cancellationToken = default) lexBoxDbContext.Attach(new Project { - Id = new Guid("0ebc5976-058d-4447-aaa7-297f8569f968"), + Id = Sena3ProjId, Name = "Sena 3", Code = "sena-3", Type = ProjectType.FLEx, @@ -97,6 +108,7 @@ private async Task SeedUserData(CancellationToken cancellationToken = default) LexEntryCount = -1 }, IsConfidential = null, + Organizations = [], Users = new() { new() @@ -146,13 +158,15 @@ private async Task SeedUserData(CancellationToken cancellationToken = default) LastCommit = DateTimeOffset.UtcNow, RetentionPolicy = RetentionPolicy.Dev, IsConfidential = false, + Organizations = [], Users = [], }); lexBoxDbContext.Attach(new Organization { - Id = new Guid("292c80e6-a815-4cd1-9ea2-34bd01274de6"), + Id = TestOrgId, Name = "Test Org", + Projects = [], Members = [ new OrgMember @@ -166,6 +180,35 @@ private async Task SeedUserData(CancellationToken cancellationToken = default) ] }); + lexBoxDbContext.Attach(new Organization + { + Id = SecondTestOrgId, + Name = "Second Test Org", + Projects = [], + Members = + [ + new OrgMember + { + Id = new Guid("03d54e43-ba53-410f-adc2-5ae0bc3cfb21"), Role = OrgRole.Admin, UserId = MangerId, + }, + new OrgMember + { + Id = new Guid("d00c7149-c3b2-448a-93ed-9ba2746d38f0"), Role = OrgRole.User, UserId = EditorId, + }, + new OrgMember + { + Id = new Guid("3035a412-8503-465b-8525-b60aaadd9488"), Role = OrgRole.User, UserId = TestAdminId, + }, + ] + }); + + lexBoxDbContext.Attach(new OrgProjects + { + Id = new Guid("f659eb4c-0289-475d-b44a-095ffddb31c8"), + OrgId = TestOrgId, + ProjectId = Sena3ProjId, + }); + foreach (var entry in lexBoxDbContext.ChangeTracker.Entries()) { var exists = await entry.GetDatabaseValuesAsync(cancellationToken) is not null; @@ -175,35 +218,81 @@ private async Task SeedUserData(CancellationToken cancellationToken = default) await lexBoxDbContext.SaveChangesAsync(cancellationToken); } - private async Task SeedOpenId(CancellationToken cancellationToken = default) + public async Task SeedOAuth(CancellationToken token = default) { if (applicationManager is null) return; - const string clientId = "becf2856-0690-434b-b192-a4032b72067f"; - if (await applicationManager.FindByClientIdAsync(clientId, cancellationToken) is null) + var dbApps = await applicationManager.ListAsync(cancellationToken: token) + .ToDictionaryAwaitAsync(async app => await applicationManager.GetClientIdAsync(app, token) ?? throw new InvalidOperationException("ClientId is null"), token); + var seedApps = OAuthApps; + foreach (var clientId in dbApps.Keys.Union(seedApps.Keys)) { - await applicationManager.CreateAsync(new OpenIddictApplicationDescriptor - { - ClientId = clientId,//must be guid for MSAL - ClientType = OpenIddictConstants.ClientTypes.Public, - ApplicationType = OpenIddictConstants.ApplicationTypes.Web, - DisplayName = "Oidc Debugger", - //explicit requires the user to consent, Implicit does not, External requires an admin to approve, not currently supported - ConsentType = OpenIddictConstants.ConsentTypes.Explicit, - Permissions = - { - OpenIddictConstants.Permissions.Endpoints.Authorization, - OpenIddictConstants.Permissions.Endpoints.Token, - OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, - OpenIddictConstants.Permissions.GrantTypes.RefreshToken, - OpenIddictConstants.Permissions.ResponseTypes.Code, - OpenIddictConstants.Permissions.Scopes.Email, - OpenIddictConstants.Permissions.Scopes.Profile - }, - RedirectUris = { new Uri("https://oidcdebugger.com/debug")} - }, - cancellationToken); + var dbApp = dbApps.GetValueOrDefault(clientId); + var seedApp = seedApps.GetValueOrDefault(clientId); + await ((dbApp, seedApp) switch { + { dbApp: null, seedApp: not null } => CreateApp(seedApp, token), + { dbApp: not null, seedApp: null } => applicationManager.DeleteAsync(dbApp, token), + { dbApp: not null, seedApp: not null } => UpdateApp(dbApp, seedApp, token), + _ => ValueTask.CompletedTask + }); + } + + async ValueTask CreateApp(OpenIddictApplicationDescriptor seedApp, CancellationToken token) + { + await applicationManager.CreateAsync(seedApp, token); + } + + async ValueTask UpdateApp(object dbApp, OpenIddictApplicationDescriptor seedApp, CancellationToken token) + { + await applicationManager.PopulateAsync(dbApp, seedApp, token); + await applicationManager.UpdateAsync(dbApp, token); } } + private Dictionary OAuthApps => Enumerable.ToDictionary( + [ + ..environment.IsProduction() ? [] : DevApps, + new OpenIddictApplicationDescriptor + { + ClientId = "becf2856-0690-434b-b192-a4032b72067f", //must be guid for MSAL + ClientType = OpenIddictConstants.ClientTypes.Public, + ApplicationType = OpenIddictConstants.ApplicationTypes.Native, //native allows the redirect port to be dynamic + DisplayName = "FieldWorks Lite", + ConsentType = OpenIddictConstants.ConsentTypes.Explicit, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, + OpenIddictConstants.Permissions.ResponseTypes.Code, + OpenIddictConstants.Permissions.Scopes.Email, + OpenIddictConstants.Permissions.Scopes.Profile + }, + // port is dynamic due to the nature of the native app + RedirectUris = { new Uri("http://localhost:5173/api/auth/oauth-callback"), new Uri("http://127.0.0.1:5173/api/auth/oauth-callback") } + } + ], a => a.ClientId ?? throw new InvalidOperationException("ClientId is null")); + + private IEnumerable DevApps => [ + new OpenIddictApplicationDescriptor + { + ClientId = "oidc-debugger", + ClientType = OpenIddictConstants.ClientTypes.Public, + ApplicationType = OpenIddictConstants.ApplicationTypes.Web, + DisplayName = "Oidc Debugger", + //explicit requires the user to consent, Implicit does not, External requires an admin to approve, not currently supported + ConsentType = OpenIddictConstants.ConsentTypes.Explicit, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, + OpenIddictConstants.Permissions.ResponseTypes.Code, + OpenIddictConstants.Permissions.Scopes.Profile + }, + RedirectUris = { new Uri("https://oidcdebugger.com/debug") } + }, + ]; public async Task CleanUpSeedData() { diff --git a/backend/LocalWebApp/Auth/AuthConfig.cs b/backend/LocalWebApp/Auth/AuthConfig.cs new file mode 100644 index 000000000..f6c992740 --- /dev/null +++ b/backend/LocalWebApp/Auth/AuthConfig.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace LocalWebApp.Auth; + +public class AuthConfig +{ + [Required] + public required Uri DefaultAuthority { get; set; } + public required string ClientId { get; set; } +} diff --git a/backend/LocalWebApp/Auth/AuthHelpers.cs b/backend/LocalWebApp/Auth/AuthHelpers.cs new file mode 100644 index 000000000..1fa078c68 --- /dev/null +++ b/backend/LocalWebApp/Auth/AuthHelpers.cs @@ -0,0 +1,165 @@ +using System.Net.Http.Headers; +using System.Security.Cryptography; +using LocalWebApp.Routes; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensions.Msal; + +namespace LocalWebApp.Auth; + +/// +/// when injected directly it will use the authority of the current project, to get a different authority use +/// helper class for using MSAL.net +/// docs: https://learn.microsoft.com/en-us/entra/msal/dotnet/acquiring-tokens/overview +/// +public class AuthHelpers +{ + public static IReadOnlyCollection DefaultScopes { get; } = ["profile", "openid"]; + public const string AuthHttpClientName = "AuthHttpClient"; + private readonly HostString _redirectHost; + private readonly bool _isRedirectHostGuess; + private readonly IHttpMessageHandlerFactory _httpMessageHandlerFactory; + private readonly OAuthService _oAuthService; + private readonly UrlContext _urlContext; + private readonly Uri _authority; + private readonly ILogger _logger; + private readonly IPublicClientApplication _application; + AuthenticationResult? _authResult; + + public AuthHelpers(LoggerAdapter loggerAdapter, + IHttpMessageHandlerFactory httpMessageHandlerFactory, + IOptions options, + LinkGenerator linkGenerator, + OAuthService oAuthService, + UrlContext urlContext, + Uri authority, + ILogger logger, + IHostEnvironment hostEnvironment) + { + _httpMessageHandlerFactory = httpMessageHandlerFactory; + _oAuthService = oAuthService; + _urlContext = urlContext; + _authority = authority; + _logger = logger; + (var hostUrl, _isRedirectHostGuess) = urlContext.GetUrl(); + _redirectHost = HostString.FromUriComponent(hostUrl); + var redirectUri = linkGenerator.GetUriByRouteValues(AuthRoutes.CallbackRoute, new RouteValueDictionary(), hostUrl.Scheme, _redirectHost); + var optionsValue = options.Value; + //todo configure token cache as seen here + //https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/wiki/Cross-platform-Token-Cache + _application = PublicClientApplicationBuilder.Create(optionsValue.ClientId) + .WithExperimentalFeatures() + .WithLogging(loggerAdapter, hostEnvironment.IsDevelopment()) + .WithHttpClientFactory(new HttpClientFactoryAdapter(httpMessageHandlerFactory)) + .WithRedirectUri(redirectUri) + .WithOidcAuthority(authority.ToString()) + .Build(); + _ = MsalCacheHelper.CreateAsync(BuildCacheProperties()).ContinueWith( + task => + { + var msalCacheHelper = task.Result; + msalCacheHelper.RegisterCache(_application.UserTokenCache); + }, scheduler: TaskScheduler.Default); + } + + public static readonly KeyValuePair LinuxKeyRingAttr1 = new("Version", "1"); + + public static readonly KeyValuePair LinuxKeyRingAttr2 = new("ProductGroup", "Lexbox"); + + private static StorageCreationProperties BuildCacheProperties() + { + const string KeyChainServiceName = "lexbox_msal_service"; + const string KeyChainAccountName = "lexbox_msal_account"; + + const string LinuxKeyRingSchema = "org.sil.lexbox.tokencache"; + const string LinuxKeyRingCollection = MsalCacheHelper.LinuxKeyRingDefaultCollection; + const string LinuxKeyRingLabel = "MSAL token cache for Lexbox."; + + var propertiesBuilder = new StorageCreationPropertiesBuilder("msal.cache", Directory.GetCurrentDirectory()); +#if DEBUG + propertiesBuilder.WithUnprotectedFile(); +#else + propertiesBuilder.WithLinuxKeyring(LinuxKeyRingSchema, + LinuxKeyRingCollection, + LinuxKeyRingLabel, + LinuxKeyRingAttr1, + LinuxKeyRingAttr2) + .WithMacKeyChain(KeyChainServiceName, KeyChainAccountName); +#endif + return propertiesBuilder.Build(); + } + + public bool IsHostUrlValid() + { + return !_isRedirectHostGuess || _redirectHost == HostString.FromUriComponent(_urlContext.GetUrl().host); + } + + private class HttpClientFactoryAdapter(IHttpMessageHandlerFactory httpMessageHandlerFactory) + : IMsalHttpClientFactory + { + public HttpClient GetHttpClient() + { + return new HttpClient(httpMessageHandlerFactory.CreateHandler(AuthHttpClientName), false); + } + } + + public async Task SignIn(CancellationToken cancellation = default) + { + var authUri = await _oAuthService.SubmitLoginRequest(_application, cancellation); + return authUri.ToString(); + } + + public async Task Logout() + { + _authResult = null; + var accounts = await _application.GetAccountsAsync(); + foreach (var account in accounts) + { + await _application.RemoveAsync(account); + } + } + + private async ValueTask GetAuth() + { + if (DateTimeOffset.UtcNow.AddMinutes(5) < _authResult?.ExpiresOn) + { + return _authResult; + } + + var accounts = await _application.GetAccountsAsync(); + var account = accounts.FirstOrDefault(); + if (account is null) return null; + try + { + _authResult = await _application.AcquireTokenSilent(DefaultScopes, account).ExecuteAsync(); + } + catch (MsalServiceException e) when (e.InnerException is HttpRequestException) + { + _logger.LogWarning(e, "Failed to acquire token silently"); + await _application.RemoveAsync(account);//todo might not be the best way to handle this, maybe it's a transient error? + _authResult = null; + } + return _authResult; + } + + public async Task GetCurrentName() + { + var auth = await GetAuth(); + return auth?.Account.Username; + } + + /// + /// will return null if no auth token is available + /// + public async ValueTask CreateClient() + { + var auth = await GetAuth(); + if (auth is null) return null; + + var handler = _httpMessageHandlerFactory.CreateHandler(AuthHttpClientName); + var client = new HttpClient(handler, false); + client.BaseAddress = _authority; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", auth.AccessToken); + return client; + } +} diff --git a/backend/LocalWebApp/Auth/AuthHelpersFactory.cs b/backend/LocalWebApp/Auth/AuthHelpersFactory.cs new file mode 100644 index 000000000..2776ced84 --- /dev/null +++ b/backend/LocalWebApp/Auth/AuthHelpersFactory.cs @@ -0,0 +1,69 @@ +using System.Collections.Concurrent; +using LcmCrdt; +using Microsoft.Extensions.Options; + +namespace LocalWebApp.Auth; + +public class AuthHelpersFactory( + IServiceProvider provider, + ProjectContext projectContext, + IHttpContextAccessor contextAccessor, + IOptions options) +{ + private readonly ConcurrentDictionary _helpers = new(); + + /// + /// gets the default (as configured in the options) Auth Helper, usually for lexbox.org + /// + public AuthHelpers GetDefault() + { + return GetHelper(options.Value.DefaultAuthority); + } + + private string AuthorityKey(Uri authority) => + authority.GetComponents(UriComponents.HostAndPort, UriFormat.Unescaped); + + /// + /// gets an Auth Helper for the given authority + /// + /// should include scheme, host and port, no path + public AuthHelpers GetHelper(Uri authority) + { + var helper = _helpers.GetOrAdd(AuthorityKey(authority), + static (host, arg) => ActivatorUtilities.CreateInstance(arg.provider, arg.authority), + (authority, provider)); + //an auth helper can get created based on the server host, however in development that will not be the same as the client host + //so we need to recreate it if the host is not valid + if (!helper.IsHostUrlValid()) + { + _helpers.TryRemove(AuthorityKey(authority), out _); + return GetHelper(authority); + } + + return helper; + } + + /// + /// get auth helper for a given project + /// + public AuthHelpers GetHelper(ProjectData project) + { + var originDomain = project.OriginDomain; + if (string.IsNullOrEmpty(originDomain)) throw new InvalidOperationException("No origin domain in project data"); + return GetHelper(new Uri(originDomain)); + } + + /// + /// get the auth helper for the current project, this method is used when trying to inject an AuthHelper into a service + /// + /// when not in the context of a project (typically requests include the project name in the path) + public AuthHelpers GetCurrentHelper() + { + if (projectContext.Project is null) + throw new InvalidOperationException("No current project, probably not in a request context"); + var currentProjectService = + contextAccessor.HttpContext?.RequestServices.GetRequiredService(); + if (currentProjectService is null) throw new InvalidOperationException("No current project service"); + return GetHelper(currentProjectService.ProjectData); + } +} diff --git a/backend/LocalWebApp/Auth/LoggerAdapter.cs b/backend/LocalWebApp/Auth/LoggerAdapter.cs new file mode 100644 index 000000000..ea75bdd5c --- /dev/null +++ b/backend/LocalWebApp/Auth/LoggerAdapter.cs @@ -0,0 +1,29 @@ +using Microsoft.IdentityModel.Abstractions; + +namespace LocalWebApp.Auth; + +public class LoggerAdapter(ILogger logger): IIdentityLogger +{ + private LogLevel Convert(EventLogLevel eventLogLevel) + { + return eventLogLevel switch + { + EventLogLevel.LogAlways => LogLevel.Trace, + EventLogLevel.Critical => LogLevel.Critical, + EventLogLevel.Error => LogLevel.Error, + EventLogLevel.Warning => LogLevel.Warning, + EventLogLevel.Informational => LogLevel.Information, + EventLogLevel.Verbose => LogLevel.Debug, + _ => throw new ArgumentOutOfRangeException(nameof(eventLogLevel), eventLogLevel, null) + }; + } + public bool IsEnabled(EventLogLevel eventLogLevel) + { + return logger.IsEnabled(Convert(eventLogLevel)); + } + + public void Log(LogEntry entry) + { + logger.Log(Convert(entry.EventLogLevel), entry.Message); + } +} diff --git a/backend/LocalWebApp/Auth/OAuthService.cs b/backend/LocalWebApp/Auth/OAuthService.cs new file mode 100644 index 000000000..092d197bc --- /dev/null +++ b/backend/LocalWebApp/Auth/OAuthService.cs @@ -0,0 +1,112 @@ +using System.Threading.Channels; +using System.Web; +using LocalWebApp.Utils; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensibility; + +namespace LocalWebApp.Auth; + +//this class is commented with a number of step comments, these are the steps in the OAuth flow +//if a step comes before a method that means it awaits that call, if it comes after that means it resumes after the above await +public class OAuthService(ILogger logger, IHostApplicationLifetime applicationLifetime) : BackgroundService +{ + public async Task SubmitLoginRequest(IPublicClientApplication application, CancellationToken cancellation) + { + var request = new OAuthLoginRequest(application); + if (!_requestChannel.Writer.TryWrite(request)) + { + throw new InvalidOperationException("Only one request at a time"); + } + //step 1 + var uri = await request.GetAuthUri(applicationLifetime.ApplicationStopping.Merge(cancellation)); + //step 4 + if (request.State is null) throw new InvalidOperationException("State is null"); + _oAuthLoginRequests[request.State] = request; + return uri; + } + + public async Task FinishLoginRequest(Uri uri, CancellationToken cancellation = default) + { + var queryString = HttpUtility.ParseQueryString(uri.Query); + var state = queryString.Get("state") ?? throw new InvalidOperationException("State is null"); + if (!_oAuthLoginRequests.TryGetValue(state, out var request)) + throw new InvalidOperationException("Invalid state"); + //step 5 + request.SetReturnUri(uri); + return await request.GetAuthenticationResult(applicationLifetime.ApplicationStopping.Merge(cancellation)); + //step 8 + } + + private readonly Dictionary _oAuthLoginRequests = new(); + private readonly Channel _requestChannel = Channel.CreateBounded(1);//only one request at a time + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await foreach (var loginRequest in _requestChannel.Reader.ReadAllAsync(stoppingToken)) + { + //this sits here and waits for AcquireAuthorizationCodeAsync to finish, meanwhile the uri passed in to that method is sent back to the caller of SubmitLoginRequest + //which then redirects the browser to that uri, once it's done it's sent back and calls FinishLoginRequest, which sends it's uri to OAuthLoginRequest + //which causes AcquireAuthorizationCodeAsync to return + + try + { + //todo we can get stuck here if the user doesn't complete the login, this basically bricks the login at the moment. We need a timeout or something + //step 2 + var result = await loginRequest.Application.AcquireTokenInteractive(AuthHelpers.DefaultScopes) + .WithCustomWebUi(loginRequest) + .ExecuteAsync(stoppingToken); + //step 7, causes step 8 to resume + loginRequest.SetAuthenticationResult(result); + } + catch (Exception e) + { + logger.LogError(e, "Error getting token"); + loginRequest.SetException(e); + } + + if (loginRequest.State is not null) + _oAuthLoginRequests.Remove(loginRequest.State); + } + } +} + +/// +/// this is a bit of a hack. the MSAL library expects to be running in a native app which opens a browser and waits for a response URL to come back +/// instead we have to do this so we can use the currently open browser, redirect it to the auth url passed in here and then once it's done and the callback comes to our server, +/// send that call to here so that MSAL can pull out the access token +/// +public class OAuthLoginRequest(IPublicClientApplication app) : ICustomWebUi +{ + public IPublicClientApplication Application { get; } = app; + public string? State { get; private set; } + private readonly TaskCompletionSource _authUriTcs = new(); + private readonly TaskCompletionSource _returnUriTcs = new(); + private readonly TaskCompletionSource _resultTcs = new(); + + public async Task AcquireAuthorizationCodeAsync(Uri authorizationUri, + Uri redirectUri, + CancellationToken cancellationToken) + { + cancellationToken.Register(_resultTcs.SetCanceled); + State = HttpUtility.ParseQueryString(authorizationUri.Query).Get("state"); + //triggers step 1 to finish awaiting + _authUriTcs.SetResult(authorizationUri); + + //step 3 + return await _returnUriTcs.Task.WaitAsync(cancellationToken); + //step 6 + } + + public Task GetAuthUri(CancellationToken cancellation) => _authUriTcs.Task.WaitAsync(cancellation); + public void SetReturnUri(Uri uri) => _returnUriTcs.SetResult(uri); + public void SetAuthenticationResult(AuthenticationResult result) => _resultTcs.SetResult(result); + public void SetException(Exception e) + { + if (_authUriTcs.Task.IsCompleted) + _resultTcs.SetException(e); + else + _authUriTcs.SetException(e); + } + + public Task GetAuthenticationResult(CancellationToken cancellation) => _resultTcs.Task.WaitAsync(cancellation); +} diff --git a/backend/LocalWebApp/BackgroundSyncService.cs b/backend/LocalWebApp/BackgroundSyncService.cs index c8f991340..4205b8fa8 100644 --- a/backend/LocalWebApp/BackgroundSyncService.cs +++ b/backend/LocalWebApp/BackgroundSyncService.cs @@ -1,6 +1,8 @@ using System.Threading.Channels; using Crdt; using LcmCrdt; +using LocalWebApp.Auth; +using Microsoft.Extensions.Options; using MiniLcm; namespace LocalWebApp; @@ -8,6 +10,8 @@ namespace LocalWebApp; public class BackgroundSyncService( IServiceProvider serviceProvider, ProjectsService projectsService, + IHostApplicationLifetime applicationLifetime, + IOptionsoptions, ProjectContext projectContext) : BackgroundService { private readonly Channel _syncResultsChannel = Channel.CreateUnbounded(); @@ -18,27 +22,23 @@ public void TriggerSync() throw new InvalidOperationException("No project selected")); } + private Task StartedAsync() + { + var tcs = new TaskCompletionSource(); + applicationLifetime.ApplicationStarted.Register(() => tcs.SetResult()); + return tcs.Task; + } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + //need to wait until application is started, otherwise Server urls will be unknown which prevents creating downstream services + await StartedAsync(); var crdtProjects = await projectsService.ListProjects(); foreach (var crdtProject in crdtProjects) { await SyncProject(crdtProject); } - if (!projectsService.ProjectExists("sena-3")) - { - await projectsService.CreateProject("sena-3", - new Guid("0ebc5976-058d-4447-aaa7-297f8569f968"), //same as sena 3 project id in SeedDatabase - "http://localhost:5158", - async (provider, project) => - { - var (_, _, isSynced) = await SyncProject(project); - //seed will only create if missing, so seed anyway since the project should exist on the server always - await SeedDb(provider.GetRequiredService()); - }); - } - await foreach (var project in _syncResultsChannel.Reader.ReadAllAsync(stoppingToken)) { //todo, this might not be required, but I can't remember why I added it @@ -49,65 +49,8 @@ await projectsService.CreateProject("sena-3", private async Task SyncProject(CrdtProject crdtProject) { - using var serviceScope = projectsService.CreateProjectScope(crdtProject); + await using var serviceScope = projectsService.CreateProjectScope(crdtProject); var syncService = serviceScope.ServiceProvider.GetRequiredService(); return await syncService.ExecuteSync(); } - - private async Task SeedDb(ILexboxApi lexboxApi) - { - //id is fixed to prevent duplicates - var id = new Guid("c7328f18-118a-4f83-9d88-c408778b7f63"); - if (await lexboxApi.GetEntry(id) is null) - { - await lexboxApi.CreateEntry(new() - { - Id = id, - LexemeForm = { Values = { { "en", "Kevin" } } }, - Note = { Values = { { "en", "this is a test note from Kevin" } } }, - CitationForm = { Values = { { "en", "Kevin" } } }, - LiteralMeaning = { Values = { { "en", "Kevin" } } }, - Senses = - [ - new() - { - Gloss = { Values = { { "en", "Kevin" } } }, - Definition = { Values = { { "en", "Kevin" } } }, - SemanticDomain = ["Person"], - ExampleSentences = - [ - new() { Sentence = { Values = { { "en", "Kevin is a good guy" } } } } - ] - } - ] - }); - } - - var writingSystems = await lexboxApi.GetWritingSystems(); - if (writingSystems.Analysis.Length == 0) - { - await lexboxApi.CreateWritingSystem(WritingSystemType.Analysis, - new() - { - Id = "en", - Name = "English", - Abbreviation = "en", - Font = "Arial", - Exemplars = WritingSystem.LatinExemplars - }); - } - - if (writingSystems.Vernacular.Length == 0) - { - await lexboxApi.CreateWritingSystem(WritingSystemType.Vernacular, - new() - { - Id = "en", - Name = "English", - Abbreviation = "en", - Font = "Arial", - Exemplars = WritingSystem.LatinExemplars - }); - } - } } diff --git a/backend/LocalWebApp/CrdtHttpSyncService.cs b/backend/LocalWebApp/CrdtHttpSyncService.cs index 2556939e0..649790f4c 100644 --- a/backend/LocalWebApp/CrdtHttpSyncService.cs +++ b/backend/LocalWebApp/CrdtHttpSyncService.cs @@ -2,16 +2,18 @@ using Crdt; using Crdt.Db; using LcmCrdt; +using LocalWebApp.Auth; using Refit; namespace LocalWebApp; -public class CrdtHttpSyncService(IHttpClientFactory clientFactory, ILogger logger, RefitSettings refitSettings) +public class CrdtHttpSyncService(AuthHelpersFactory authHelpersFactory, ILogger logger, RefitSettings refitSettings) { //todo replace with a IMemoryCache check private bool? _isHealthy; private DateTimeOffset _lastHealthCheck = DateTimeOffset.MinValue; + //todo pull this out into a service wrapped around auth helpers so that any service making requests can use it public async ValueTask ShouldSync(ISyncHttp syncHttp) { if (_isHealthy is not null && _lastHealthCheck + TimeSpan.FromMinutes(30) > DateTimeOffset.UtcNow) @@ -40,18 +42,22 @@ public async ValueTask ShouldSync(ISyncHttp syncHttp) return _isHealthy.Value; } - public ISyncHttp CreateSyncHttp(string originDomain) + public async ValueTask CreateProjectSyncable(ProjectData project) { - var uri = new Uri(originDomain); - var httpClient = clientFactory.CreateClient(uri.Host); - httpClient.BaseAddress = uri; - return RestService.For(httpClient, refitSettings); - } + if (string.IsNullOrEmpty(project.OriginDomain)) + { + logger.LogWarning("Project {ProjectName} has no origin domain, unable to create http sync client", project.Name); + return NullSyncable.Instance; + } - public ISyncable CreateProjectSyncable(ProjectData project) - { - if (string.IsNullOrEmpty(project.OriginDomain)) return NullSyncable.Instance; - return new CrdtProjectSync(CreateSyncHttp(project.OriginDomain), project.Id, project.OriginDomain, this); + var client = await authHelpersFactory.GetHelper(project).CreateClient(); + if (client is null) + { + logger.LogWarning("Unable to create http client to sync project, user is not authenticated to {OriginDomain}", project.OriginDomain); + return NullSyncable.Instance; + } + + return new CrdtProjectSync(RestService.For(client, refitSettings), project.Id, project.OriginDomain, this); } } @@ -94,15 +100,15 @@ Task ISyncable.SyncMany(ISyncable[] remotes) public interface ISyncHttp { - [Get("/api/healthz")] + [Get("/api/AuthTesting/requires-auth")] Task HealthCheck(); - [Post("/api/sync/{id}/add")] + [Post("/api/crdt/{id}/add")] internal Task AddRange(Guid id, IEnumerable commits); - [Get("/api/sync/{id}/get")] + [Get("/api/crdt/{id}/get")] internal Task GetSyncState(Guid id); - [Post("/api/sync/{id}/changes")] + [Post("/api/crdt/{id}/changes")] internal Task> GetChanges(Guid id, SyncState otherHeads); } diff --git a/backend/LocalWebApp/HttpHelpers.cs b/backend/LocalWebApp/HttpHelpers.cs index 4bc327df3..73774c7e0 100644 --- a/backend/LocalWebApp/HttpHelpers.cs +++ b/backend/LocalWebApp/HttpHelpers.cs @@ -1,10 +1,18 @@ -namespace LocalWebApp; +using LocalWebApp.Hubs; + +namespace LocalWebApp; public static class HttpHelpers { public static string? GetProjectName(this HttpContext? context) { - var name = context?.Request.RouteValues.GetValueOrDefault(LexboxApiHub.ProjectRouteKey, null)?.ToString(); + var name = context?.Request.RouteValues.GetValueOrDefault(CrdtMiniLcmApiHub.ProjectRouteKey, null)?.ToString(); + return string.IsNullOrWhiteSpace(name) ? null : name; + } + + public static string? GetFwDataName(this HttpContext? context) + { + var name = context?.Request.RouteValues.GetValueOrDefault(FwDataMiniLcmHub.ProjectRouteKey, null)?.ToString(); return string.IsNullOrWhiteSpace(name) ? null : name; } } diff --git a/backend/LocalWebApp/LexboxApiHub.cs b/backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs similarity index 98% rename from backend/LocalWebApp/LexboxApiHub.cs rename to backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs index 502dd04fd..72c600fdb 100644 --- a/backend/LocalWebApp/LexboxApiHub.cs +++ b/backend/LocalWebApp/Hubs/CrdtMiniLcmApiHub.cs @@ -4,14 +4,14 @@ using MiniLcm; using SystemTextJsonPatch; -namespace LocalWebApp; +namespace LocalWebApp.Hubs; public interface ILexboxClient { Task OnEntryUpdated(Entry entry); } -public class LexboxApiHub( +public class CrdtMiniLcmApiHub( ILexboxApi lexboxApi, IOptions jsonOptions, BackgroundSyncService backgroundSyncService, diff --git a/backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs b/backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs new file mode 100644 index 000000000..3c5527320 --- /dev/null +++ b/backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs @@ -0,0 +1,118 @@ +using FwDataMiniLcmBridge; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Options; +using MiniLcm; +using SystemTextJsonPatch; + +namespace LocalWebApp.Hubs; + +public class FwDataMiniLcmHub([FromKeyedServices(FwDataBridgeKernel.FwDataApiKey)] ILexboxApi lexboxApi) : Hub +{ + public const string ProjectRouteKey = "fwdata"; + public override async Task OnConnectedAsync() + { + } + + public async Task GetWritingSystems() + { + return await lexboxApi.GetWritingSystems(); + } + + public async Task CreateWritingSystem(WritingSystemType type, WritingSystem writingSystem) + { + var newWritingSystem = await lexboxApi.CreateWritingSystem(type, writingSystem); + return newWritingSystem; + } + + public async Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, JsonPatchDocument update) + { + var writingSystem = await lexboxApi.UpdateWritingSystem(id, type, new JsonPatchUpdateInput(update)); + return writingSystem; + } + + public IAsyncEnumerable GetEntriesForExemplar(string exemplar, QueryOptions? options = null) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable GetEntries(QueryOptions? options = null) + { + return lexboxApi.GetEntries(options); + } + + public IAsyncEnumerable SearchEntries(string query, QueryOptions? options = null) + { + return lexboxApi.SearchEntries(query, options); + } + + public async Task GetEntry(Guid id) + { + return await lexboxApi.GetEntry(id); + } + + public async Task CreateEntry(Entry entry) + { + var newEntry = await lexboxApi.CreateEntry(entry); + await NotifyEntryUpdated(newEntry); + return newEntry; + } + + public async Task UpdateEntry(Guid id, JsonPatchDocument update) + { + var entry = await lexboxApi.UpdateEntry(id, new JsonPatchUpdateInput(update)); + await NotifyEntryUpdated(entry); + return entry; + } + + public async Task DeleteEntry(Guid id) + { + await lexboxApi.DeleteEntry(id); + } + + public async Task CreateSense(Guid entryId, Sense sense) + { + var createdSense = await lexboxApi.CreateSense(entryId, sense); + return createdSense; + } + + public async Task UpdateSense(Guid entryId, Guid senseId, JsonPatchDocument update) + { + var sense = await lexboxApi.UpdateSense(entryId, senseId, new JsonPatchUpdateInput(update)); + return sense; + } + + public async Task DeleteSense(Guid entryId, Guid senseId) + { + await lexboxApi.DeleteSense(entryId, senseId); + } + + public async Task CreateExampleSentence(Guid entryId, + Guid senseId, + ExampleSentence exampleSentence) + { + var createdSentence = await lexboxApi.CreateExampleSentence(entryId, senseId, exampleSentence); + return createdSentence; + } + + public async Task UpdateExampleSentence(Guid entryId, + Guid senseId, + Guid exampleSentenceId, + JsonPatchDocument update) + { + var sentence = await lexboxApi.UpdateExampleSentence(entryId, + senseId, + exampleSentenceId, + new JsonPatchUpdateInput(update)); + return sentence; + } + + public async Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId) + { + await lexboxApi.DeleteExampleSentence(entryId, senseId, exampleSentenceId); + } + + private async Task NotifyEntryUpdated(Entry entry) + { + await Clients.Others.OnEntryUpdated(entry); + } +} diff --git a/backend/LocalWebApp/LocalAppKernel.cs b/backend/LocalWebApp/LocalAppKernel.cs index b35ab86f9..421def69b 100644 --- a/backend/LocalWebApp/LocalAppKernel.cs +++ b/backend/LocalWebApp/LocalAppKernel.cs @@ -1,6 +1,9 @@ using System.Text.Json; using Crdt; +using FwDataMiniLcmBridge; using LcmCrdt; +using LocalWebApp.Services; +using LocalWebApp.Auth; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Options; @@ -10,13 +13,20 @@ namespace LocalWebApp; public static class LocalAppKernel { - public static void AddLocalAppServices(this IServiceCollection services) + public static IServiceCollection AddLocalAppServices(this IServiceCollection services, IHostEnvironment environment) { - services.AddSingleton(); services.AddHttpContextAccessor(); + services.AddHttpClient(); + services.AddAuthHelpers(environment); + services.AddSingleton(); services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(s => s.GetRequiredService()); services.AddLcmCrdtClient(); + services.AddFwDataBridge(); + services.AddOptions().PostConfigure>((jsonOptions, crdtConfig) => { jsonOptions.SerializerOptions.TypeInfoResolver = crdtConfig.Value.MakeJsonTypeResolver(); @@ -37,5 +47,30 @@ public static void AddLocalAppServices(this IServiceCollection services) }) }); services.AddSingleton(); + return services; + } + + private static void AddAuthHelpers(this IServiceCollection services, IHostEnvironment environment) + { + services.AddSingleton(); + services.AddTransient(sp => sp.GetRequiredService().GetCurrentHelper()); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddOptionsWithValidateOnStart().BindConfiguration("Auth").ValidateDataAnnotations(); + services.AddSingleton(); + var httpClientBuilder = services.AddHttpClient(AuthHelpers.AuthHttpClientName); + if (environment.IsDevelopment()) + { + // Allow self-signed certificates in development + httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + ClientCertificateOptions = ClientCertificateOption.Manual, + ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true + }; + }); + } + } } diff --git a/backend/LocalWebApp/LocalWebApp.csproj b/backend/LocalWebApp/LocalWebApp.csproj index a9967e008..f103c49ab 100644 --- a/backend/LocalWebApp/LocalWebApp.csproj +++ b/backend/LocalWebApp/LocalWebApp.csproj @@ -16,6 +16,7 @@ + @@ -28,6 +29,7 @@ + diff --git a/backend/LocalWebApp/LocalWebApp.http b/backend/LocalWebApp/LocalWebApp.http index 09e2099db..1e49aa754 100644 --- a/backend/LocalWebApp/LocalWebApp.http +++ b/backend/LocalWebApp/LocalWebApp.http @@ -1,6 +1,7 @@ -@LocalWebApp_HostAddress = http://localhost:5137 +@LocalWebApp_HostAddress = http://localhost:5173 -GET {{LocalWebApp_HostAddress}}/weatherforecast/ +GET {{LocalWebApp_HostAddress}}/api/auth/login/default Accept: application/json + ### diff --git a/backend/LocalWebApp/Program.cs b/backend/LocalWebApp/Program.cs index d44281031..2f88cf432 100644 --- a/backend/LocalWebApp/Program.cs +++ b/backend/LocalWebApp/Program.cs @@ -1,14 +1,28 @@ +using FwDataMiniLcmBridge; +using FwDataMiniLcmBridge.LcmUtils; using LcmCrdt; using LocalWebApp; +using LocalWebApp.Hubs; +using LocalWebApp.Auth; using LocalWebApp.Routes; +using LocalWebApp.Utils; using Microsoft.AspNetCore.StaticFiles.Infrastructure; using Microsoft.Extensions.FileProviders; var builder = WebApplication.CreateBuilder(args); if (!builder.Environment.IsDevelopment()) builder.WebHost.UseUrls("http://127.0.0.1:0"); +if (builder.Environment.IsDevelopment()) +{ + //do this early so we catch bugs on startup + ProjectLoader.Init(); +} +builder.ConfigureDev(config => config.DefaultAuthority = new("https://lexbox.dev.languagetechnology.org")); +//for now prod builds will also use lt dev until we deploy oauth to prod +builder.ConfigureProd(config => config.DefaultAuthority = new("https://lexbox.dev.languagetechnology.org")); +builder.Services.Configure(c => c.ClientId = "becf2856-0690-434b-b192-a4032b72067f"); -builder.Services.AddLocalAppServices(); +builder.Services.AddLocalAppServices(builder.Environment); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddSignalR().AddJsonProtocol(); @@ -21,9 +35,11 @@ app.UseSwaggerUI(); } +//configure dotnet to serve static files from the embedded resources var sharedOptions = new SharedOptions() { FileProvider = new ManifestEmbeddedFileProvider(typeof(Program).Assembly) }; app.UseDefaultFiles(new DefaultFilesOptions(sharedOptions)); app.UseStaticFiles(new StaticFileOptions(sharedOptions)); + app.Use(async (context, next) => { var projectName = context.GetProjectName(); @@ -32,14 +48,26 @@ var projectsService = context.RequestServices.GetRequiredService(); projectsService.SetProjectScope(projectsService.GetProject(projectName) ?? throw new InvalidOperationException($"Project {projectName} not found")); + await context.RequestServices.GetRequiredService().PopulateProjectDataCache(); + } + + var fwData = context.GetFwDataName(); + if (!string.IsNullOrWhiteSpace(fwData)) + { + var fwDataProjectContext = context.RequestServices.GetRequiredService(); + fwDataProjectContext.Project = FieldWorksProjectList.GetProject(fwData) ?? throw new InvalidOperationException($"FwData {fwData} not found"); } await next(context); }); -app.MapHub($"/api/hub/{{{LexboxApiHub.ProjectRouteKey}}}/lexbox"); +app.MapHub($"/api/hub/{{{CrdtMiniLcmApiHub.ProjectRouteKey}}}/lexbox"); +app.MapHub($"/api/hub/{{{FwDataMiniLcmHub.ProjectRouteKey}}}/fwdata"); app.MapHistoryRoutes(); app.MapActivities(); app.MapProjectRoutes(); +app.MapTest(); +app.MapImport(); +app.MapAuthRoutes(); await using (app) { diff --git a/backend/LocalWebApp/Routes/ActivityRoutes.cs b/backend/LocalWebApp/Routes/ActivityRoutes.cs index 9c3f02439..f1526a30e 100644 --- a/backend/LocalWebApp/Routes/ActivityRoutes.cs +++ b/backend/LocalWebApp/Routes/ActivityRoutes.cs @@ -1,6 +1,7 @@ using Crdt.Changes; using Crdt.Core; using Crdt.Db; +using LocalWebApp.Hubs; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; @@ -14,7 +15,7 @@ public static IEndpointConventionBuilder MapActivities(this WebApplication app) { operation.Parameters.Add(new OpenApiParameter() { - Name = LexboxApiHub.ProjectRouteKey, + Name = CrdtMiniLcmApiHub.ProjectRouteKey, In = ParameterLocation.Path, Required = true }); diff --git a/backend/LocalWebApp/Routes/AuthRoutes.cs b/backend/LocalWebApp/Routes/AuthRoutes.cs new file mode 100644 index 000000000..5f98d80bb --- /dev/null +++ b/backend/LocalWebApp/Routes/AuthRoutes.cs @@ -0,0 +1,31 @@ +using System.Security.AccessControl; +using System.Web; +using LocalWebApp.Auth; + +namespace LocalWebApp.Routes; + +public static class AuthRoutes +{ + public const string CallbackRoute = "AuthRoutes_Callback"; + public static IEndpointConventionBuilder MapAuthRoutes(this WebApplication app) + { + var group = app.MapGroup("/api/auth").WithOpenApi(); + group.MapGet("/login/default", async (AuthHelpersFactory factory) => Results.Redirect(await factory.GetDefault().SignIn())); + group.MapGet("/oauth-callback", + async (OAuthService oAuthService, HttpContext context) => + { + var uriBuilder = new UriBuilder(context.Request.Scheme, context.Request.Host.Host, context.Request.Host.Port ?? 80, context.Request.Path); + uriBuilder.Query = context.Request.QueryString.ToUriComponent(); + + await oAuthService.FinishLoginRequest(uriBuilder.Uri); + return Results.Redirect("/"); + }).WithName(CallbackRoute); + group.MapGet("/me", async (AuthHelpersFactory factory) => new { name = await factory.GetDefault().GetCurrentName() }); + group.MapGet("/logout/default", async (AuthHelpersFactory factory) => + { + await factory.GetDefault().Logout(); + return Results.Redirect("/"); + }); + return group; + } +} diff --git a/backend/LocalWebApp/Routes/HistoryRoutes.cs b/backend/LocalWebApp/Routes/HistoryRoutes.cs index e59afea59..09f6236b9 100644 --- a/backend/LocalWebApp/Routes/HistoryRoutes.cs +++ b/backend/LocalWebApp/Routes/HistoryRoutes.cs @@ -5,6 +5,7 @@ using Crdt.Entities; using LinqToDB; using LinqToDB.EntityFrameworkCore; +using LocalWebApp.Hubs; using Microsoft.OpenApi.Models; namespace LocalWebApp.Routes; @@ -17,7 +18,7 @@ public static IEndpointConventionBuilder MapHistoryRoutes(this WebApplication ap { operation.Parameters.Add(new OpenApiParameter() { - Name = LexboxApiHub.ProjectRouteKey, In = ParameterLocation.Path, Required = true + Name = CrdtMiniLcmApiHub.ProjectRouteKey, In = ParameterLocation.Path, Required = true }); return operation; }); diff --git a/backend/LocalWebApp/Routes/ImportRoutes.cs b/backend/LocalWebApp/Routes/ImportRoutes.cs new file mode 100644 index 000000000..7d43edf4d --- /dev/null +++ b/backend/LocalWebApp/Routes/ImportRoutes.cs @@ -0,0 +1,17 @@ + using Crdt.Db; + using LocalWebApp.Services; + using Microsoft.OpenApi.Models; +using MiniLcm; + +namespace LocalWebApp.Routes; + +public static class ImportRoutes +{ + public static IEndpointConventionBuilder MapImport(this WebApplication app) + { + var group = app.MapGroup("/api/import"); + group.MapPost("/fwdata/{fwDataProjectName}", + async (string fwDataProjectName, ImportFwdataService importService) => await importService.Import(fwDataProjectName)); + return group; + } +} diff --git a/backend/LocalWebApp/Routes/ProjectRoutes.cs b/backend/LocalWebApp/Routes/ProjectRoutes.cs index 014ad271d..00630fc58 100644 --- a/backend/LocalWebApp/Routes/ProjectRoutes.cs +++ b/backend/LocalWebApp/Routes/ProjectRoutes.cs @@ -1,16 +1,51 @@ using System.Text.RegularExpressions; +using FwDataMiniLcmBridge; using LcmCrdt; +using LocalWebApp.Auth; +using LocalWebApp.Hubs; +using LocalWebApp.Services; +using Microsoft.Extensions.Options; using MiniLcm; namespace LocalWebApp.Routes; -public static class ProjectRoutes +public static partial class ProjectRoutes { public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication app) { var group = app.MapGroup("/api").WithOpenApi(); - group.MapGet("/projects", (ProjectsService projectService) => projectService.ListProjects()); - Regex alphaNumericRegex = new Regex("^[a-zA-Z0-9]*$"); + group.MapGet("/projects", + async (ProjectsService projectService, LexboxProjectService lexboxProjectService) => + { + var crdtProjects = await projectService.ListProjects(); + var projects = crdtProjects.ToDictionary(p => p.Name, p => new ProjectModel(p.Name, true, false)); + //basically populate projects and indicate if they are lexbox or fwdata + foreach (var p in FieldWorksProjectList.EnumerateProjects()) + { + if (projects.TryGetValue(p.Name, out var project)) + { + projects[p.Name] = project with { Fwdata = true }; + } + else + { + projects.Add(p.Name, new ProjectModel(p.Name, false, true)); + } + } + //todo split this out into it's own request so we can return other project types right away + foreach (var lexboxProject in await lexboxProjectService.GetLexboxProjects()) + { + if (projects.TryGetValue(lexboxProject.Name, out var project)) + { + projects[lexboxProject.Name] = project with { Lexbox = true }; + } + else + { + projects.Add(lexboxProject.Name, new ProjectModel(lexboxProject.Name, false, false, true)); + } + } + + return projects.Values; + }); group.MapPost("/project", async (ProjectsService projectService, string name) => { @@ -18,32 +53,68 @@ public static IEndpointConventionBuilder MapProjectRoutes(this WebApplication ap return Results.BadRequest("Project name is required"); if (projectService.ProjectExists(name)) return Results.BadRequest("Project already exists"); - if (!alphaNumericRegex.IsMatch(name)) - return Results.BadRequest("Project name must be alphanumeric"); + if (!ProjectName().IsMatch(name)) + return Results.BadRequest("Only letters, numbers, '-' and '_' are allowed"); await projectService.CreateProject(name, afterCreate: AfterCreate); return TypedResults.Ok(); }); + group.MapPost($"/upload/crdt/{{{CrdtMiniLcmApiHub.ProjectRouteKey}}}", + async (LexboxProjectService lexboxProjectService, + SyncService syncService, + IOptions options, + CurrentProjectService currentProjectService) => + { + //todo let the user pick a project to upload to instead of matching the name with the project code. + var foundProjectGuid = + await lexboxProjectService.GetLexboxProjectId(currentProjectService.ProjectData.Name); + if (foundProjectGuid is null) + return Results.BadRequest( + $"Project code {currentProjectService.ProjectData.Name} not found on lexbox"); + await currentProjectService.SetProjectSyncOrigin(options.Value.DefaultAuthority, foundProjectGuid); + await syncService.ExecuteSync(); + return TypedResults.Ok(); + }); + group.MapPost("/download/crdt/{newProjectName}", + async (LexboxProjectService lexboxProjectService, + IOptions options, + ProjectsService projectService, + string newProjectName + ) => + { + if (!ProjectName().IsMatch(newProjectName)) + return Results.BadRequest("Project name is invalid"); + var foundProjectGuid = await lexboxProjectService.GetLexboxProjectId(newProjectName); + if (foundProjectGuid is null) + return Results.BadRequest($"Project code {newProjectName} not found on lexbox"); + await projectService.CreateProject(newProjectName, foundProjectGuid.Value, options.Value.DefaultAuthority, + async (provider, project) => + { + await provider.GetRequiredService().ExecuteSync(); + }); + return TypedResults.Ok(); + }); return group; } + public record ProjectModel(string Name, bool Crdt, bool Fwdata, bool Lexbox = false); + private static async Task AfterCreate(IServiceProvider provider, CrdtProject project) { var lexboxApi = provider.GetRequiredService(); await lexboxApi.CreateEntry(new() { Id = Guid.NewGuid(), - LexemeForm = { Values = { { "en", "Kevin" } } }, - Note = { Values = { { "en", "this is a test note from Kevin" } } }, - CitationForm = { Values = { { "en", "Kevin" } } }, - LiteralMeaning = { Values = { { "en", "Kevin" } } }, + LexemeForm = { Values = { { "en", "Apple" } } }, + CitationForm = { Values = { { "en", "Apple" } } }, + LiteralMeaning = { Values = { { "en", "Fruit" } } }, Senses = [ new() { - Gloss = { Values = { { "en", "Kevin" } } }, - Definition = { Values = { { "en", "Kevin" } } }, - SemanticDomain = ["Person"], - ExampleSentences = [new() { Sentence = { Values = { { "en", "Kevin is a good guy" } } } }] + Gloss = { Values = { { "en", "Fruit" } } }, + Definition = { Values = { { "en", "fruit with red, yellow, or green skin with a sweet or tart crispy white flesh" } } }, + SemanticDomain = ["Fruit"], + ExampleSentences = [new() { Sentence = { Values = { { "en", "We ate an apple" } } } }] } ] }); @@ -68,4 +139,7 @@ await lexboxApi.CreateWritingSystem(WritingSystemType.Analysis, Exemplars = WritingSystem.LatinExemplars }); } + + [GeneratedRegex("^[a-zA-Z0-9][a-zA-Z0-9-_]+$")] + private static partial Regex ProjectName(); } diff --git a/backend/LocalWebApp/Routes/TestRoutes.cs b/backend/LocalWebApp/Routes/TestRoutes.cs new file mode 100644 index 000000000..70383654c --- /dev/null +++ b/backend/LocalWebApp/Routes/TestRoutes.cs @@ -0,0 +1,33 @@ +using Crdt.Core; +using Crdt.Db; +using LocalWebApp.Hubs; +using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; +using MiniLcm; +using Entry = LcmCrdt.Objects.Entry; + +namespace LocalWebApp.Routes; + +public static class TestRoutes +{ + public static IEndpointConventionBuilder MapTest(this WebApplication app) + { + var group = app.MapGroup("/api/test/{project}").WithOpenApi(operation => + { + operation.Parameters.Add(new OpenApiParameter() + { + Name = CrdtMiniLcmApiHub.ProjectRouteKey, + In = ParameterLocation.Path, + Required = true + }); + return operation; + }); + group.MapGet("/entries", + (CrdtDbContext dbContext, ILexboxApi api) => + { + return api.GetEntries(); + return dbContext.Set().Take(1000).AsAsyncEnumerable(); + }); + return group; + } +} diff --git a/backend/LocalWebApp/Services/ImportFwdataService.cs b/backend/LocalWebApp/Services/ImportFwdataService.cs new file mode 100644 index 000000000..5b9cb3b0a --- /dev/null +++ b/backend/LocalWebApp/Services/ImportFwdataService.cs @@ -0,0 +1,59 @@ +using FwDataMiniLcmBridge; +using FwDataMiniLcmBridge.Api; +using FwDataMiniLcmBridge.LcmUtils; +using LcmCrdt; +using MiniLcm; + +namespace LocalWebApp.Services; + +public class ImportFwdataService(ProjectsService projectsService, ILogger logger, FwDataFactory fwDataFactory) +{ + public async Task Import(string projectName) + { + var fwDataProject = FieldWorksProjectList.GetProject(projectName); + if (fwDataProject is null) + { + throw new InvalidOperationException($"Project {projectName} not found."); + } + using var fwDataApi = fwDataFactory.GetFwDataMiniLcmApi(fwDataProject, false); + var project = await projectsService.CreateProject(fwDataProject.Name, + afterCreate: async (provider, project) => + { + var crdtApi = provider.GetRequiredService(); + await ImportProject(crdtApi, fwDataApi, fwDataApi.EntryCount); + }); + logger.LogInformation("Import of {ProjectName} complete!", fwDataApi.Project.Name); + return project; + } + + async Task ImportProject(ILexboxApi importTo, ILexboxApi importFrom, int entryCount) + { + var writingSystems = await importFrom.GetWritingSystems(); + foreach (var ws in writingSystems.Analysis) + { + await importTo.CreateWritingSystem(WritingSystemType.Analysis, ws); + logger.LogInformation("Imported ws {WsId}", ws.Id); + } + + foreach (var ws in writingSystems.Vernacular) + { + await importTo.CreateWritingSystem(WritingSystemType.Vernacular, ws); + logger.LogInformation("Imported ws {WsId}", ws.Id); + } + + var index = 0; + await foreach (var entry in importFrom.GetEntries(new QueryOptions(Count: 100_000, Offset: 0))) + { + if (importTo is CrdtLexboxApi crdtLexboxApi) + { + await crdtLexboxApi.CreateEntryLite(entry); + } + else + { + await importTo.CreateEntry(entry); + } + + logger.LogInformation("Imported entry, {Index} of {Count} {Id}", index++, entryCount, entry.Id); + } + } +} diff --git a/backend/LocalWebApp/Services/LexboxProjectService.cs b/backend/LocalWebApp/Services/LexboxProjectService.cs new file mode 100644 index 000000000..c8abc02be --- /dev/null +++ b/backend/LocalWebApp/Services/LexboxProjectService.cs @@ -0,0 +1,38 @@ +using LocalWebApp.Auth; + +namespace LocalWebApp.Services; + +public class LexboxProjectService(AuthHelpersFactory helpersFactory, ILogger logger) +{ + public record LexboxCrdtProject(Guid Id, string Name); + + public async Task GetLexboxProjects() + { + var httpClient = await helpersFactory.GetDefault().CreateClient(); + if (httpClient is null) return []; + try + { + return await httpClient.GetFromJsonAsync("api/crdt/listProjects") ?? []; + } + catch (HttpRequestException e) + { + logger.LogError(e, "Error getting lexbox projects"); + return []; + } + } + + public async Task GetLexboxProjectId(string code) + { + var httpClient = await helpersFactory.GetDefault().CreateClient(); + if (httpClient is null) return null; + try + { + return (await httpClient.GetFromJsonAsync($"api/crdt/lookupProjectId?code={code}")); + } + catch (HttpRequestException e) + { + logger.LogError(e, "Error getting lexbox project id"); + return null; + } + } +} diff --git a/backend/LocalWebApp/SyncService.cs b/backend/LocalWebApp/SyncService.cs index 9540ff2dd..5168f40d1 100644 --- a/backend/LocalWebApp/SyncService.cs +++ b/backend/LocalWebApp/SyncService.cs @@ -1,16 +1,18 @@ using Crdt; using LcmCrdt; +using LocalWebApp.Auth; namespace LocalWebApp; public class SyncService( DataModel dataModel, CrdtHttpSyncService remoteSyncServiceServer, + AuthHelpersFactory factory, CurrentProjectService currentProjectService) { public async Task ExecuteSync() { - var remoteModel = remoteSyncServiceServer.CreateProjectSyncable(await currentProjectService.GetProjectData()); + var remoteModel = await remoteSyncServiceServer.CreateProjectSyncable(await currentProjectService.GetProjectData()); return await dataModel.SyncWith(remoteModel); } } diff --git a/backend/LocalWebApp/UrlContext.cs b/backend/LocalWebApp/UrlContext.cs new file mode 100644 index 000000000..106bd26d0 --- /dev/null +++ b/backend/LocalWebApp/UrlContext.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; + +namespace LocalWebApp; + +public class UrlContext(IServer server, IHttpContextAccessor contextAccessor) +{ + /// + /// url returned is a guess when it comes from IServer, instead of IHttpContext + /// + /// + /// + public (Uri host, bool guess) GetUrl() + { + var httpContext = contextAccessor.HttpContext; + if (httpContext is not null) + { + var uriBuilder = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, httpContext.Request.Host.Port ?? 80); + return (uriBuilder.Uri, false); + } + var address = server.Features.Get()?.Addresses.FirstOrDefault() ?? throw new InvalidOperationException("No server address"); + return (new Uri(address), true); + } +} diff --git a/backend/LocalWebApp/Utils/CancellationTokenExtensions.cs b/backend/LocalWebApp/Utils/CancellationTokenExtensions.cs new file mode 100644 index 000000000..35da0129f --- /dev/null +++ b/backend/LocalWebApp/Utils/CancellationTokenExtensions.cs @@ -0,0 +1,10 @@ +namespace LocalWebApp.Utils; + +public static class CancellationTokenExtensions +{ + public static CancellationToken Merge(this CancellationToken token1, CancellationToken token2) + { + var cts = CancellationTokenSource.CreateLinkedTokenSource(token1, token2); + return cts.Token; + } +} diff --git a/backend/LocalWebApp/Utils/ConfigExtensions.cs b/backend/LocalWebApp/Utils/ConfigExtensions.cs new file mode 100644 index 000000000..62a95a732 --- /dev/null +++ b/backend/LocalWebApp/Utils/ConfigExtensions.cs @@ -0,0 +1,16 @@ +namespace LocalWebApp.Utils; + +public static class ConfigExtensions +{ + public static void ConfigureProd(this WebApplicationBuilder builder, Action configureOptions) where T : class + { + if (!builder.Environment.IsProduction()) return; + builder.Services.Configure(configureOptions); + } + + public static void ConfigureDev(this WebApplicationBuilder builder, Action configureOptions) where T : class + { + if (!builder.Environment.IsDevelopment()) return; + builder.Services.Configure(configureOptions); + } +} diff --git a/backend/LocalWebApp/appsettings.Development.json b/backend/LocalWebApp/appsettings.Development.json index 0c208ae91..960be5067 100644 --- a/backend/LocalWebApp/appsettings.Development.json +++ b/backend/LocalWebApp/appsettings.Development.json @@ -1,8 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Information" } } } diff --git a/backend/MiniLcm/MultiString.cs b/backend/MiniLcm/MultiString.cs index 8946807a2..6f7e96e1f 100644 --- a/backend/MiniLcm/MultiString.cs +++ b/backend/MiniLcm/MultiString.cs @@ -14,13 +14,13 @@ public class MultiString: IDictionary { public MultiString() { - _values = new MultiStringDict(); + Values = new MultiStringDict(); } [JsonConstructor] public MultiString(IDictionary values) { - _values = new MultiStringDict(values); + Values = new MultiStringDict(values); } public virtual MultiString Copy() @@ -28,13 +28,12 @@ public virtual MultiString Copy() return new(Values); } - private readonly MultiStringDict _values; - public virtual IDictionary Values => _values; + public virtual IDictionary Values { get; } public string this[WritingSystemId key] { - get => _values[key]; - set => _values[key] = value; + get => Values[key]; + set => Values[key] = value; } private class MultiStringDict : Dictionary, #pragma warning disable CS8644 // Type does not implement interface member. Nullability of reference types in interface implemented by the base type doesn't match. @@ -79,42 +78,42 @@ void IDictionary.Add(object key, object? value) void IDictionary.Add(object key, object? value) { - ((IDictionary)_values).Add(key, value); + ((IDictionary)Values).Add(key, value); } void IDictionary.Clear() { - ((IDictionary)_values).Clear(); + ((IDictionary)Values).Clear(); } bool IDictionary.Contains(object key) { - return ((IDictionary)_values).Contains(key); + return ((IDictionary)Values).Contains(key); } IDictionaryEnumerator IDictionary.GetEnumerator() { - return ((IDictionary)_values).GetEnumerator(); + return ((IDictionary)Values).GetEnumerator(); } void IDictionary.Remove(object key) { - ((IDictionary)_values).Remove(key); + ((IDictionary)Values).Remove(key); } - bool IDictionary.IsFixedSize => ((IDictionary)_values).IsFixedSize; + bool IDictionary.IsFixedSize => ((IDictionary)Values).IsFixedSize; - bool IDictionary.IsReadOnly => ((IDictionary)_values).IsReadOnly; + bool IDictionary.IsReadOnly => ((IDictionary)Values).IsReadOnly; object? IDictionary.this[object key] { - get => ((IDictionary)_values)[key]; - set => ((IDictionary)_values)[key] = value; + get => ((IDictionary)Values)[key]; + set => ((IDictionary)Values)[key] = value; } - ICollection IDictionary.Keys => ((IDictionary)_values).Keys; + ICollection IDictionary.Keys => ((IDictionary)Values).Keys; - ICollection IDictionary.Values => ((IDictionary)_values).Values; + ICollection IDictionary.Values => ((IDictionary)Values).Values; IEnumerator IEnumerable.GetEnumerator() { @@ -123,14 +122,14 @@ IEnumerator IEnumerable.GetEnumerator() void ICollection.CopyTo(Array array, int index) { - ((IDictionary)_values).CopyTo(array, index); + ((IDictionary)Values).CopyTo(array, index); } - int ICollection.Count => ((IDictionary)_values).Count; + int ICollection.Count => ((IDictionary)Values).Count; - bool ICollection.IsSynchronized => ((IDictionary)_values).IsSynchronized; + bool ICollection.IsSynchronized => ((IDictionary)Values).IsSynchronized; - object ICollection.SyncRoot => ((IDictionary)_values).SyncRoot; + object ICollection.SyncRoot => ((IDictionary)Values).SyncRoot; } public static class MultiStringExtensions diff --git a/backend/SyncReverseProxy/HgRequestTransformer.cs b/backend/SyncReverseProxy/HgRequestTransformer.cs index 493412e1b..90c795dc4 100644 --- a/backend/SyncReverseProxy/HgRequestTransformer.cs +++ b/backend/SyncReverseProxy/HgRequestTransformer.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using Microsoft.Net.Http.Headers; using Yarp.ReverseProxy.Forwarder; namespace LexSyncReverseProxy; @@ -14,6 +15,11 @@ public override async ValueTask TransformRequestAsync(HttpContext httpContext, CancellationToken cancellationToken) { await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken); + + // Remove the cookie header from the request + proxyRequest.Headers.Remove(HeaderNames.Cookie); + proxyRequest.Headers.Remove(HeaderNames.Authorization); + var path = httpContext.Request.Path.ToString(); if (path.StartsWith("/hg")) path = path["/hg".Length..]; var builder = new UriBuilder(RequestUtilities.MakeDestinationAddress(destinationPrefix, diff --git a/backend/Taskfile.yml b/backend/Taskfile.yml index a6fc36ee9..c0e4637ae 100644 --- a/backend/Taskfile.yml +++ b/backend/Taskfile.yml @@ -1,4 +1,8 @@ version: 3 +includes: + ui: + taskfile: ../frontend/Taskfile.yml + dir: ../frontend tasks: api-only: @@ -87,10 +91,16 @@ tasks: cmds: - kubectl port-forward service/db 27018:27017 -n languageforge --context dallas-rke - local-web-app: + local-web-app-for-develop: label: dotnet dir: ./LocalWebApp cmd: dotnet watch --no-hot-reload + local-web-app: + label: Run LocalWebApp with Local LexBox + env: + Auth__DefaultAuthority: "https://localhost:3050" + dir: ./LocalWebApp + cmd: dotnet watch --no-hot-reload publish-local-all: cmds: @@ -99,6 +109,7 @@ tasks: - task: publish-local-osx publish-local-win: dir: ./LocalWebApp + deps: [ui:build-viewer-app] cmd: dotnet publish -r win-x64 publish-local-linux: dir: ./LocalWebApp diff --git a/backend/Testing/LexCore/Services/ProjectServiceTest.cs b/backend/Testing/LexCore/Services/ProjectServiceTest.cs index 2151e8db8..49e785c49 100644 --- a/backend/Testing/LexCore/Services/ProjectServiceTest.cs +++ b/backend/Testing/LexCore/Services/ProjectServiceTest.cs @@ -1,7 +1,7 @@ using LexBoxApi.Services; +using LexBoxApi.Services.Email; using LexCore.Entities; using LexCore.ServiceInterfaces; -using LexData; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; @@ -22,6 +22,7 @@ public ProjectServiceTest(TestingServicesFixture testing) var serviceProvider = testing.ConfigureServices(s => { s.AddScoped(_ => Mock.Of()); + s.AddScoped(_ => Mock.Of()); s.AddSingleton(_ => Mock.Of()); s.AddScoped(); }); @@ -32,7 +33,7 @@ public ProjectServiceTest(TestingServicesFixture testing) public async Task CanCreateProject() { var projectId = await _projectService.CreateProject( - new(null, "TestProject", "Test", "test", ProjectType.FLEx, RetentionPolicy.Test, false, null)); + new(null, "TestProject", "Test", "test", ProjectType.FLEx, RetentionPolicy.Test, false, null, null)); projectId.ShouldNotBe(default); } @@ -41,10 +42,10 @@ public async Task ShouldErrorIfCreatingAProjectWithTheSameCode() { //first project should be created await _projectService.CreateProject( - new(null, "TestProject", "Test", "test-dup-code", ProjectType.FLEx, RetentionPolicy.Test, false, null)); + new(null, "TestProject", "Test", "test-dup-code", ProjectType.FLEx, RetentionPolicy.Test, false, null, null)); var exception = await _projectService.CreateProject( - new(null, "Test2", "Test desc", "test-dup-code", ProjectType.Unknown, RetentionPolicy.Dev, false, null) + new(null, "Test2", "Test desc", "test-dup-code", ProjectType.Unknown, RetentionPolicy.Dev, false, null, null) ).ShouldThrowAsync(); exception.InnerException.ShouldBeOfType() diff --git a/backend/harmony b/backend/harmony index 26d825dd8..41d6c7f9f 160000 --- a/backend/harmony +++ b/backend/harmony @@ -1 +1 @@ -Subproject commit 26d825dd8509a4a0793f71cc17ff367327599363 +Subproject commit 41d6c7f9fb04620793a82b3926472a193dfb1789 diff --git a/data/Dockerfile b/data/Dockerfile new file mode 100644 index 000000000..61f205bb4 --- /dev/null +++ b/data/Dockerfile @@ -0,0 +1,4 @@ +FROM busybox:1.36.1 + +WORKDIR /init-repos +COPY . /init-repos/ diff --git a/deployment/Taskfile.yml b/deployment/Taskfile.yml index 8dee1bd1f..70d861614 100644 --- a/deployment/Taskfile.yml +++ b/deployment/Taskfile.yml @@ -76,6 +76,10 @@ tasks: cmds: - kubectl port-forward service/db 5434:5432 -n languagedepot --context dallas-rke + develop-db-forward: + cmds: + - kubectl port-forward service/db 5436:5432 -n languagedepot-dev --context dallas-rke + prod-db-forward: cmds: - kubectl port-forward service/db 5435:5432 -n languagedepot --context aws-rke diff --git a/deployment/base/app-config.yaml b/deployment/base/app-config.yaml index 2cdf08cde..7a0103c97 100644 --- a/deployment/base/app-config.yaml +++ b/deployment/base/app-config.yaml @@ -6,4 +6,5 @@ data: environment-name: "Development" hg-otel-disabled: "false" # "true" to disable OpenTelemetry hg-domain: "hg.localhost" + enable-oauth: "false" diff --git a/deployment/base/kustomization.yaml b/deployment/base/kustomization.yaml index 4cd40b495..fe9002a1a 100644 --- a/deployment/base/kustomization.yaml +++ b/deployment/base/kustomization.yaml @@ -13,3 +13,4 @@ resources: - ui-deployment.yaml - proxy-deployment.yaml - app-config.yaml +- oauth-certs.yaml diff --git a/deployment/base/lexbox-deployment.yaml b/deployment/base/lexbox-deployment.yaml index 2e03a1dcc..da2c22e73 100644 --- a/deployment/base/lexbox-deployment.yaml +++ b/deployment/base/lexbox-deployment.yaml @@ -75,6 +75,12 @@ spec: volumeMounts: - name: repos mountPath: /hg-repos + - name: oauth-signing-cert + mountPath: /oauth-certs/signing + readOnly: true + - name: oauth-encryption-cert + mountPath: /oauth-certs/encryption + readOnly: true env: - name: DOTNET_URLS @@ -174,6 +180,11 @@ spec: secretKeyRef: key: SMTP_PASSWORD name: email + - name: Authentication__OpenId__Enable + valueFrom: + configMapKeyRef: + name: app-config + key: enable-oauth - name: Authentication__Google__ClientId valueFrom: secretKeyRef: @@ -192,7 +203,7 @@ spec: value: /tmp/tus-reset-upload - name: otel-collector - image: otel/opentelemetry-collector-contrib:0.87.0 + image: otel/opentelemetry-collector-contrib:0.101.0 args: - "--config" - "/etc/otelcol-contrib/config.yaml" @@ -231,6 +242,14 @@ spec: items: - key: collector-config.yaml path: config.yaml + - name: oauth-signing-cert + secret: + secretName: oauth-signing-cert + optional: true + - name: oauth-encryption-cert + secret: + secretName: oauth-encryption-cert + optional: true initContainers: - name: db-migrations @@ -261,6 +280,11 @@ spec: configMapKeyRef: name: app-config key: environment-name + - name: Authentication__OpenId__Enable + valueFrom: + configMapKeyRef: + name: app-config + key: enable-oauth - name: set-repo-structure-owner-to-www-data securityContext: # Make sure we're authorized to set ownership diff --git a/deployment/base/oauth-certs.yaml b/deployment/base/oauth-certs.yaml new file mode 100644 index 000000000..d35e11e2a --- /dev/null +++ b/deployment/base/oauth-certs.yaml @@ -0,0 +1,46 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned-issuer +spec: + selfSigned: { } +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: oauth-encryption-cert +spec: + commonName: lexbox Encryption cert + duration: 2160h # 90 days + renewBefore: 360h # 15 days + issuerRef: + group: cert-manager.io + kind: Issuer + name: selfsigned-issuer + privateKey: + rotationPolicy: Always + algorithm: RSA + revisionHistoryLimit: 5 + secretName: oauth-encryption-cert + usages: + - key encipherment +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: oauth-signing-cert +spec: + commonName: lexbox Signing cert + duration: 2160h # 90 days + renewBefore: 360h # 15 days + issuerRef: + group: cert-manager.io + kind: Issuer + name: selfsigned-issuer + privateKey: + rotationPolicy: Always + algorithm: RSA + revisionHistoryLimit: 5 + secretName: oauth-signing-cert + usages: + - digital signature diff --git a/deployment/develop/app-config.yaml b/deployment/develop/app-config.yaml index f0e1b2f3c..7dfe2725d 100644 --- a/deployment/develop/app-config.yaml +++ b/deployment/develop/app-config.yaml @@ -5,4 +5,5 @@ metadata: data: environment-name: "Development" hg-domain: "hg-develop.lexbox.org" + enable-oauth: "true" diff --git a/deployment/init-repos/hg-deployment-patch.yaml b/deployment/init-repos/hg-deployment-patch.yaml index 1fefc7d51..aff50e41d 100644 --- a/deployment/init-repos/hg-deployment-patch.yaml +++ b/deployment/init-repos/hg-deployment-patch.yaml @@ -13,17 +13,26 @@ spec: runAsGroup: 33 # www-data runAsNonRoot: true image: busybox:1.36.1 + imagePullPolicy: IfNotPresent command: - 'sh' - '-c' - | if [ ! -d /repos/s/sena-3 ] && [ ! -d /repos/sena-3 ]; then - wget -O /tmp/sena-3.zip 'https://drive.google.com/uc?export=download&id=1I-hwc0RHoQqW774gbS5qR-GHa1E7BlsS' - unzip -q /tmp/sena-3.zip -d /repos/s/ + if [ -f /init-repos/sena-3.zip ]; then + unzip -q /init-repos/sena-3.zip -d /repos/s/ + else + wget -O /tmp/sena-3.zip 'https://drive.google.com/uc?export=download&id=1I-hwc0RHoQqW774gbS5qR-GHa1E7BlsS' + unzip -q /tmp/sena-3.zip -d /repos/s/ + fi fi if [ ! -d /repos/e/elawa-dev-flex ] && [ ! -d /repos/elawa-dev-flex ]; then - wget -O /tmp/elawa.zip 'https://drive.usercontent.google.com/download?export=download&id=1Jk-eSDho8ATBMS-Kmfatwi-MWQth26ro&confirm=t' - unzip -q /tmp/elawa.zip -d /repos/e/ + if [ -f /init-repos/elawa.zip ]; then + unzip -q /init-repos/elawa.zip -d /repos/e/ + else + wget -O /tmp/elawa.zip 'https://drive.usercontent.google.com/download?export=download&id=1Jk-eSDho8ATBMS-Kmfatwi-MWQth26ro&confirm=t' + unzip -q /tmp/elawa.zip -d /repos/e/ + fi fi volumeMounts: - name: repos diff --git a/deployment/local-dev/app-config.yaml b/deployment/local-dev/app-config.yaml new file mode 100644 index 000000000..5a08624a5 --- /dev/null +++ b/deployment/local-dev/app-config.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + enable-oauth: "true" diff --git a/deployment/local-dev/delete-oauth-certs.yaml b/deployment/local-dev/delete-oauth-certs.yaml new file mode 100644 index 000000000..44d9e483f --- /dev/null +++ b/deployment/local-dev/delete-oauth-certs.yaml @@ -0,0 +1,8 @@ +$patch: delete +# This patch deletes the Issuer and Certificate resources that are used for OAuth because local dev does not support them +# the values here are actually ignored, the $patch: delete is the only important part +# we can't remove the following otherwise there would be a parsing error +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned-issuer diff --git a/deployment/local-dev/ingress-config.patch.yaml b/deployment/local-dev/ingress-config.patch.yaml index e4678f4d5..a035e1fa2 100644 --- a/deployment/local-dev/ingress-config.patch.yaml +++ b/deployment/local-dev/ingress-config.patch.yaml @@ -1,7 +1,7 @@ -- op: replace - path: /spec/tls/0/secretName - value: null -- op: add +- op: add # ~1 gets replaced with a / in the path path: /metadata/annotations/nginx.ingress.kubernetes.io~1ssl-redirect value: "false" +- op: add + path: /metadata/annotations/cert-manager.io~1cluster-issuer + value: selfsigned-issuer diff --git a/deployment/local-dev/kustomization.yaml b/deployment/local-dev/kustomization.yaml index 259f675b8..37d03473b 100644 --- a/deployment/local-dev/kustomization.yaml +++ b/deployment/local-dev/kustomization.yaml @@ -8,6 +8,7 @@ resources: - ingress-deployment.yaml - db-secrets.yaml - lf-classic-secrets.yaml +- self-signed-ssl.yaml components: - ../init-repos @@ -29,6 +30,14 @@ patches: version: v1 kind: PersistentVolumeClaim path: change-storage-class.patch.yaml + - target: + kind: Issuer + path: delete-oauth-certs.yaml + - target: + kind: Certificate + path: delete-oauth-certs.yaml + + - path: app-config.yaml - path: lexbox-deployment.patch.yaml - path: ui-deployment.patch.yaml - path: hg-repos-pvc.patch.yaml @@ -38,3 +47,7 @@ patches: kind: Ingress name: proxy namespace: languagedepot + +images: + - name: busybox:1.36.1 + newName: local-dev-init diff --git a/deployment/local-dev/self-signed-ssl.yaml b/deployment/local-dev/self-signed-ssl.yaml new file mode 100644 index 000000000..2ed283b50 --- /dev/null +++ b/deployment/local-dev/self-signed-ssl.yaml @@ -0,0 +1,6 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned-issuer +spec: + selfSigned: { } diff --git a/frontend/Taskfile.yml b/frontend/Taskfile.yml index ece4c16b6..e72e0c6a6 100644 --- a/frontend/Taskfile.yml +++ b/frontend/Taskfile.yml @@ -38,6 +38,10 @@ tasks: aliases: [ s ] cmd: pnpm {{.CLI_ARGS}} + build-viewer-app: + dir: ./viewer + deps: [ install-viewer ] + cmd: pnpm run build-app install-viewer: dir: ./viewer method: checksum @@ -52,3 +56,20 @@ tasks: dir: ./viewer deps: [ install-viewer ] cmd: pnpm run dev-app + + + install-https-proxy: + dir: ./https-proxy + method: checksum + sources: + - package.json + cmds: + - corepack enable || true + - pnpm install + https-proxy: + dir: ./https-proxy + desc: "MSAL requires the oauth authority to be available over https. That's why this is here. As a bonus it dynamically looks for the UI either locally or in k8s." + aliases: [ https-oauth-authority ] + deps: [ install-https-proxy ] + cmd: pnpm run dev + diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 739761c65..2e376186d 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -20,7 +20,8 @@ export default [ 'playwright.config.ts', '.svelte-kit/**', '**/generated/**', - 'viewer/' + 'viewer/', + 'https-proxy/', ], }, js.configs.recommended, diff --git a/frontend/https-proxy/.gitignore b/frontend/https-proxy/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/frontend/https-proxy/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/https-proxy/package.json b/frontend/https-proxy/package.json new file mode 100644 index 000000000..2e333b1be --- /dev/null +++ b/frontend/https-proxy/package.json @@ -0,0 +1,15 @@ +{ + "name": "https-proxy", + "version": "0.0.1", + "private": true, + "packageManager": "pnpm@8.15.1", + "type": "module", + "scripts": { + "dev": "vite" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.2.0", + "@vitejs/plugin-basic-ssl": "^1.1.0" + } +} diff --git a/frontend/https-proxy/vite.config.ts b/frontend/https-proxy/vite.config.ts new file mode 100644 index 000000000..79b3c0227 --- /dev/null +++ b/frontend/https-proxy/vite.config.ts @@ -0,0 +1,64 @@ +import basicSsl from '@vitejs/plugin-basic-ssl'; +import {defineConfig, type ProxyOptions} from 'vite'; +import http from 'http'; + +async function checkTargetAvailability(url: string): Promise { + return new Promise((resolve) => { + const req = http.get(url, (res) => { + resolve(!!res.statusCode && res.statusCode < 400); + }); + + req.on('error', () => { + resolve(false); + }); + + req.end(); + }); +} + +const targets = ['http://localhost:3000', 'http://localhost']; + +const lexboxServer: ProxyOptions = { + target: targets[0], + secure: false, + changeOrigin: false, + autoRewrite: true, + protocolRewrite: 'https', + headers: { + 'x-forwarded-proto': 'https', + }, + configure: async (proxy, options) => { + let availableTarget: string | undefined = undefined; + + proxy.on('proxyReq', function () { + if (!availableTarget) console.warn(`Request before target (${lexboxServer.target}) was confirmed to be available.`); + }); + + while (!availableTarget) { + for (const target of targets) { + const isAvailable = await checkTargetAvailability(target); + if (isAvailable) { + options.target = availableTarget = target; + console.log('Will proxy to available target:', target); + return; + } + } + console.warn('No target available, retrying in 5s'); + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + }, +}; + +export default defineConfig({ + plugins: [ + basicSsl(), + ], + server: { + port: 3050, + host: true, + strictPort: true, + proxy: { + '/': lexboxServer, + } + }, +}); diff --git a/frontend/package.json b/frontend/package.json index 9696f83cf..1733b9b14 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "test-flaky": "playwright test --retries=3 -j 30%", "test-hard": "playwright test --repeat-each=3 -j 30%", "test-report": "playwright show-report test-results/_html-report", - "_env-comment": "Run any command with .env.local loaded: (e.g. `pnpm run env run test -g \"test name\")", + "_env-comment": "Run any command with .env.local loaded: (e.g. `pnpm run env run test -g 'test name')", "env": "dotenvx run --env-file=.env.local -- pnpm", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", @@ -38,8 +38,8 @@ "@iconify-json/mdi": "^1.1.66", "@playwright/test": "^1.44.0", "@sveltejs/adapter-node": "^4.0.1", - "@sveltejs/kit": "^2.5.8", - "@sveltejs/vite-plugin-svelte": "^3.1.0", + "@sveltejs/kit": "^2.5.10", + "@sveltejs/vite-plugin-svelte": "^3.1.1", "@tailwindcss/typography": "^0.5.13", "@types/mjml": "^4.7.4", "@types/node": "^20.12.12", @@ -69,7 +69,7 @@ "tslib": "^2.6.2", "type-fest": "^4.18.2", "typescript": "^5.3.3", - "vite": "^5.2.11", + "vite": "^5.2.13", "vite-plugin-graphql-codegen": "^3.3.6", "vitest": "^1.6.0", "zod": "^3.23.8", @@ -79,8 +79,8 @@ "dependencies": { "@floating-ui/dom": "^1.6.5", "@opentelemetry/api": "^1.8.0", - "@opentelemetry/auto-instrumentations-node": "^0.46.1", - "@opentelemetry/auto-instrumentations-web": "^0.39.0", + "@opentelemetry/auto-instrumentations-node": "^0.47.1", + "@opentelemetry/auto-instrumentations-web": "^0.40.0", "@opentelemetry/context-zone": "^1.24.1", "@opentelemetry/exporter-trace-otlp-http": "^0.51.1", "@opentelemetry/instrumentation": "^0.51.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2bc116381..1fed2c423 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -15,11 +15,11 @@ importers: specifier: ^1.8.0 version: 1.8.0 '@opentelemetry/auto-instrumentations-node': - specifier: ^0.46.1 - version: 0.46.1(@opentelemetry/api@1.8.0) + specifier: ^0.47.1 + version: 0.47.1(@opentelemetry/api@1.8.0) '@opentelemetry/auto-instrumentations-web': - specifier: ^0.39.0 - version: 0.39.0(@opentelemetry/api@1.8.0)(zone.js@0.11.8) + specifier: ^0.40.0 + version: 0.40.0(@opentelemetry/api@1.8.0)(zone.js@0.14.7) '@opentelemetry/context-zone': specifier: ^1.24.1 version: 1.24.1(@opentelemetry/api@1.8.0) @@ -52,7 +52,7 @@ importers: version: 2.4.7 '@vitejs/plugin-basic-ssl': specifier: ^1.1.0 - version: 1.1.0(vite@5.2.11) + version: 1.1.0(vite@5.2.13) css-tree: specifier: ^2.3.1 version: 2.3.1 @@ -70,10 +70,10 @@ importers: version: 3.0.5(svelte@4.2.17) svelte-intl-precompile: specifier: ^0.12.3 - version: 0.12.3(@babel/core@7.24.4)(svelte@4.2.17) + version: 0.12.3(@babel/core@7.24.7)(svelte@4.2.17) sveltekit-search-params: specifier: ^2.1.2 - version: 2.1.2(@sveltejs/kit@2.5.8)(svelte@4.2.17)(vite@5.2.11) + version: 2.1.2(@sveltejs/kit@2.5.10)(svelte@4.2.17)(vite@5.2.13) tus-js-client: specifier: ^4.1.0 version: 4.1.0 @@ -113,13 +113,13 @@ importers: version: 1.44.0 '@sveltejs/adapter-node': specifier: ^4.0.1 - version: 4.0.1(@sveltejs/kit@2.5.8) + version: 4.0.1(@sveltejs/kit@2.5.10) '@sveltejs/kit': - specifier: ^2.5.8 - version: 2.5.8(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.17)(vite@5.2.11) + specifier: ^2.5.10 + version: 2.5.10(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@4.2.17)(vite@5.2.13) '@sveltejs/vite-plugin-svelte': - specifier: ^3.1.0 - version: 3.1.0(svelte@4.2.17)(vite@5.2.11) + specifier: ^3.1.1 + version: 3.1.1(svelte@4.2.17)(vite@5.2.13) '@tailwindcss/typography': specifier: ^0.5.13 version: 0.5.13(tailwindcss@3.4.3) @@ -182,19 +182,19 @@ importers: version: 4.2.17 svelte-check: specifier: ^3.7.1 - version: 3.7.1(@babel/core@7.24.4)(postcss@8.4.38)(svelte@4.2.17) + version: 3.7.1(@babel/core@7.24.7)(postcss@8.4.38)(svelte@4.2.17) svelte-eslint-parser: specifier: ^0.33.1 version: 0.33.1(svelte@4.2.17) svelte-preprocess: specifier: ^5.1.4 - version: 5.1.4(@babel/core@7.24.4)(postcss@8.4.38)(svelte@4.2.17)(typescript@5.3.3) + version: 5.1.4(@babel/core@7.24.7)(postcss@8.4.38)(svelte@4.2.17)(typescript@5.3.3) svelte-turnstile: specifier: ^0.5.0 version: 0.5.0(svelte@4.2.17) sveltekit-superforms: specifier: ^1.13.4 - version: 1.13.4(@sveltejs/kit@2.5.8)(svelte@4.2.17)(zod@3.23.8) + version: 1.13.4(@sveltejs/kit@2.5.10)(svelte@4.2.17)(zod@3.23.8) tailwindcss: specifier: ^3.4.3 version: 3.4.3 @@ -208,11 +208,11 @@ importers: specifier: ^5.3.3 version: 5.3.3 vite: - specifier: ^5.2.11 - version: 5.2.11(@types/node@20.12.12) + specifier: ^5.2.13 + version: 5.2.13(@types/node@20.12.12) vite-plugin-graphql-codegen: specifier: ^3.3.6 - version: 3.3.6(@graphql-codegen/cli@5.0.2)(graphql@16.8.1)(vite@5.2.11) + version: 3.3.6(@graphql-codegen/cli@5.0.2)(graphql@16.8.1)(vite@5.2.13) vitest: specifier: ^1.6.0 version: 1.6.0(@types/node@20.12.12) @@ -223,6 +223,18 @@ importers: specifier: ^4.4.2 version: 4.4.2 + https-proxy: + devDependencies: + '@vitejs/plugin-basic-ssl': + specifier: ^1.1.0 + version: 1.1.0(vite@5.2.13) + typescript: + specifier: ^5.2.2 + version: 5.3.3 + vite: + specifier: ^5.2.0 + version: 5.2.13(@types/node@20.12.12) + viewer: dependencies: '@microsoft/dotnet-js-interop': @@ -242,13 +254,13 @@ importers: version: 8.4.38 svelte-preprocess: specifier: ^5.1.4 - version: 5.1.4(@babel/core@7.24.4)(postcss@8.4.38)(svelte@4.2.17)(typescript@5.3.3) + version: 5.1.4(@babel/core@7.24.7)(postcss@8.4.38)(svelte@4.2.17)(typescript@5.3.3) svelte-routing: specifier: ^2.12.0 version: 2.13.0 svelte-ux: - specifier: ^0.62.10 - version: 0.62.12(@babel/core@7.24.4)(postcss@8.4.38)(svelte@4.2.17) + specifier: ^0.66.8 + version: 0.66.8(@babel/core@7.24.7)(postcss@8.4.38)(svelte@4.2.17) type-fest: specifier: ^4.18.2 version: 4.18.2 @@ -263,8 +275,8 @@ importers: specifier: ^7.4.47 version: 7.4.47 '@sveltejs/vite-plugin-svelte': - specifier: ^3.1.0 - version: 3.1.0(svelte@4.2.17)(vite@5.2.11) + specifier: ^3.1.1 + version: 3.1.1(svelte@4.2.17)(vite@5.2.13) '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.4 @@ -273,7 +285,7 @@ importers: version: 4.2.17 svelte-check: specifier: ^3.7.1 - version: 3.7.1(@babel/core@7.24.4)(postcss@8.4.38)(svelte@4.2.17) + version: 3.7.1(@babel/core@7.24.7)(postcss@8.4.38)(svelte@4.2.17) tailwindcss: specifier: ^3.4.3 version: 3.4.3 @@ -287,8 +299,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 vite: - specifier: ^5.2.11 - version: 5.2.11(@types/node@20.12.12) + specifier: ^5.2.13 + version: 5.2.13(@types/node@20.12.12) packages: @@ -385,10 +397,23 @@ packages: dependencies: '@babel/highlight': 7.24.2 picocolors: 1.0.0 + dev: true + + /@babel/code-frame@7.24.7: + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 /@babel/compat-data@7.23.5: resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} engines: {node: '>=6.9.0'} + dev: true + + /@babel/compat-data@7.24.7: + resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} + engines: {node: '>=6.9.0'} /@babel/core@7.24.4: resolution: {integrity: sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==} @@ -405,7 +430,30 @@ packages: '@babel/traverse': 7.24.1 '@babel/types': 7.24.0 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.5 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/core@7.24.7: + resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helpers': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + convert-source-map: 2.0.0 + debug: 4.3.5 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -420,6 +468,16 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 + dev: true + + /@babel/generator@7.24.7: + resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 /@babel/helper-annotate-as-pure@7.22.5: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} @@ -434,7 +492,18 @@ packages: dependencies: '@babel/compat-data': 7.23.5 '@babel/helper-validator-option': 7.23.5 - browserslist: 4.22.3 + browserslist: 4.23.1 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-compilation-targets@7.24.7: + resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + browserslist: 4.23.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -459,6 +528,13 @@ packages: /@babel/helper-environment-visitor@7.22.20: resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-environment-visitor@7.24.7: + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 /@babel/helper-function-name@7.23.0: resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} @@ -466,12 +542,27 @@ packages: dependencies: '@babel/template': 7.24.0 '@babel/types': 7.24.0 + dev: true + + /@babel/helper-function-name@7.24.7: + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true + + /@babel/helper-hoist-variables@7.24.7: + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 /@babel/helper-member-expression-to-functions@7.23.0: resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} @@ -485,6 +576,16 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true + + /@babel/helper-module-imports@7.24.7: + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.4): resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} @@ -498,6 +599,22 @@ packages: '@babel/helper-simple-access': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7): + resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color /@babel/helper-optimise-call-expression@7.22.5: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} @@ -527,6 +644,16 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true + + /@babel/helper-simple-access@7.24.7: + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color /@babel/helper-skip-transparent-expression-wrappers@7.22.5: resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} @@ -540,18 +667,40 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true + + /@babel/helper-split-export-declaration@7.24.7: + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 /@babel/helper-string-parser@7.24.1: resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-string-parser@7.24.7: + resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} + engines: {node: '>=6.9.0'} /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.24.7: + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} /@babel/helper-validator-option@7.23.5: resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.24.7: + resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} + engines: {node: '>=6.9.0'} /@babel/helpers@7.24.4: resolution: {integrity: sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==} @@ -562,6 +711,14 @@ packages: '@babel/types': 7.24.0 transitivePeerDependencies: - supports-color + dev: true + + /@babel/helpers@7.24.7: + resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 /@babel/highlight@7.24.2: resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} @@ -570,7 +727,17 @@ packages: '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.0.0 + picocolors: 1.0.1 + dev: true + + /@babel/highlight@7.24.7: + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 /@babel/parser@7.24.4: resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==} @@ -578,6 +745,14 @@ packages: hasBin: true dependencies: '@babel/types': 7.24.0 + dev: true + + /@babel/parser@7.24.7: + resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.7 /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.24.4): resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} @@ -887,6 +1062,15 @@ packages: '@babel/code-frame': 7.24.2 '@babel/parser': 7.24.4 '@babel/types': 7.24.0 + dev: true + + /@babel/template@7.24.7: + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 /@babel/traverse@7.24.1: resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} @@ -900,7 +1084,25 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.24.4 '@babel/types': 7.24.0 - debug: 4.3.4 + debug: 4.3.5 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/traverse@7.24.7: + resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + debug: 4.3.5 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -912,6 +1114,15 @@ packages: '@babel/helper-string-parser': 7.24.1 '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 + dev: true + + /@babel/types@7.24.7: + resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 /@colors/colors@1.6.0: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} @@ -1234,8 +1445,8 @@ packages: tslib: 2.6.2 dev: false - /@fortawesome/fontawesome-common-types@6.5.1: - resolution: {integrity: sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==} + /@fortawesome/fontawesome-common-types@6.5.2: + resolution: {integrity: sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==} engines: {node: '>=6'} requiresBuild: true dev: false @@ -2049,83 +2260,90 @@ packages: '@opentelemetry/api': 1.8.0 dev: false + /@opentelemetry/api-logs@0.52.0: + resolution: {integrity: sha512-HxjD7xH9iAE4OyhNaaSec65i1H6QZYBWSwWkowFfsc5YAcDvJG30/J1sRKXEQqdmUcKTXEAnA66UciqZha/4+Q==} + engines: {node: '>=14'} + dependencies: + '@opentelemetry/api': 1.8.0 + dev: false + /@opentelemetry/api@1.8.0: resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==} engines: {node: '>=8.0.0'} dev: false - /@opentelemetry/auto-instrumentations-node@0.46.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-s0CwmY9KYtPawOhV5YO2Gf62uVOQRNvT6Or8IZ0S4gr/kPVNhoMehTsQvqBwSWQfoFrkmW3KKOHiKJEp4dVGXg==} + /@opentelemetry/auto-instrumentations-node@0.47.1(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-W7Iz4SZhj6z5iqYTu4zZXr2woP/zD4dA6zFAz9PQEx21/SGn6+y6plcJTA08KnPVMbRff60D1IBdl547TyGy9A==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.4.1 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-amqplib': 0.37.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-aws-lambda': 0.41.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-aws-sdk': 0.41.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-bunyan': 0.38.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-cassandra-driver': 0.38.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-connect': 0.36.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-cucumber': 0.6.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-dataloader': 0.9.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-dns': 0.36.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-express': 0.39.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-fastify': 0.36.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-fs': 0.12.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-generic-pool': 0.36.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-graphql': 0.40.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-grpc': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-hapi': 0.38.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-http': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-ioredis': 0.40.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-knex': 0.36.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-koa': 0.40.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-lru-memoizer': 0.37.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-memcached': 0.36.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-mongodb': 0.43.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-mongoose': 0.38.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-mysql': 0.38.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-mysql2': 0.38.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-nestjs-core': 0.37.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-net': 0.36.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-pg': 0.41.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-pino': 0.39.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-redis': 0.39.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-redis-4': 0.39.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-restify': 0.38.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-router': 0.37.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-socket.io': 0.39.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-tedious': 0.10.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-undici': 0.2.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-winston': 0.37.0(@opentelemetry/api@1.8.0) - '@opentelemetry/resource-detector-alibaba-cloud': 0.28.9(@opentelemetry/api@1.8.0) - '@opentelemetry/resource-detector-aws': 1.5.0(@opentelemetry/api@1.8.0) - '@opentelemetry/resource-detector-azure': 0.2.7(@opentelemetry/api@1.8.0) - '@opentelemetry/resource-detector-container': 0.3.9(@opentelemetry/api@1.8.0) - '@opentelemetry/resource-detector-gcp': 0.29.9(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-amqplib': 0.38.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-aws-lambda': 0.42.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-aws-sdk': 0.42.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-bunyan': 0.39.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-cassandra-driver': 0.39.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-connect': 0.37.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-cucumber': 0.7.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-dataloader': 0.10.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-dns': 0.37.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-express': 0.40.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-fastify': 0.37.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-fs': 0.13.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-generic-pool': 0.37.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-graphql': 0.41.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-grpc': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-hapi': 0.39.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-http': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-ioredis': 0.41.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-knex': 0.37.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-koa': 0.41.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.38.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-memcached': 0.37.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-mongodb': 0.45.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-mongoose': 0.39.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-mysql': 0.39.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-mysql2': 0.39.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-nestjs-core': 0.38.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-net': 0.37.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-pg': 0.42.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-pino': 0.40.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-redis': 0.40.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-redis-4': 0.40.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-restify': 0.39.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-router': 0.38.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-socket.io': 0.40.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-tedious': 0.11.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-undici': 0.3.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-winston': 0.38.0(@opentelemetry/api@1.8.0) + '@opentelemetry/resource-detector-alibaba-cloud': 0.28.10(@opentelemetry/api@1.8.0) + '@opentelemetry/resource-detector-aws': 1.5.1(@opentelemetry/api@1.8.0) + '@opentelemetry/resource-detector-azure': 0.2.9(@opentelemetry/api@1.8.0) + '@opentelemetry/resource-detector-container': 0.3.11(@opentelemetry/api@1.8.0) + '@opentelemetry/resource-detector-gcp': 0.29.10(@opentelemetry/api@1.8.0) '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/sdk-node': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-node': 0.52.0(@opentelemetry/api@1.8.0) transitivePeerDependencies: - encoding - supports-color dev: false - /@opentelemetry/auto-instrumentations-web@0.39.0(@opentelemetry/api@1.8.0)(zone.js@0.11.8): - resolution: {integrity: sha512-RnN2NdWASajyRmErDk/8aMfSb6Vyphpg1bc7j+5Hz0+XrlokmniTyaQT04z6AU8EYLX06dM26r56/RhUV6yNJQ==} + /@opentelemetry/auto-instrumentations-web@0.40.0(@opentelemetry/api@1.8.0)(zone.js@0.14.7): + resolution: {integrity: sha512-WaeIjd9HyK+cGitynZrDocIqLnNQj1NrBwo5R3z/xHW+oYhkBpo8GwuvdSZW7erVqqPcRntHQt+IF+eUi9Tylw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 zone.js: ^0.11.4 || ^0.13.0 || ^0.14.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-document-load': 0.38.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-fetch': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-user-interaction': 0.38.0(@opentelemetry/api@1.8.0)(zone.js@0.11.8) - '@opentelemetry/instrumentation-xml-http-request': 0.51.1(@opentelemetry/api@1.8.0) - zone.js: 0.11.8 + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-document-load': 0.39.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-fetch': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation-user-interaction': 0.39.0(@opentelemetry/api@1.8.0)(zone.js@0.14.7) + '@opentelemetry/instrumentation-xml-http-request': 0.52.0(@opentelemetry/api@1.8.0) + zone.js: 0.14.7 transitivePeerDependencies: - supports-color dev: false @@ -2139,6 +2357,15 @@ packages: '@opentelemetry/api': 1.8.0 dev: false + /@opentelemetry/context-async-hooks@1.25.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-sBW313mnMyFg0cp/40BRzrZBWG+581s2j5gIsa5fgGadswyILk4mNFATsqrCOpAx945RDuZ2B7ThQLgor9OpfA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.8.0 + dev: false + /@opentelemetry/context-zone-peer-dep@1.24.1(@opentelemetry/api@1.8.0)(zone.js@0.11.8): resolution: {integrity: sha512-s67becvBZFFjSLKSiy8ia2m7htsC4gsk8J/X0368FzBYseb/26daYr4ewx6tKcAsmZqJA7402cTQirv175x5BA==} engines: {node: '>=14'} @@ -2160,24 +2387,24 @@ packages: - '@opentelemetry/api' dev: false - /@opentelemetry/core@1.21.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-KP+OIweb3wYoP7qTYL/j5IpOlu52uxBv5M4+QhSmmUfLyTgu1OIS71msK3chFo1D6Y61BIH3wMiMYRCxJCQctA==} + /@opentelemetry/core@1.24.1(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-wMSGfsdmibI88K9wB498zXY04yThPexo8jvwNNlm542HZB7XrrMRBbAyKJqG8qDRJwIBdBrPMi4V9ZPW/sqrcg==} engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.8.0' + '@opentelemetry/api': '>=1.0.0 <1.9.0' dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/semantic-conventions': 1.21.0 + '@opentelemetry/semantic-conventions': 1.24.1 dev: false - /@opentelemetry/core@1.24.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-wMSGfsdmibI88K9wB498zXY04yThPexo8jvwNNlm542HZB7XrrMRBbAyKJqG8qDRJwIBdBrPMi4V9ZPW/sqrcg==} + /@opentelemetry/core@1.25.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-n0B3s8rrqGrasTgNkXLKXzN0fXo+6IYP7M5b7AMsrZM33f/y6DS6kJ0Btd7SespASWq8bgL3taLo0oe0vB52IQ==} engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.9.0' + '@opentelemetry/api': '>=1.0.0 <1.10.0' dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/semantic-conventions': 1.25.0 dev: false /@opentelemetry/exporter-trace-otlp-grpc@0.51.1(@opentelemetry/api@1.8.0): @@ -2195,6 +2422,21 @@ packages: '@opentelemetry/sdk-trace-base': 1.24.1(@opentelemetry/api@1.8.0) dev: false + /@opentelemetry/exporter-trace-otlp-grpc@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-Ln3HU54/ytTeEMrDGNDj01357YV8Kk9PkGDHvBRo1n7bWhwZoTEnX/cTuXLYOiygBIJJjCCM+VMfWCnvtFl4Kw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@grpc/grpc-js': 1.9.14 + '@opentelemetry/api': 1.8.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/otlp-transformer': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/resources': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-base': 1.25.0(@opentelemetry/api@1.8.0) + dev: false + /@opentelemetry/exporter-trace-otlp-http@0.51.1(@opentelemetry/api@1.8.0): resolution: {integrity: sha512-n+LhLPsX07URh+HhV2SHVSvz1t4G/l/CE5BjpmhAPqeTceFac1VpyQkavWEJbvnK5bUEXijWt4LxAxFpt2fXyw==} engines: {node: '>=14'} @@ -2209,6 +2451,20 @@ packages: '@opentelemetry/sdk-trace-base': 1.24.1(@opentelemetry/api@1.8.0) dev: false + /@opentelemetry/exporter-trace-otlp-http@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-umj9tOSEAuUdqw2EZua1Dby3c+FZ6xWGT2OF/KGLFLtyIvxhtTOSeMfBy/9CaxHn4vF8mAynmAP5MvVKnRYunA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/otlp-exporter-base': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/otlp-transformer': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/resources': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-base': 1.25.0(@opentelemetry/api@1.8.0) + dev: false + /@opentelemetry/exporter-trace-otlp-proto@0.51.1(@opentelemetry/api@1.8.0): resolution: {integrity: sha512-SE9f0/6V6EeXC9i+WA4WFjS1EYgaBCpAnI5+lxWvZ7iO7EU1IvHvZhP6Kojr0nLldo83gqg6G7OWFqsID3uF+w==} engines: {node: '>=14'} @@ -2224,6 +2480,20 @@ packages: '@opentelemetry/sdk-trace-base': 1.24.1(@opentelemetry/api@1.8.0) dev: false + /@opentelemetry/exporter-trace-otlp-proto@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-mpMEZFGaGnvon5pbjLieh7ffE9BuYnrG7qd4O5P3j1fk/4PCR3BcGfGhIfyZi0X8kBcjEhipiBfaHYqI7rxcXg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/otlp-exporter-base': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/otlp-transformer': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/resources': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-base': 1.25.0(@opentelemetry/api@1.8.0) + dev: false + /@opentelemetry/exporter-zipkin@1.24.1(@opentelemetry/api@1.8.0): resolution: {integrity: sha512-+Rl/VFmu2n6eaRMnVbyfZx1DqR/1KNyWebYuHyQBZaEAVIn/ZLgmofRpXN1X2nhJ4BNaptQUNxAstCYYz6dKoQ==} engines: {node: '>=14'} @@ -2237,28 +2507,41 @@ packages: '@opentelemetry/semantic-conventions': 1.24.1 dev: false - /@opentelemetry/instrumentation-amqplib@0.37.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-XjOHeAOreh0XX4jlzTTUWWqu1dIGvMWM8yvd43JJdRMAmTZisezjKsxLjMEMIvF0PzQdoXwh9DiS9nYE4/QmpA==} + /@opentelemetry/exporter-zipkin@1.25.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-nnhY0e5DHg8BfUSNCQZoGZnGeqz+zMTeEUOh1dfgtaXmF99uM0QPuTa1i2lH+eZqebP8w1WDWZlewu9FUlHqIg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/resources': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-base': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/semantic-conventions': 1.25.0 + dev: false + + /@opentelemetry/instrumentation-amqplib@0.38.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-6i1sZl2B329NoOeCFm0R6H/u0DLex7L3NVLEQGSujfM6ztNxEZGmrFhV57eFkzwIHVHUqq9pfmpAAYVkGgrO1w==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-aws-lambda@0.41.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-/BLG+0DQr2tCILFGJKJH2Fg6eyjhqOlVflYpNddUEXnzyQ/PAhTdgirkqbICFgeSW2XYcEY9zXpuRldrVNw9cA==} + /@opentelemetry/instrumentation-aws-lambda@0.42.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-GhV3s62W8gWXDuCdPkWj60W3giHGadHoGBPGW5Wud2fUK9lY6FiYxv6AmCokzugTaiRfB2RjsaJWd9xTtYttVA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/propagator-aws-xray': 1.3.1(@opentelemetry/api@1.8.0) '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 @@ -2267,277 +2550,275 @@ packages: - supports-color dev: false - /@opentelemetry/instrumentation-aws-sdk@0.41.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-7+8WMY0LQeqv6KIObXK+Py44qNFLeCU0ZLLxSZtXEbZ2wJlQISP1St65jRto0NV7isnZoyuOxb2+ZpypPPNv7Q==} + /@opentelemetry/instrumentation-aws-sdk@0.42.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-6b4LQAeBSKU5RhKEP9rH+wMcKswlllIT9J65uREmnWQQJo5zogD6cWa2sJ814o9K25/aDi+zheVHDFDuA7iVCQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/propagation-utils': 0.30.9(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/propagation-utils': 0.30.10(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-bunyan@0.38.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-ThNcgTE22W7PKzTzz5qfGxb5Gf7rA3EORousYo2nJWHHcF6gqiMNv2+GXY3MdpjLBr8IgCfhtvbQdD6rlIPUpA==} + /@opentelemetry/instrumentation-bunyan@0.39.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-AQ845Wh5Yhd7S0argkCd1vrThNo4q/p6LJePC4OlFifPa9i5O2MzfLNh4mo8YWa0rYvcc+jbhodkGNa+1YJk/A==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/api-logs': 0.51.1 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/api-logs': 0.52.0 + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@types/bunyan': 1.8.9 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-cassandra-driver@0.38.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-ML4Vw0it2uIpETfX6skuSIGLHF9D3TUKOfdfrk9lnrzzWSzg2aS6pl3UeepkQX4wXHdzlxVRB0USrUqsmxMd5Q==} + /@opentelemetry/instrumentation-cassandra-driver@0.39.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-D1p7zNNHQYI6/d0ulAFXe+71oDAgzxctfB0EICT8GpBhOCRlCW0U4rxRWrnZW6T5sJaBJqSsY4QF5CPqvCc00w==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-connect@0.36.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-xI5Q/CMmzBmHshPnzzjD19ptFaYO/rQWzokpNio4QixZYWhJsa35QgRvN9FhPkwgtuJIbt/CWWAufJ3egJNHEA==} + /@opentelemetry/instrumentation-connect@0.37.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-SeQktDIH5rNzjiEiazWiJAIXkmnLOnNV7wwHpahrqE0Ph+Z3heqMfxRtoMtbdJSIYLfcNZYO51AjxZ00IXufdw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 '@types/connect': 3.4.36 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-cucumber@0.6.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-90eAF2JPSbPAsOuGfYyctYaoYXqy4Clbxt0j/uUgg6dto4oqwUw3AvTyHQEztLGxeXwEzC1EQigDtVPg5ZexYA==} + /@opentelemetry/instrumentation-cucumber@0.7.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-bF9gpkUsDbg5Ii47PrhOzgCJKKrT0Tn0wfowOOgcW8PruqfuXgnQ9q1B6GGdSqtIaFnX3xFxGCyWcmf5emt64w==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-dataloader@0.9.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-fiyCOAw+tlbneok1x7P5UseoGW5nS60CWWx7NXzYW+WOexpSmDQQW7olttGa8fqE6/sVCoi1l+QdfVoETZi/NQ==} + /@opentelemetry/instrumentation-dataloader@0.10.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-yoAHGsgXx0YNFJ5XgCAgPo2Wr7Hy4IQX7YTcCulnKuxdfFXybsM9Yz7wiF9X2X2eB6HRLRJRufXT0sujbHaq1g==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-dns@0.36.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-NWRbQ7q0E3co/CNTWLZZvUzZoKhB1iTitY282IM8HDTXkA6VRssCfOcvaHw5ezOh23TJbAeYxmmpVj4hFvDPYQ==} + /@opentelemetry/instrumentation-dns@0.37.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-vhIOqqUGq1qwSKS6mF9tpXP7GmVQpQK4zm7bn2UYModpm+YYQzghtf/D8JH6lxXyUMP40zA37xUd2HO6uze/dw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 - semver: 7.5.4 + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) + semver: 7.6.2 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-document-load@0.38.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-X/AOG8sDcVp/bVGRWDDG7MCRjcmuQwZqG2B2C6/oj8V4koXPNRNDvW2GEIGJhF5/WxJxZsTRIGPG+yeJ52QOww==} + /@opentelemetry/instrumentation-document-load@0.39.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-M8QTHM1fFoJvQ1EYaxAF7V5RJhG4c+o4gWHLSFQl6dvQJuGiSdhM3azenRFcTe88Sn6AmVYRGiUjlac9GSVQ2g==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/sdk-trace-base': 1.21.0(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-base': 1.24.1(@opentelemetry/api@1.8.0) '@opentelemetry/sdk-trace-web': 1.24.1(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-express@0.39.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-AG8U7z7D0JcBu/7dDcwb47UMEzj9/FMiJV2iQZqrsZnxR3FjB9J9oIH2iszJYci2eUdp2WbdvtpD9RV/zmME5A==} + /@opentelemetry/instrumentation-express@0.40.1(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-+RKMvVe2zw3kIXRup9c1jFu3T4d0fs5aKy015TpiMyoCKX1UMu3Z0lfgYtuyiSTANvg5hZnDbWmQmqSPj9VTvg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-fastify@0.36.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-3Nfm43PI0I+3EX+1YbSy6xbDu276R1Dh1tqAk68yd4yirnIh52Kd5B+nJ8CgHA7o3UKakpBjj6vSzi5vNCzJIA==} + /@opentelemetry/instrumentation-fastify@0.37.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-WRjwzNZgupSzbEYvo9s+QuHJRqZJjVdNxSEpGBwWK8RKLlHGwGVAu0gcc2gPamJWUJsGqPGvahAPWM18ZkWj6A==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-fetch@0.51.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-LzciqAnJmmYaXWo2hlrN99NMbYbExo6n6lBKBeMHAi7X/ddhCSOsgSNVbF3kDB7P++PJhjYqLT3hy6SU4AFHcg==} + /@opentelemetry/instrumentation-fetch@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-ay1Ot0z/586MBnhZnWJJFWXjBCQjddVVjCxLPRECnorhzmXuOsjUb7zTY88Vv9ddRtcHe0EIp9Z8sWQeLT02kA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/sdk-trace-web': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-web': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/semantic-conventions': 1.25.0 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-fs@0.12.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-Waf+2hekJRxIwq1PmivxOWLdMOtYbY22hKr34gEtfbv2CArSv8FBJH4BmQxB9o5ZcwkdKu589qs009dbuSfNmQ==} + /@opentelemetry/instrumentation-fs@0.13.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-sZxofhMkul95/Rb4R/Q1eP8mIpgWX8dXNCAOk1jMzl/I8xPJ5tnPgT+PIInPSiDh3kgZDTxK5Up1zMnUh0XqSg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-generic-pool@0.36.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-CExAEqJvK8jYxrhN8cl6EaGg57EGJi+qsSKouLC5lndXi68gZLOKbZIMZg4pF0kNfp/D4BFaGmA6Ap7d5WoPTw==} + /@opentelemetry/instrumentation-generic-pool@0.37.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-l3VivYfu+FRw0/hHu2jlFLz4mfxZrOg4r96usDF5dJgDRQrRUmjtq6xssYGuFKn1FXAfN8Rcn1Tdk/c40PNYEA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-graphql@0.40.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-LVRdEHWACWOczv2imD+mhUrLMxsEjPPi32vIZJT57zygR5aUiA4em8X3aiGOCycgbMWkIu8xOSGSxdx3JmzN+w==} + /@opentelemetry/instrumentation-graphql@0.41.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-R/gXeljgIhaRDKquVkKYT5QHPnFouM8ooyePZEP0kqyaVAedtR1V7NfAUJbxfTG5fBQa5wdmLjvu63+tzRXZCA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-grpc@0.51.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-coRTugFL7De/VNH/1NqPlxnfik87jS+jBXsny+Y/lMhXIA3x8t71IyL9ihuewkD+lNtIxIz6Y7Sq6kPuOqz5dQ==} + /@opentelemetry/instrumentation-grpc@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-YYhA2pbhMWgF5Hp6eR7AHp1utzZQ3Y0VB8GIwd8zJoLtAuQRZa1N29DUtZ+t/pGRJF+xGPVI+vP+7ugHgeN0zQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/semantic-conventions': 1.25.0 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-hapi@0.38.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-ZcOqEuwuutTDYIjhDIStix22ECblG/i9pHje23QGs4Q4YS4RMaZ5hKCoQJxW88Z4K7T53rQkdISmoXFKDV8xMg==} + /@opentelemetry/instrumentation-hapi@0.39.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-ik2nA9Yj2s2ay+aNY+tJsKCsEx6Tsc2g/MK0iWBW5tibwrWKTy1pdVt5sB3kd5Gkimqj23UV5+FH2JFcQLeKug==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-http@0.51.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-6b3nZnFFEz/3xZ6w8bVxctPUWIPWiXuPQ725530JgxnN1cvYFd8CJ75PrHZNjynmzSSnqBkN3ef4R9N+RpMh8Q==} + /@opentelemetry/instrumentation-http@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-E6ywZuxTa4LnVXZGwL1oj3e2Eog1yIaNqa8KjKXoGkDNKte9/SjQnePXOmhQYI0A9nf0UyFbP9aKd+yHrkJXUA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 - semver: 7.5.4 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/semantic-conventions': 1.25.0 + semver: 7.6.2 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-ioredis@0.40.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-Jv/fH7KhpWe4KBirsiqeUJIYrsdR2iu2l4nWhfOlRvaZ+zYIiLEzTQR6QhBbyRoAbU4OuYJzjWusOmmpGBnwng==} + /@opentelemetry/instrumentation-ioredis@0.41.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-rxiLloU8VyeJGm5j2fZS8ShVdB82n7VNP8wTwfUQqDwRfHCnkzGr+buKoxuhGD91gtwJ91RHkjHA1Eg6RqsUTg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/redis-common': 0.36.2 '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-knex@0.36.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-6bEuiI+yMf3D0+ZWZE2AKmXhIhBvZ0brdO/0A8lUqeqeS+sS4fTcjA1F2CclsCNxYWEgcs8o3QyQqPceBeVRlg==} + /@opentelemetry/instrumentation-knex@0.37.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-NyXHezcUYiWnzhiY4gJE/ZMABnaC7ZQUCyx7zNB4J9Snmc4YCsRbLpTkJmCLft3ey/8Qg1Un+6efZcpgthQqbg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-koa@0.40.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-dJc3H/bKMcgUYcQpLF+1IbmUKus0e5Fnn/+ru/3voIRHwMADT3rFSUcGLWSczkg68BCgz0vFWGDTvPtcWIFr7A==} + /@opentelemetry/instrumentation-koa@0.41.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-mbPnDt7ELvpM2S0vixYUsde7122lgegLOJQxx8iJQbB8YHal/xnTh9v7IfArSVzIDo+E+080hxZyUZD4boOWkw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 '@types/koa': 2.14.0 '@types/koa__router': 12.0.3 @@ -2545,122 +2826,122 @@ packages: - supports-color dev: false - /@opentelemetry/instrumentation-lru-memoizer@0.37.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-dHLrn55qVWsHJQYdForPWPUWDk2HZ2jjzkT+WoQSqpYT1j4HxfoiLfBTF+I3EbEYFAJnDRmRAUfA6nU5GPdCLQ==} + /@opentelemetry/instrumentation-lru-memoizer@0.38.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-x41JPoCbltEeOXlHHVxHU6Xcd/91UkaXHNIqj8ejfp9nVQe0lFHBJ8wkUaVJlasu60oEPmiz6VksU3Wa42BrGw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-memcached@0.36.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-5efkT8ZfN8il5z+yfKYFGm2YR3mhlhaJoGfNOAylKE/6tUH3WDTTWaP7nrURtWGc+fuvDktcEch18Se8qsGS7w==} + /@opentelemetry/instrumentation-memcached@0.37.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-30mEfl+JdeuA6m7GRRwO6XYkk7dj4dp0YB70vMQ4MS2qBMVQvkEu3Gb+WFhSHukTYv753zyBeohDkeXw7DEsvw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 '@types/memcached': 2.2.10 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-mongodb@0.43.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-bMKej7Y76QVUD3l55Q9YqizXybHUzF3pujsBFjqbZrRn2WYqtsDtTUlbCK7fvXNPwFInqZ2KhnTqd0gwo8MzaQ==} + /@opentelemetry/instrumentation-mongodb@0.45.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-xnZP9+ayeB1JJyNE9cIiwhOJTzNEsRhXVdLgfzmrs48Chhhk026mQdM5CITfyXSCfN73FGAIB8d91+pflJEfWQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/sdk-metrics': 1.21.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-metrics': 1.24.1(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-mongoose@0.38.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-zaeiasdnRjXe6VhYCBMdkmAVh1S5MmXC/0spet+yqoaViGnYst/DOxPvhwg3yT4Yag5crZNWsVXnA538UjP6Ow==} + /@opentelemetry/instrumentation-mongoose@0.39.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-J1r66A7zJklPPhMtrFOO7/Ud2p0Pv5u8+r23Cd1JUH6fYPmftNJVsLp2urAt6PHK4jVqpP/YegN8wzjJ2mZNPQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-mysql2@0.38.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-qkpHMgWSDTYVB1vlZ9sspf7l2wdS5DDq/rbIepDwX5BA0N0068JTQqh0CgAh34tdFqSCnWXIhcyOXC2TtRb0sg==} + /@opentelemetry/instrumentation-mysql2@0.39.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-Iypuq2z6TCfriAXCIZjRq8GTFCKhQv5SpXbmI+e60rYdXw8NHtMH4NXcGF0eKTuoCsC59IYSTUvDQYDKReaszA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.8.0) transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-mysql@0.38.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-+iBAawUaTfX/HAlvySwozx0C2B6LBfNPXX1W8Z2On1Uva33AGkw2UjL9XgIg1Pj4eLZ9R4EoJ/aFz+Xj4E/7Fw==} + /@opentelemetry/instrumentation-mysql@0.39.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-8snHPh83rhrDf31v9Kq0Nf+ts8hdr7NguuszRqZomZBHgE0+UyXZSkXHAAFZoBPPRMGyM68uaFE5hVtFl+wOcA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 '@types/mysql': 2.15.22 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-nestjs-core@0.37.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-ebYQjHZEmGHWEALwwDGhSQVLBaurFnuLIkZD5igPXrt7ohfF4lc5/4al1LO+vKc0NHk8SJWStuRueT86ISA8Vg==} + /@opentelemetry/instrumentation-nestjs-core@0.38.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-M381Df1dM8aqihZz2yK+ugvMFK5vlHG/835dc67Sx2hH4pQEQYDA2PpFPTgc9AYYOydQaj7ClFQunESimjXDgg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-net@0.36.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-rZlbSgwAJys8lpug+xIeAdO98ypYMAPVqrHqc4AHuUl5S4MULHEcjGLMZLoE/guEGO4xAQ5XUezpRFGM1SAnsg==} + /@opentelemetry/instrumentation-net@0.37.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-kLTnWs4R/FtNDvJC7clS7/tBzK7I8DH5IV1I8abog4/1fHh/CFiwWeTRlPlREwcGfVJyL95pDX2Utjviybr5Dg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-pg@0.41.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-BSlhpivzBD77meQNZY9fS4aKgydA8AJBzv2dqvxXFy/Hq64b7HURgw/ztbmwFeYwdF5raZZUifiiNSMLpOJoSA==} + /@opentelemetry/instrumentation-pg@0.42.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-sjgcM8CswYy8zxHgXv4RAZ09DlYhQ+9TdlourUs63Df/ek5RrB1ZbjznqW7PB6c3TyJJmX6AVtPTjAsROovEjA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.8.0) '@types/pg': 8.6.1 @@ -2669,153 +2950,153 @@ packages: - supports-color dev: false - /@opentelemetry/instrumentation-pino@0.39.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-uA17F2iP77o3NculB63QD2zv3jkJ093Gfb0GxHLEqTIqpYs1ToJ53ybWwjJwqFByxk7GrliaxaxVtWC23PKzBg==} + /@opentelemetry/instrumentation-pino@0.40.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-29B7mpabiB5m9YeVuUpWNceKv2E2semh44Y0EngFn7Z/Dwg13j+jsD3h6RaLPLUmUynWKSa160jZm0XrWbx40w==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-redis-4@0.39.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-Zpfqfi83KeKgVQ0C2083GZPon3ZPYQ5E59v9FAbhubtOoUb9Rh7n111YD8FPW3sgx6JKp1odXmBmfQhWCaTOpQ==} + /@opentelemetry/instrumentation-redis-4@0.40.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-0ieQYJb6yl35kXA75LQUPhHtGjtQU9L85KlWa7d4ohBbk/iQKZ3X3CFl5jC5vNMq/GGPB3+w3IxNvALlHtrp7A==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/redis-common': 0.36.2 '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-redis@0.39.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-HUjTerD84jRJnSyDrRPqn6xQ7K91o9qLflRPZqzRvq0GRj5PMfc6TJ/z3q/ayWy/2Kzffhrp7HCIVp0u0TkgUg==} + /@opentelemetry/instrumentation-redis@0.40.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-vf2EwBrb979ztLMbf8ew+65ECP3yMxeFwpMLu9KjX6+hFf1Ng776jlM2H9GeP1YePbvoBB5Jbo0MBU6Y0HEgzA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/redis-common': 0.36.2 '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-restify@0.38.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-VYK47Z9GBaZX5MQLL7kZDdzQDdyUtHRD4J/GSr6kdwmIpdpUQXLsV3EnboeB8P+BlpucF57FyJKE8yWTOEMfnA==} + /@opentelemetry/instrumentation-restify@0.39.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-+KDpaGvJLW28LYoT3AZAEVnywzy8dGS+wTWirXU6edKXu4w5mwdxui3UB3Vy/+FV7gbMWidzedaihTDlQvZXRA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-router@0.37.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-+OPcm7C9I5oPqnpStE+1WkdPWjRx0k5XKratxQmIDFZrmhRcqvMte3vrrzE/OBPg9iqh2tKrSe0y7+0sRfTJyQ==} + /@opentelemetry/instrumentation-router@0.38.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-HMeeBva/rqIqg/KHzmKcvutK4JS90Sk59i4qCnLhHW57CMVruj18aXEpBT+QMVJRjmzrvhkJnIpNcPu5vglmRg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-socket.io@0.39.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-4J2ehk5mJyDT6j2yJCOuPxAjit5QB1Fwzhx0LID5jjvhI9LxzZIGDNAPTTHyghSiaRDeNMzceXKkkEQJkg2MNw==} + /@opentelemetry/instrumentation-socket.io@0.40.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-BJFMytiHnvKM7n6n67pT9eTBGpZetY+LHic8UKrIQ313uBp+MBbRyqiJY6dT4bcN1B6sl47JzCyKmVprSuSnBA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-tedious@0.10.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-maSXMxgS0szU52khQzAROV4nWr+3M8mZajMQOc3/7tYjo+Q3HlWAowOuagPvp4pwROK4x6oDaFYlY+ZSj1qjYA==} + /@opentelemetry/instrumentation-tedious@0.11.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-Dh93CyaR7vldKf0oXwtYlSEdqvMGUTv270N0YGBQtODPKtgIMr9816vIA7cJPCZ4SbbREgLNQJfbh0qeadAM4Q==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 '@types/tedious': 4.0.14 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-undici@0.2.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-RH9WdVRtpnyp8kvya2RYqKsJouPxvHl7jKPsIfrbL8u2QCKloAGi0uEqDHoOS15ZRYPQTDXZ7d8jSpUgSQmvpA==} + /@opentelemetry/instrumentation-undici@0.3.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-LMbOE4ofjpQyZ3266Ah6XL9JIBaShebLN0aaZPvqXozKPu41rHmggO3qk0H+Unv8wbiUnHgYZDvq8yxXyKAadg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.7.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-user-interaction@0.38.0(@opentelemetry/api@1.8.0)(zone.js@0.11.8): - resolution: {integrity: sha512-/UZT7zZUpi3WavRW6GmxKSa3d3PQ1ApM9nG9PKq95d4w/zhXBaYiqGT/wzcyXefW4TL2oAq4sjJvt1rZpOlImA==} + /@opentelemetry/instrumentation-user-interaction@0.39.0(@opentelemetry/api@1.8.0)(zone.js@0.14.7): + resolution: {integrity: sha512-hO/D1g8/P1fo5lEEHpxHHnxXDRfysjBZdFGwR6oSIoZWAQW1EasxZRe2mYDnxc37vEZsFFxTWXayH+XMKOo/sw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 zone.js: ^0.11.4 || ^0.13.0 || ^0.14.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) '@opentelemetry/sdk-trace-web': 1.24.1(@opentelemetry/api@1.8.0) - zone.js: 0.11.8 + zone.js: 0.14.7 transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-winston@0.37.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-vOx55fxdNjo2XojJf8JN4jP7VVvQCh7UQzzQ2Q2FpGJpt8Z3EErKaY8xOBkOuJH0TtL/Q72rmIn9c+mRG46BxA==} + /@opentelemetry/instrumentation-winston@0.38.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-rBAoVkv5HGyKFIpM3Xy5raPNJ/Le1JsAFPbxwbfOZUxpLT2YBB99h/jJYsHm+eNueJ7EBwz2ftqY8rEpVlk3XA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/api-logs': 0.51.1 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/api-logs': 0.52.0 + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) transitivePeerDependencies: - supports-color dev: false - /@opentelemetry/instrumentation-xml-http-request@0.51.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-CrQZxADNFr9M3aCgjM3/KXDz12lBrZA5LW+btfgNb78hsLNwqxThtFvXOHr86UsOzJt+h9m4yDeH6YLEvCTBbw==} + /@opentelemetry/instrumentation-xml-http-request@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-Q6dEFKBkVmLs6XByXNPkCZXYF1Ovs3fFCD33nA4d4dgBgv8zMPt7xBLIxfEw0QVDZhyBhKXwv7byvMDB+yYQdA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/sdk-trace-web': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-web': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/semantic-conventions': 1.25.0 transitivePeerDependencies: - supports-color dev: false @@ -2837,6 +3118,23 @@ packages: - supports-color dev: false + /@opentelemetry/instrumentation@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-LPwSIrw+60cheWaXsfGL8stBap/AppKQJFE+qqRvzYrgttXFH2ofoIMxWadeqPTq4BYOXM/C7Bdh/T+B60xnlQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/api-logs': 0.52.0 + '@types/shimmer': 1.0.5 + import-in-the-middle: 1.8.0 + require-in-the-middle: 7.2.0 + semver: 7.6.2 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + dev: false + /@opentelemetry/otlp-exporter-base@0.51.1(@opentelemetry/api@1.8.0): resolution: {integrity: sha512-UYlnOYyDdzo1Gw559EHCzru0RwhvuXCwoH8jGo9J4gO1TE58GjnEmIjomMsKBCym3qWNJfIQXw+9SZCV0DdQNg==} engines: {node: '>=14'} @@ -2847,6 +3145,17 @@ packages: '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) dev: false + /@opentelemetry/otlp-exporter-base@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-rlyg5UKW9yMTNMUxYYib9XxEE/krpH7Q6mIuJNOBMbjLwmqe1WQ2MNKNzobVZTKop/FX4CvyNN3wUEl/6gnvfw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/otlp-transformer': 0.52.0(@opentelemetry/api@1.8.0) + dev: false + /@opentelemetry/otlp-grpc-exporter-base@0.51.1(@opentelemetry/api@1.8.0): resolution: {integrity: sha512-ZAS+4pq8o7dsugGTwV9s6JMKSxi+guIHdn0acOv0bqj26e9pWDFx5Ky+bI0aY46uR9Y0JyXqY+KAEYM/SO3DFA==} engines: {node: '>=14'} @@ -2860,6 +3169,19 @@ packages: protobufjs: 7.2.6 dev: false + /@opentelemetry/otlp-grpc-exporter-base@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-iVq3wCElOoKUkxBOuvV8cfaELG8WO/zfLWIZil6iw/6hj6rktLodnJ7kVOneVmLki7Po1BjE1K7JOp2G3UPgYg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@grpc/grpc-js': 1.9.14 + '@opentelemetry/api': 1.8.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/otlp-exporter-base': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/otlp-transformer': 0.52.0(@opentelemetry/api@1.8.0) + dev: false + /@opentelemetry/otlp-proto-exporter-base@0.51.1(@opentelemetry/api@1.8.0): resolution: {integrity: sha512-gxxxwfk0inDMb5DLeuxQ3L8TtptxSiTNHE4nnAJH34IQXAVRhXSXW1rK8PmDKDngRPIZ6J7ncUCjjIn8b+AgqQ==} engines: {node: '>=14'} @@ -2887,8 +3209,24 @@ packages: '@opentelemetry/sdk-trace-base': 1.24.1(@opentelemetry/api@1.8.0) dev: false - /@opentelemetry/propagation-utils@0.30.9(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-DP2Y91zyw2uNgKLbej6c3IIjyF27sKnRK/UY/6msMIVGPIbZgtH9L0JOioN5L5kYjEkH4CDvt921SjutN7hY4A==} + /@opentelemetry/otlp-transformer@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-40acy3JxCAqQYcYepypF/64GVB8jerC6Oiz1HRUXxiSTVwg+ud7UtywfOkPRpc9bjHiyJouWxTjiUPQ9VBMKbg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/api-logs': 0.52.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/resources': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-logs': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-metrics': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-base': 1.25.0(@opentelemetry/api@1.8.0) + protobufjs: 7.3.2 + dev: false + + /@opentelemetry/propagation-utils@0.30.10(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-hhTW8pFp9PSyosYzzuUL9rdm7HF97w3OCyElufFHyUnYnKkCBbu8ne2LyF/KSdI/xZ81ubxWZs78hX4S7pLq5g==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -2903,7 +3241,7 @@ packages: '@opentelemetry/api': ^1.0.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) dev: false /@opentelemetry/propagator-b3@1.24.1(@opentelemetry/api@1.8.0): @@ -2916,6 +3254,16 @@ packages: '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) dev: false + /@opentelemetry/propagator-b3@1.25.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-/A+1Tbnf0uwnP51OkoaQlrb9YILdHsoqIISna1MNXpZRzf42xm6LVLb49i+m/zlJoW1e8P4ekcrditR5pfmwog==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + dev: false + /@opentelemetry/propagator-jaeger@1.24.1(@opentelemetry/api@1.8.0): resolution: {integrity: sha512-7bRBJn3FG1l195A1m+xXRHvgzAOBsfmRi9uZ5Da18oTh7BLmNDiA8+kpk51FpTsU1PCikPVpRDNPhKVB6lyzZg==} engines: {node: '>=14'} @@ -2926,13 +3274,23 @@ packages: '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) dev: false + /@opentelemetry/propagator-jaeger@1.25.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-uwA5xqaPISXeX+YutqbjmzENnCGCvrIXlqIXP5gRoA5N6S3W28p+ExL77TugMKHN5gXklapF67jDfz7lq5ETzQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + dev: false + /@opentelemetry/redis-common@0.36.2: resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} engines: {node: '>=14'} dev: false - /@opentelemetry/resource-detector-alibaba-cloud@0.28.9(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-cTV2YFFkKAZUZgs5SMknIX4MmFb/0KQhrJuiz2dtJKnI1n7OanCgnMkuXzJ5+CbifRB57I2g3HnwcSPOx3zsKw==} + /@opentelemetry/resource-detector-alibaba-cloud@0.28.10(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-TZv/1Y2QCL6sJ+X9SsPPBXe4786bc/Qsw0hQXFsNTbJzDTGGUmOAlSZ2qPiuqAd4ZheUYfD+QA20IvAjUz9Hhg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -2942,20 +3300,20 @@ packages: '@opentelemetry/semantic-conventions': 1.24.1 dev: false - /@opentelemetry/resource-detector-aws@1.5.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-JNk/kSzzNQaiMo/F0b/bm8S3Qtr/m89BckN9B4U/cPHSqKLdxX03vgRBOqkXJ5KlAD8kc6K1Etcr8QfvGw6+uA==} + /@opentelemetry/resource-detector-aws@1.5.1(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-+IUh4gAwJf49vOJM6PIjmgOapRH5zr21ZpFnNU0QZmxRi52AXVhZN7A89pKW6GAQheWnVQLD7iUN87ieYt70tw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 dev: false - /@opentelemetry/resource-detector-azure@0.2.7(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-+R3VnPaK6rc+kKfdvhgQlYDGXy0+JMAjPNDjcRQSeXY8pVOzHGCIrY+gT6gUrpjsw8w1EgNBVofr+qeNOr+o4A==} + /@opentelemetry/resource-detector-azure@0.2.9(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-16Z6kyrmszoa7J1uj1kbSAgZuk11K07yEDj6fa3I9XBf8Debi8y4K8ex94kpxbCfEraWagXji3bCWvaq3k4dRg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -2965,8 +3323,8 @@ packages: '@opentelemetry/semantic-conventions': 1.24.1 dev: false - /@opentelemetry/resource-detector-container@0.3.9(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-kfJ78av51EKk09fn5cwe5UNt+G7UBLvPTmfK/nZzvmNs7enw/TGB8X0j0JUHb9487ypRGph6MBoeP1+qZh+w1A==} + /@opentelemetry/resource-detector-container@0.3.11(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-22ndMDakxX+nuhAYwqsciexV8/w26JozRUV0FN9kJiqSWtA1b5dCVtlp3J6JivG5t8kDN9UF5efatNnVbqRT9Q==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -2976,14 +3334,14 @@ packages: '@opentelemetry/semantic-conventions': 1.24.1 dev: false - /@opentelemetry/resource-detector-gcp@0.29.9(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-rTUm0U0cF8f75JzeMpMLbQ4m1uLph+Q31DQKk8ekdDe6SZ1EPD4rM1JgRnbxZtsC2sE8ju87s5nEio77xPz7dQ==} + /@opentelemetry/resource-detector-gcp@0.29.10(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-rm2HKJ9lsdoVvrbmkr9dkOzg3Uk0FksXNxvNBgrCprM1XhMoJwThI5i0h/5sJypISUAJlEeJS6gn6nROj/NpkQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.8.0) '@opentelemetry/semantic-conventions': 1.24.1 gcp-metadata: 6.1.0 @@ -2992,26 +3350,26 @@ packages: - supports-color dev: false - /@opentelemetry/resources@1.21.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-1Z86FUxPKL6zWVy2LdhueEGl9AHDJcx+bvHStxomruz6Whd02mE3lNUMjVJ+FGRoktx/xYQcxccYb03DiUP6Yw==} + /@opentelemetry/resources@1.24.1(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-cyv0MwAaPF7O86x5hk3NNgenMObeejZFLJJDVuSeSMIsknlsj3oOZzRv3qSzlwYomXsICfBeFFlxwHQte5mGXQ==} engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.8.0' + '@opentelemetry/api': '>=1.0.0 <1.9.0' dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.21.0 + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/semantic-conventions': 1.24.1 dev: false - /@opentelemetry/resources@1.24.1(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-cyv0MwAaPF7O86x5hk3NNgenMObeejZFLJJDVuSeSMIsknlsj3oOZzRv3qSzlwYomXsICfBeFFlxwHQte5mGXQ==} + /@opentelemetry/resources@1.25.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-iHjydPMYJ+Li1auveJCq2rp5U2h6Mhq8BidiyE0jfVlDTFyR1ny8AfJHfmFzJ/RAM8vT8L7T21kcmGybxZC7lQ==} engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.9.0' + '@opentelemetry/api': '>=1.0.0 <1.10.0' dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/semantic-conventions': 1.25.0 dev: false /@opentelemetry/sdk-logs@0.51.1(@opentelemetry/api-logs@0.51.1)(@opentelemetry/api@1.8.0): @@ -3027,16 +3385,16 @@ packages: '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.8.0) dev: false - /@opentelemetry/sdk-metrics@1.21.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-on1jTzIHc5DyWhRP+xpf+zrgrREXcHBH4EDAfaB5mIG7TWpKxNXooQ1JCylaPsswZUv4wGnVTinr4HrBdGARAQ==} + /@opentelemetry/sdk-logs@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-Dp6g7w7WglrDZMn2yHBMAKRGqQy8C0PUbFovkSwcSsmL47n4FSEc3eeGblZTtueOUW+rTsPJpLHoUpEdS0Wibw==} engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.8.0' + '@opentelemetry/api': '>=1.4.0 <1.10.0' dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/resources': 1.21.0(@opentelemetry/api@1.8.0) - lodash.merge: 4.6.2 + '@opentelemetry/api-logs': 0.52.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/resources': 1.25.0(@opentelemetry/api@1.8.0) dev: false /@opentelemetry/sdk-metrics@1.24.1(@opentelemetry/api@1.8.0): @@ -3051,6 +3409,18 @@ packages: lodash.merge: 4.6.2 dev: false + /@opentelemetry/sdk-metrics@1.25.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-IF+Sv4VHgBr/BPMKabl+GouJIhEqAOexCHgXVTISdz3q9P9H/uA8ScCF+22gitQ69aFtESbdYOV+Fen5+avQng==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/resources': 1.25.0(@opentelemetry/api@1.8.0) + lodash.merge: 4.6.2 + dev: false + /@opentelemetry/sdk-node@0.51.1(@opentelemetry/api@1.8.0): resolution: {integrity: sha512-GgmNF9C+6esr8PIJxCqHw84rEOkYm6XdFWZ2+Wyc3qaUt92ACoN7uSw5iKNvaUq62W0xii1wsGxwHzyENtPP8w==} engines: {node: '>=14'} @@ -3075,16 +3445,28 @@ packages: - supports-color dev: false - /@opentelemetry/sdk-trace-base@1.21.0(@opentelemetry/api@1.8.0): - resolution: {integrity: sha512-yrElGX5Fv0umzp8Nxpta/XqU71+jCAyaLk34GmBzNcrW43nqbrqvdPs4gj4MVy/HcTjr6hifCDCYA3rMkajxxA==} + /@opentelemetry/sdk-node@0.52.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-3RNnsoHGutya3oVsoc2WRrk/TKlxr+R2uN6H4boNJvW7kc8yxS4QrOI6DlbQYAgEMeC1PMu95jW9LirPOWcMGw==} engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.8.0' + '@opentelemetry/api': '>=1.3.0 <1.10.0' dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/resources': 1.21.0(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.21.0 + '@opentelemetry/api-logs': 0.52.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/exporter-trace-otlp-http': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/exporter-zipkin': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/instrumentation': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/resources': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-logs': 0.52.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-metrics': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-base': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-node': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/semantic-conventions': 1.25.0 + transitivePeerDependencies: + - supports-color dev: false /@opentelemetry/sdk-trace-base@1.24.1(@opentelemetry/api@1.8.0): @@ -3099,6 +3481,18 @@ packages: '@opentelemetry/semantic-conventions': 1.24.1 dev: false + /@opentelemetry/sdk-trace-base@1.25.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-6+g2fiRQUG39guCsKVeY8ToeuUf3YUnPkN6DXRA1qDmFLprlLvZm9cS6+chgbW70cZJ406FTtSCDnJwxDC5sGQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/resources': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/semantic-conventions': 1.25.0 + dev: false + /@opentelemetry/sdk-trace-node@1.24.1(@opentelemetry/api@1.8.0): resolution: {integrity: sha512-/FZX8uWaGIAwsDhqI8VvQ+qWtfMNlXjaFYGc+vmxgdRFppCSSIRwrPyIhJO1qx61okyYhoyxVEZAfoiNxrfJCg==} engines: {node: '>=14'} @@ -3114,6 +3508,21 @@ packages: semver: 7.5.4 dev: false + /@opentelemetry/sdk-trace-node@1.25.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-sYdZmNCkqthPpjwCxAJk5aQNLxCOQjT1u3JMGvO6rb3Ic8uFdnzXavP13Md9uYPcZBo+KxetyDhCf0x8wJGRng==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/context-async-hooks': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/propagator-b3': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/propagator-jaeger': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-base': 1.25.0(@opentelemetry/api@1.8.0) + semver: 7.6.2 + dev: false + /@opentelemetry/sdk-trace-web@1.24.1(@opentelemetry/api@1.8.0): resolution: {integrity: sha512-0w+aKRai9VREeo3VrtW+hcbrE2Fl/uKL7G+oXgRNf6pI9QLaEGuEzUTX+oxXVPBadzjOd+5dqCHYdX7UeVjzwA==} engines: {node: '>=14'} @@ -3126,9 +3535,16 @@ packages: '@opentelemetry/semantic-conventions': 1.24.1 dev: false - /@opentelemetry/semantic-conventions@1.21.0: - resolution: {integrity: sha512-lkC8kZYntxVKr7b8xmjCVUgE0a8xgDakPyDo9uSWavXPyYqLgYYGdEd2j8NxihRyb6UwpX3G/hFUF4/9q2V+/g==} + /@opentelemetry/sdk-trace-web@1.25.0(@opentelemetry/api@1.8.0): + resolution: {integrity: sha512-TAWRDRiVOeliE1A99z8idWb4pwEKKY9Vj5aTpLtrF4cvPOl0mPg3ZczvOw/HnpfRsWY0Ra/J3vS5uFSpoqPwEA==} engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.8.0 + '@opentelemetry/core': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/sdk-trace-base': 1.25.0(@opentelemetry/api@1.8.0) + '@opentelemetry/semantic-conventions': 1.25.0 dev: false /@opentelemetry/semantic-conventions@1.24.1: @@ -3136,6 +3552,11 @@ packages: engines: {node: '>=14'} dev: false + /@opentelemetry/semantic-conventions@1.25.0: + resolution: {integrity: sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ==} + engines: {node: '>=14'} + dev: false + /@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.8.0): resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} engines: {node: '>=14'} @@ -3143,7 +3564,7 @@ packages: '@opentelemetry/api': ^1.1.0 dependencies: '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.8.0) + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) dev: false /@peculiar/asn1-schema@2.3.8: @@ -3534,7 +3955,7 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true - /@sveltejs/adapter-node@4.0.1(@sveltejs/kit@2.5.8): + /@sveltejs/adapter-node@4.0.1(@sveltejs/kit@2.5.10): resolution: {integrity: sha512-IviiTtKCDp+0QoTmmMlGGZBA1EoUNsjecU6XGV9k62S3f01SNsVhpqi2e4nbI62BLGKh/YKKfFii+Vz/b9XIxg==} peerDependencies: '@sveltejs/kit': ^2.4.0 @@ -3542,12 +3963,12 @@ packages: '@rollup/plugin-commonjs': 25.0.7(rollup@4.9.6) '@rollup/plugin-json': 6.1.0(rollup@4.9.6) '@rollup/plugin-node-resolve': 15.2.3(rollup@4.9.6) - '@sveltejs/kit': 2.5.8(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.17)(vite@5.2.11) + '@sveltejs/kit': 2.5.10(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@4.2.17)(vite@5.2.13) rollup: 4.9.6 dev: true - /@sveltejs/kit@2.5.8(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.17)(vite@5.2.11): - resolution: {integrity: sha512-ZQXYaVHd1p0kDGwOi4l82i5kAiUQtrhMthDKtJi0zVzmNupKJ0ZlBVAoceuarCuIntPNctyQchW29h5DkFxd1Q==} + /@sveltejs/kit@2.5.10(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@4.2.17)(vite@5.2.13): + resolution: {integrity: sha512-OqoyTmFG2cYmCFAdBfW+Qxbg8m23H4dv6KqwEt7ofr/ROcfcIl3Z/VT56L22H9f0uNZyr+9Bs1eh2gedOCK9kA==} engines: {node: '>=18.13'} hasBin: true requiresBuild: true @@ -3556,53 +3977,53 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 vite: ^5.0.3 dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.0(svelte@4.2.17)(vite@5.2.11) + '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.17)(vite@5.2.13) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.0.0 esm-env: 1.0.0 - import-meta-resolve: 4.0.0 + import-meta-resolve: 4.1.0 kleur: 4.1.5 - magic-string: 0.30.6 + magic-string: 0.30.10 mrmime: 2.0.0 sade: 1.8.1 set-cookie-parser: 2.6.0 sirv: 2.0.4 svelte: 4.2.17 tiny-glob: 0.2.9 - vite: 5.2.11(@types/node@20.12.12) + vite: 5.2.13(@types/node@20.12.12) - /@sveltejs/vite-plugin-svelte-inspector@2.0.0(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.17)(vite@5.2.11): - resolution: {integrity: sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==} + /@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@4.2.17)(vite@5.2.13): + resolution: {integrity: sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==} engines: {node: ^18.0.0 || >=20} peerDependencies: '@sveltejs/vite-plugin-svelte': ^3.0.0 svelte: ^4.0.0 || ^5.0.0-next.0 vite: ^5.0.0 dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.0(svelte@4.2.17)(vite@5.2.11) + '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.17)(vite@5.2.13) debug: 4.3.4 svelte: 4.2.17 - vite: 5.2.11(@types/node@20.12.12) + vite: 5.2.13(@types/node@20.12.12) transitivePeerDependencies: - supports-color - /@sveltejs/vite-plugin-svelte@3.1.0(svelte@4.2.17)(vite@5.2.11): - resolution: {integrity: sha512-sY6ncCvg+O3njnzbZexcVtUqOBE3iYmQPJ9y+yXSkOwG576QI/xJrBnQSRXFLGwJNBa0T78JEKg5cIR0WOAuUw==} + /@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.17)(vite@5.2.13): + resolution: {integrity: sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A==} engines: {node: ^18.0.0 || >=20} peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 vite: ^5.0.0 dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 2.0.0(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.17)(vite@5.2.11) + '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@4.2.17)(vite@5.2.13) debug: 4.3.4 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.10 svelte: 4.2.17 svelte-hmr: 0.16.0(svelte@4.2.17) - vite: 5.2.11(@types/node@20.12.12) - vitefu: 0.2.5(vite@5.2.11) + vite: 5.2.13(@types/node@20.12.12) + vitefu: 0.2.5(vite@5.2.13) transitivePeerDependencies: - supports-color @@ -4090,14 +4511,13 @@ packages: wonka: 6.3.4 dev: true - /@vitejs/plugin-basic-ssl@1.1.0(vite@5.2.11): + /@vitejs/plugin-basic-ssl@1.1.0(vite@5.2.13): resolution: {integrity: sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==} engines: {node: '>=14.6.0'} peerDependencies: vite: ^3.0.0 || ^4.0.0 || ^5.0.0 dependencies: - vite: 5.2.11(@types/node@20.12.12) - dev: false + vite: 5.2.13(@types/node@20.12.12) /@vitest/expect@1.6.0: resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} @@ -4228,7 +4648,7 @@ packages: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} dependencies: - debug: 4.3.4 + debug: 4.3.5 transitivePeerDependencies: - supports-color @@ -4396,12 +4816,12 @@ packages: dependencies: dequal: 2.0.3 - /babel-plugin-precompile-intl@0.5.2(@babel/core@7.24.4): + /babel-plugin-precompile-intl@0.5.2(@babel/core@7.24.7): resolution: {integrity: sha512-sTXC+8+krOCP72euH47HqUZ0RAc/mcNA7UoX5joPb5J+dladwOwVcDQMWdv8MflGuWW0PkvAwXdQQWF+/L3Qxg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.4 + '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.22.5 '@formatjs/icu-messageformat-parser': 2.7.6 dev: false @@ -4493,16 +4913,6 @@ packages: dependencies: fill-range: 7.0.1 - /browserslist@4.22.3: - resolution: {integrity: sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001582 - electron-to-chromium: 1.4.653 - node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.22.3) - /browserslist@4.23.0: resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -4513,6 +4923,16 @@ packages: node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) + /browserslist@4.23.1: + resolution: {integrity: sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001633 + electron-to-chromium: 1.4.801 + node-releases: 2.0.14 + update-browserslist-db: 1.0.16(browserslist@4.23.1) + /bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: @@ -4585,12 +5005,12 @@ packages: engines: {node: '>=6'} dev: true - /caniuse-lite@1.0.30001582: - resolution: {integrity: sha512-vsJG3V5vgfduaQGVxL53uSX/HUzxyr2eA8xCo36OLal7sRcSZbibJtLeh0qja4sFOr/QQGt4opB4tOy+eOgAxg==} - /caniuse-lite@1.0.30001620: resolution: {integrity: sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==} + /caniuse-lite@1.0.30001633: + resolution: {integrity: sha512-6sT0yf/z5jqf8tISAgpJDrmwOpLsrpnyCdD/lOZKvKkkJK4Dn0X5i7KF7THEZhOq+30bmhwBlNEaqPUiHiKtZg==} + /capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} dependencies: @@ -4783,8 +5203,8 @@ packages: engines: {node: '>=0.8'} dev: true - /clsx@2.1.0: - resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} + /clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} dev: false @@ -5118,6 +5538,17 @@ packages: dependencies: ms: 2.1.2 + /debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + /decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -5326,12 +5757,12 @@ packages: semver: 7.5.4 dev: false - /electron-to-chromium@1.4.653: - resolution: {integrity: sha512-wA2A2LQCqnEwQAvwADQq3KpMpNwgAUBnRmrFgRzHnPhbQUFArTR32Ab46f4p0MovDLcg4uqd4nCsN2hTltslpA==} - /electron-to-chromium@1.4.772: resolution: {integrity: sha512-jFfEbxR/abTTJA3ci+2ok1NTuOBBtB4jH+UT6PUmRN+DY3WSD4FFRsgoVQ+QNIJ0T7wrXwzsWCI2WKC46b++2A==} + /electron-to-chromium@1.4.801: + resolution: {integrity: sha512-PnlUz15ii38MZMD2/CEsAzyee8tv9vFntX5nhtd2/4tv4HqY7C5q2faUAjmkXS/UFpVooJ/5H6kayRKYWoGMXQ==} + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -5399,6 +5830,10 @@ packages: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + /escape-goat@3.0.0: resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} engines: {node: '>=10'} @@ -5706,7 +6141,7 @@ packages: resolution: {integrity: sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==} dependencies: set-cookie-parser: 2.6.0 - tough-cookie: 4.1.3 + tough-cookie: 4.1.4 dev: false /figures@3.2.0: @@ -6101,7 +6536,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4 + debug: 4.3.5 transitivePeerDependencies: - supports-color dev: true @@ -6111,7 +6546,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4 + debug: 4.3.5 transitivePeerDependencies: - supports-color @@ -6141,8 +6576,8 @@ packages: engines: {node: '>= 4'} dev: true - /immer@10.0.4: - resolution: {integrity: sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==} + /immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} dev: false /immutable@3.7.6: @@ -6172,8 +6607,17 @@ packages: module-details-from-path: 1.0.3 dev: false - /import-meta-resolve@4.0.0: - resolution: {integrity: sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==} + /import-in-the-middle@1.8.0: + resolution: {integrity: sha512-/xQjze8szLNnJ5rvHSzn+dcVXqCAU6Plbk4P24U/jwPmg1wy7IIp9OjKIO5tYue8GSPhDpPDiApQjvBUmWwhsQ==} + dependencies: + acorn: 8.11.3 + acorn-import-attributes: 1.9.5(acorn@8.11.3) + cjs-module-lexer: 1.2.3 + module-details-from-path: 1.0.3 + dev: false + + /import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} @@ -7190,7 +7634,7 @@ packages: resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} dependencies: '@types/debug': 4.1.12 - debug: 4.3.4 + debug: 4.3.5 decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 @@ -8084,6 +8528,9 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + /picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -8349,6 +8796,25 @@ packages: long: 5.2.3 dev: false + /protobufjs@7.3.2: + resolution: {integrity: sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.12.12 + long: 5.2.3 + dev: false + /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} dev: false @@ -8568,8 +9034,8 @@ packages: glob: 10.3.10 dev: true - /rollup-plugin-svelte@7.2.0(rollup@2.79.1)(svelte@4.2.17): - resolution: {integrity: sha512-Qvo5VNFQZtaI+sHSjcCIFDP+olfKVyslAoJIkL3DxuhUpNY5Ys0+hhxUY3kuEKt9BXFgkFJiiic/XRb07zdSbg==} + /rollup-plugin-svelte@7.2.2(rollup@2.79.1)(svelte@4.2.17): + resolution: {integrity: sha512-hgnIblTRewaBEVQD6N0Q43o+y6q1TmDRhBjaEzQCi50bs8TXqjc+d1zFZyE8tsfgcfNHZQzclh4RxlFUB85H8Q==} engines: {node: '>=10'} peerDependencies: rollup: '>=2.0.0' @@ -8701,7 +9167,6 @@ packages: resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} engines: {node: '>=10'} hasBin: true - dev: true /sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} @@ -8959,8 +9424,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - /sveld@0.19.1(@babel/core@7.24.4)(postcss@8.4.38): - resolution: {integrity: sha512-ec4fRABGafRNASMdvyztLLlkjLBvBdlI7p3egd9HtIai1ThPwdu7eNn044ywKtzjtjRy0JIvaucHCfGt1YyP8Q==} + /sveld@0.20.0(@babel/core@7.24.7)(postcss@8.4.38): + resolution: {integrity: sha512-PQRs0evdOjsFzjHuPu8B/flqbJX2osN2D0++6nl77jhwriQazaEF0pkQAMwPRaSn/kMivvUdWkkPKi1NmHLosg==} hasBin: true dependencies: '@rollup/plugin-node-resolve': 13.3.0(rollup@2.79.1) @@ -8969,9 +9434,9 @@ packages: fast-glob: 3.3.2 prettier: 2.8.8 rollup: 2.79.1 - rollup-plugin-svelte: 7.2.0(rollup@2.79.1)(svelte@4.2.17) + rollup-plugin-svelte: 7.2.2(rollup@2.79.1)(svelte@4.2.17) svelte: 4.2.17 - svelte-preprocess: 5.1.4(@babel/core@7.24.4)(postcss@8.4.38)(svelte@4.2.17)(typescript@5.3.3) + svelte-preprocess: 5.1.4(@babel/core@7.24.7)(postcss@8.4.38)(svelte@4.2.17)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: - '@babel/core' @@ -8985,7 +9450,7 @@ packages: - sugarss dev: false - /svelte-check@3.7.1(@babel/core@7.24.4)(postcss@8.4.38)(svelte@4.2.17): + /svelte-check@3.7.1(@babel/core@7.24.7)(postcss@8.4.38)(svelte@4.2.17): resolution: {integrity: sha512-U4uJoLCzmz2o2U33c7mPDJNhRYX/DNFV11XTUDlFxaKLsO7P+40gvJHMPpoRfa24jqZfST4/G9fGNcUGMO8NAQ==} hasBin: true peerDependencies: @@ -8998,7 +9463,7 @@ packages: picocolors: 1.0.0 sade: 1.8.1 svelte: 4.2.17 - svelte-preprocess: 5.1.4(@babel/core@7.24.4)(postcss@8.4.38)(svelte@4.2.17)(typescript@5.3.3) + svelte-preprocess: 5.1.4(@babel/core@7.24.7)(postcss@8.4.38)(svelte@4.2.17)(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: - '@babel/core' @@ -9068,10 +9533,10 @@ packages: dependencies: svelte: 4.2.17 - /svelte-intl-precompile@0.12.3(@babel/core@7.24.4)(svelte@4.2.17): + /svelte-intl-precompile@0.12.3(@babel/core@7.24.7)(svelte@4.2.17): resolution: {integrity: sha512-/AA4io2O07h8PzDU8Jg5ab/DBDr6C2KOzyu4CWSsjHjm8pxWtkLDhd7XkNema34Ryl7O3oqjVrH8FsjLAEUXGQ==} dependencies: - babel-plugin-precompile-intl: 0.5.2(@babel/core@7.24.4) + babel-plugin-precompile-intl: 0.5.2(@babel/core@7.24.7) js-yaml: 4.1.0 json5: 2.2.3 path-starts-with: 2.0.1 @@ -9082,7 +9547,7 @@ packages: - svelte dev: false - /svelte-preprocess@5.1.4(@babel/core@7.24.4)(postcss@8.4.38)(svelte@4.2.17)(typescript@5.3.3): + /svelte-preprocess@5.1.4(@babel/core@7.24.7)(postcss@8.4.38)(svelte@4.2.17)(typescript@5.3.3): resolution: {integrity: sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==} engines: {node: '>= 16.0.0'} requiresBuild: true @@ -9120,10 +9585,10 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.24.4 + '@babel/core': 7.24.7 '@types/pug': 2.0.10 detect-indent: 6.1.0 - magic-string: 0.30.6 + magic-string: 0.30.10 postcss: 8.4.38 sorcery: 0.11.0 strip-indent: 3.0.0 @@ -9143,27 +9608,27 @@ packages: turnstile-types: 1.2.0 dev: true - /svelte-ux@0.62.12(@babel/core@7.24.4)(postcss@8.4.38)(svelte@4.2.17): - resolution: {integrity: sha512-lkcEEulsqjtJS2a9Vs8aPMqA/vBfSgjnEnkCYl2glbJTjfcfc9pnj6VkrOYvlUHntJF5m6MMKqQTMwe9P7w7kA==} + /svelte-ux@0.66.8(@babel/core@7.24.7)(postcss@8.4.38)(svelte@4.2.17): + resolution: {integrity: sha512-PaDLLSHnktN1uv/Viu8joHidvQ2UPh3AEB8JxeWgGH/GPwirYfa7jAferad+TMrv5Cvp9V124T559ySaLThxJg==} peerDependencies: - svelte: ^3.56.0 || ^4.0.0 + svelte: ^3.56.0 || ^4.0.0 || ^5.0.0-next.120 dependencies: '@floating-ui/dom': 1.6.5 - '@fortawesome/fontawesome-common-types': 6.5.1 + '@fortawesome/fontawesome-common-types': 6.5.2 '@mdi/js': 7.4.47 - clsx: 2.1.0 + clsx: 2.1.1 culori: 4.0.1 d3-array: 3.2.4 d3-scale: 4.0.2 date-fns: 3.6.0 - immer: 10.0.4 + immer: 10.1.1 lodash-es: 4.17.21 prism-svelte: 0.5.0 prism-themes: 1.9.0 prismjs: 1.29.0 - sveld: 0.19.1(@babel/core@7.24.4)(postcss@8.4.38) + sveld: 0.20.0(@babel/core@7.24.7)(postcss@8.4.38) svelte: 4.2.17 - tailwind-merge: 2.2.2 + tailwind-merge: 2.3.0 zod: 3.23.8 transitivePeerDependencies: - '@babel/core' @@ -9196,28 +9661,28 @@ packages: magic-string: 0.30.6 periscopic: 3.1.0 - /sveltekit-search-params@2.1.2(@sveltejs/kit@2.5.8)(svelte@4.2.17)(vite@5.2.11): + /sveltekit-search-params@2.1.2(@sveltejs/kit@2.5.10)(svelte@4.2.17)(vite@5.2.13): resolution: {integrity: sha512-wh5WSo46wz48MdWvpchVGrOjoDmbmsNJ7dUToSZ4L1SQ2LOasmTjAAlFhfG/EFvEhR34phRzLF7BjE0ZHzx1Uw==} peerDependencies: '@sveltejs/kit': ^1.0.0 || ^2.0.0 svelte: ^3.55.0 || ^4.0.0 || ^5.0.0 dependencies: - '@sveltejs/kit': 2.5.8(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.17)(vite@5.2.11) - '@sveltejs/vite-plugin-svelte': 3.1.0(svelte@4.2.17)(vite@5.2.11) + '@sveltejs/kit': 2.5.10(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@4.2.17)(vite@5.2.13) + '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.17)(vite@5.2.13) svelte: 4.2.17 transitivePeerDependencies: - supports-color - vite dev: false - /sveltekit-superforms@1.13.4(@sveltejs/kit@2.5.8)(svelte@4.2.17)(zod@3.23.8): + /sveltekit-superforms@1.13.4(@sveltejs/kit@2.5.10)(svelte@4.2.17)(zod@3.23.8): resolution: {integrity: sha512-rM2+Ictaw7OAIorCLmvg82orci/mtO9ZouI4emtx8SyYngx9aED+eNZlHPLufgB6D7geL2a+hMSFtM3zmMQixQ==} peerDependencies: '@sveltejs/kit': 1.x || 2.x svelte: 3.x || 4.x zod: 3.x dependencies: - '@sveltejs/kit': 2.5.8(@sveltejs/vite-plugin-svelte@3.1.0)(svelte@4.2.17)(vite@5.2.11) + '@sveltejs/kit': 2.5.10(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@4.2.17)(vite@5.2.13) devalue: 4.3.2 klona: 2.0.6 svelte: 4.2.17 @@ -9230,8 +9695,8 @@ packages: tslib: 2.6.2 dev: true - /tailwind-merge@2.2.2: - resolution: {integrity: sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==} + /tailwind-merge@2.3.0: + resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==} dependencies: '@babel/runtime': 7.24.1 dev: false @@ -9336,8 +9801,8 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - /tough-cookie@4.1.3: - resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + /tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} dependencies: psl: 1.9.0 @@ -9534,25 +9999,25 @@ packages: normalize-path: 2.1.1 dev: true - /update-browserslist-db@1.0.13(browserslist@4.22.3): + /update-browserslist-db@1.0.13(browserslist@4.23.0): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.22.3 + browserslist: 4.23.0 escalade: 3.1.1 - picocolors: 1.0.0 + picocolors: 1.0.1 - /update-browserslist-db@1.0.13(browserslist@4.23.0): - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + /update-browserslist-db@1.0.16(browserslist@4.23.1): + resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.23.0 - escalade: 3.1.1 - picocolors: 1.0.0 + browserslist: 4.23.1 + escalade: 3.1.2 + picocolors: 1.0.1 /upper-case-first@2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} @@ -9628,7 +10093,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.2.11(@types/node@20.12.12) + vite: 5.2.13(@types/node@20.12.12) transitivePeerDependencies: - '@types/node' - less @@ -9640,7 +10105,7 @@ packages: - terser dev: true - /vite-plugin-graphql-codegen@3.3.6(@graphql-codegen/cli@5.0.2)(graphql@16.8.1)(vite@5.2.11): + /vite-plugin-graphql-codegen@3.3.6(@graphql-codegen/cli@5.0.2)(graphql@16.8.1)(vite@5.2.13): resolution: {integrity: sha512-TXMaUpPCfqzSpujjzFjVeeCH9JOSBwFWxOJottZ+gouQtNhnNpgXcj4nZep3om5Wq0UlDwDYLqXWrAa8XaZW1w==} peerDependencies: '@graphql-codegen/cli': ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -9650,11 +10115,11 @@ packages: '@graphql-codegen/cli': 5.0.2(@types/node@20.12.12)(graphql@16.8.1)(typescript@5.3.3) '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.8.1) graphql: 16.8.1 - vite: 5.2.11(@types/node@20.12.12) + vite: 5.2.13(@types/node@20.12.12) dev: true - /vite@5.2.11(@types/node@20.12.12): - resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} + /vite@5.2.13(@types/node@20.12.12): + resolution: {integrity: sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -9688,7 +10153,7 @@ packages: optionalDependencies: fsevents: 2.3.3 - /vitefu@0.2.5(vite@5.2.11): + /vitefu@0.2.5(vite@5.2.13): resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} peerDependencies: vite: ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -9696,7 +10161,7 @@ packages: vite: optional: true dependencies: - vite: 5.2.11(@types/node@20.12.12) + vite: 5.2.13(@types/node@20.12.12) /vitest@1.6.0(@types/node@20.12.12): resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} @@ -9741,7 +10206,7 @@ packages: strip-literal: 2.1.0 tinybench: 2.6.0 tinypool: 0.8.4 - vite: 5.2.11(@types/node@20.12.12) + vite: 5.2.13(@types/node@20.12.12) vite-node: 1.6.0(@types/node@20.12.12) why-is-node-running: 2.2.2 transitivePeerDependencies: @@ -10008,6 +10473,10 @@ packages: tslib: 2.6.2 dev: false + /zone.js@0.14.7: + resolution: {integrity: sha512-0w6DGkX2BPuiK/NLf+4A8FLE43QwBfuqz2dVgi/40Rj1WmqUskCqj329O/pwrqFJLG5X8wkeG2RhIAro441xtg==} + dev: false + /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: false diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 79ed274d5..9616a464a 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -1,3 +1,3 @@ packages: - - 'frontend' + - 'https-proxy' - 'viewer' diff --git a/frontend/schema.graphql b/frontend/schema.graphql index ce9aa959f..ba45f077b 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -12,6 +12,11 @@ type AddProjectMemberPayload { errors: [AddProjectMemberError!] } +type AddProjectToOrgPayload { + organization: Organization + errors: [AddProjectToOrgError!] +} + type AlreadyExistsError implements Error { message: String! } @@ -32,6 +37,16 @@ type BulkAddProjectMembersResult { existingMembers: [UserProjectRole!]! } +type ChangeOrgMemberRolePayload { + organization: Organization + errors: [ChangeOrgMemberRoleError!] +} + +type ChangeOrgNamePayload { + organization: Organization + errors: [ChangeOrgNameError!] +} + type ChangeProjectDescriptionPayload { project: Project errors: [ChangeProjectDescriptionError!] @@ -107,6 +122,11 @@ type DeleteDraftProjectPayload { errors: [DeleteDraftProjectError!] } +type DeleteOrgPayload { + organization: Organization + errors: [DeleteOrgError!] +} + type DeleteUserByAdminOrSelfPayload { user: User errors: [DeleteUserByAdminOrSelfError!] @@ -176,7 +196,12 @@ type MeDto { type Mutation { createOrganization(input: CreateOrganizationInput!): CreateOrganizationPayload! + deleteOrg(input: DeleteOrgInput!): DeleteOrgPayload! @authorize(policy: "AdminRequiredPolicy") + addProjectToOrg(input: AddProjectToOrgInput!): AddProjectToOrgPayload! + removeProjectFromOrg(input: RemoveProjectFromOrgInput!): RemoveProjectFromOrgPayload! setOrgMemberRole(input: SetOrgMemberRoleInput!): SetOrgMemberRolePayload! + changeOrgMemberRole(input: ChangeOrgMemberRoleInput!): ChangeOrgMemberRolePayload! + changeOrgName(input: ChangeOrgNameInput!): ChangeOrgNamePayload! createProject(input: CreateProjectInput!): CreateProjectPayload! @authorize(policy: "VerifiedEmailRequiredPolicy") addProjectMember(input: AddProjectMemberInput!): AddProjectMemberPayload! bulkAddProjectMembers(input: BulkAddProjectMembersInput!): BulkAddProjectMembersPayload! @authorize(policy: "AdminRequiredPolicy") @@ -201,21 +226,34 @@ type NotFoundError implements Error { } type OrgMember { + user: User! + organization: Project! userId: UUID! orgId: UUID! role: OrgRole! - user: User - organization: Organization + id: UUID! + createdDate: DateTime! + updatedDate: DateTime! +} + +type OrgProjects { + org: Organization! + project: Project! + orgId: UUID! + projectId: UUID! id: UUID! createdDate: DateTime! updatedDate: DateTime! } type Organization { + createdDate: DateTime! name: String! members: [OrgMember!]! + projects: [Project!]! + memberCount: Int! + projectCount: Int! id: UUID! - createdDate: DateTime! updatedDate: DateTime! } @@ -234,6 +272,7 @@ type Project { type: ProjectType! isConfidential: Boolean flexProjectMetadata: FlexProjectMetadata + organizations: [Organization!]! lastCommit: DateTime deletedDate: DateTime resetStatus: ResetStatus! @@ -273,6 +312,7 @@ type ProjectUsers { type Query { myProjects(orderBy: [ProjectSortInput!]): [Project!]! projects(withDeleted: Boolean! = false where: ProjectFilterInput orderBy: [ProjectSortInput!]): [Project!]! @authorize(policy: "AdminRequiredPolicy") + myDraftProjects(orderBy: [DraftProjectSortInput!]): [DraftProject!]! draftProjects(where: DraftProjectFilterInput orderBy: [DraftProjectSortInput!]): [DraftProject!]! @authorize(policy: "AdminRequiredPolicy") projectById(projectId: UUID!): Project projectByCode(code: String!): Project @@ -287,6 +327,11 @@ type Query { isAdmin: IsAdminResponse! @authorize(policy: "AdminRequiredPolicy") } +type RemoveProjectFromOrgPayload { + organization: Organization + errors: [RemoveProjectFromOrgError!] +} + type RemoveProjectMemberPayload { project: Project } @@ -357,8 +402,14 @@ type UsersCollectionSegment { union AddProjectMemberError = NotFoundError | DbError | ProjectMembersMustBeVerified | ProjectMembersMustBeVerifiedForRole | ProjectMemberInvitedByEmail | InvalidEmailError | AlreadyExistsError +union AddProjectToOrgError = DbError | NotFoundError + union BulkAddProjectMembersError = NotFoundError | InvalidEmailError | DbError +union ChangeOrgMemberRoleError = DbError | NotFoundError + +union ChangeOrgNameError = NotFoundError | DbError | RequiredError + union ChangeProjectDescriptionError = NotFoundError | DbError union ChangeProjectMemberRoleError = NotFoundError | DbError | ProjectMembersMustBeVerified | ProjectMembersMustBeVerifiedForRole @@ -377,10 +428,14 @@ union CreateProjectError = DbError | AlreadyExistsError | ProjectCreatorsMustHav union DeleteDraftProjectError = NotFoundError | DbError +union DeleteOrgError = DbError + union DeleteUserByAdminOrSelfError = NotFoundError | DbError union LeaveProjectError = NotFoundError | LastMemberCantLeaveError +union RemoveProjectFromOrgError = DbError | NotFoundError + union SetOrgMemberRoleError = DbError | NotFoundError union SetProjectConfidentialityError = NotFoundError | DbError @@ -395,6 +450,11 @@ input AddProjectMemberInput { role: ProjectRole! } +input AddProjectToOrgInput { + orgId: UUID! + projectId: UUID! +} + input BooleanOperationFilterInput { eq: Boolean neq: Boolean @@ -407,6 +467,17 @@ input BulkAddProjectMembersInput { passwordHash: String! } +input ChangeOrgMemberRoleInput { + orgId: UUID! + userId: UUID! + role: OrgRole +} + +input ChangeOrgNameInput { + orgId: UUID! + name: String! +} + input ChangeProjectDescriptionInput { projectId: UUID! description: String! @@ -458,6 +529,7 @@ input CreateProjectInput { type: ProjectType! retentionPolicy: RetentionPolicy! isConfidential: Boolean! + owningOrgId: UUID projectManagerId: UUID } @@ -480,6 +552,10 @@ input DeleteDraftProjectInput { draftProjectId: UUID! } +input DeleteOrgInput { + orgId: UUID! +} + input DeleteUserByAdminOrSelfInput { userId: UUID! } @@ -552,6 +628,20 @@ input ListFilterInputTypeOfOrgMemberFilterInput { any: Boolean } +input ListFilterInputTypeOfOrganizationFilterInput { + all: OrganizationFilterInput + none: OrganizationFilterInput + some: OrganizationFilterInput + any: Boolean +} + +input ListFilterInputTypeOfProjectFilterInput { + all: ProjectFilterInput + none: ProjectFilterInput + some: ProjectFilterInput + any: Boolean +} + input ListFilterInputTypeOfProjectUsersFilterInput { all: ProjectUsersFilterInput none: ProjectUsersFilterInput @@ -591,6 +681,9 @@ input OrganizationFilterInput { or: [OrganizationFilterInput!] name: StringOperationFilterInput members: ListFilterInputTypeOfOrgMemberFilterInput + projects: ListFilterInputTypeOfProjectFilterInput + memberCount: IntOperationFilterInput + projectCount: IntOperationFilterInput id: UuidOperationFilterInput createdDate: DateTimeOperationFilterInput updatedDate: DateTimeOperationFilterInput @@ -598,6 +691,8 @@ input OrganizationFilterInput { input OrganizationSortInput { name: SortEnumType + memberCount: SortEnumType + projectCount: SortEnumType id: SortEnumType createdDate: SortEnumType updatedDate: SortEnumType @@ -615,6 +710,7 @@ input ProjectFilterInput { isConfidential: BooleanOperationFilterInput flexProjectMetadata: FlexProjectMetadataFilterInput users: ListFilterInputTypeOfProjectUsersFilterInput + organizations: ListFilterInputTypeOfOrganizationFilterInput lastCommit: DateTimeOperationFilterInput deletedDate: DateTimeOperationFilterInput resetStatus: ResetStatusOperationFilterInput @@ -680,6 +776,11 @@ input ProjectUsersFilterInput { updatedDate: DateTimeOperationFilterInput } +input RemoveProjectFromOrgInput { + orgId: UUID! + projectId: UUID! +} + input RemoveProjectMemberInput { projectId: UUID! userId: UUID! @@ -886,4 +987,4 @@ scalar UUID scalar timestamptz @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time") -scalar uuid \ No newline at end of file +scalar uuid diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 49f9d7248..96bebe6a5 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -45,7 +45,8 @@ export const handle: Handle = ({ event, resolve }) => { } else if (PUBLIC_ROUTE_ROOTS.includes(getRoot(routeId))) { return resolve(event, options); } else if (!isAuthn(cookies)) { - redirect(307, '/login'); + const relativePath = event.url.href.substring(event.url.origin.length); + redirect(307, `/login?ReturnUrl=${encodeURIComponent(relativePath)}`); } //when at home if (routeId == `/${AUTHENTICATED_ROOT}`) { diff --git a/frontend/src/lib/app.postcss b/frontend/src/lib/app.postcss index ee5ff9963..909847aef 100644 --- a/frontend/src/lib/app.postcss +++ b/frontend/src/lib/app.postcss @@ -192,3 +192,27 @@ img[src*="onestory-editor-logo"] { radial-gradient(ellipse 10px 70% at center right, oklch(var(--b3)) 0px, rgba(0, 0, 0, 0) 100%); background-attachment: local, local, scroll, scroll, local, local, scroll, scroll; } + +.tab { + /* https://daisyui.com/docs/themes/#-5 */ + --tab-border: 0.1rem; + /* using a tab radius leads to tiny rendering issues at random screen sizes */ + --tab-radius: 0; + + /* https://daisyui.com/components/tab/#tabs-with-custom-color */ + --tab-border-color: oklch(var(--bc)); + + &:not(.tab-active):not(.tab-divider) { + border: var(--tab-border) solid var(--tab-border-color); + + &:hover { + @apply bg-base-200; + } + } + + /* .tab-divider needs .tab so it can access the tab css-variables */ + &.tab-divider { + @apply px-2; + border-bottom: var(--tab-border) solid var(--tab-border-color); + } +} diff --git a/frontend/src/lib/components/Badges/MemberBadge.svelte b/frontend/src/lib/components/Badges/MemberBadge.svelte index 5cfa1cbd8..822c5be6b 100644 --- a/frontend/src/lib/components/Badges/MemberBadge.svelte +++ b/frontend/src/lib/components/Badges/MemberBadge.svelte @@ -1,6 +1,6 @@ @@ -21,6 +20,6 @@ - + diff --git a/frontend/src/lib/components/Orgs/FormatUserOrgRole.svelte b/frontend/src/lib/components/Orgs/FormatUserOrgRole.svelte new file mode 100644 index 000000000..c1a63f8bb --- /dev/null +++ b/frontend/src/lib/components/Orgs/FormatUserOrgRole.svelte @@ -0,0 +1,16 @@ + + +{_role ?? $t('unknown')} diff --git a/frontend/src/lib/components/ProjectList.svelte b/frontend/src/lib/components/ProjectList.svelte index 564446a24..8e0d11d0b 100644 --- a/frontend/src/lib/components/ProjectList.svelte +++ b/frontend/src/lib/components/ProjectList.svelte @@ -1,42 +1,64 @@ -
+
{#each projects as project} - -
-
-

- - {project.name} - -

- -

{project.code}

- -

-

- -

- {#if project.lastCommit} - {$t('projectlist.last_change', { - lastChange: new Date(project.lastCommit), + {#if project.isDraft} +

+
+
+

+ + {project.name} + + +

+

{project.code}

+ + + {$t('project.awaiting_approval')} + +

+ {$t('projectlist.requested', { + requested: new Date(project.createdDate), })} - {:else} - - {$t('projectlist.no_changes')} - - {/if} -

+

+
-
+ {:else} + + @@ -50,6 +72,9 @@ hover:border-neutral hover:shadow-xl; + max-height: 50vh; + max-width: 100%; + .bg { @apply absolute w-full @@ -73,4 +98,9 @@ @apply opacity-100; } } + + .draft.card { + pointer-events: none; + box-shadow: none; + } diff --git a/frontend/src/lib/components/FormatUserProjectRole.svelte b/frontend/src/lib/components/Projects/FormatUserProjectRole.svelte similarity index 67% rename from frontend/src/lib/components/FormatUserProjectRole.svelte rename to frontend/src/lib/components/Projects/FormatUserProjectRole.svelte index a93002bc5..c0a823ffd 100644 --- a/frontend/src/lib/components/FormatUserProjectRole.svelte +++ b/frontend/src/lib/components/Projects/FormatUserProjectRole.svelte @@ -2,15 +2,15 @@ import { ProjectRole } from '$lib/gql/types'; import t from '$lib/i18n'; - export let projectRole: ProjectRole; + export let role: ProjectRole; const roles: Record = { [ProjectRole.Manager]: $t('project_role.manager'), [ProjectRole.Editor]: $t('project_role.editor'), - [ProjectRole.Unknown]: $t('unknown') + [ProjectRole.Unknown]: $t('unknown'), }; - $: role = roles[projectRole]; + $: _role = roles[role]; -{role ?? $t('unknown')} +{_role ?? $t('unknown')} diff --git a/frontend/src/lib/components/Projects/ProjectTable.svelte b/frontend/src/lib/components/Projects/ProjectTable.svelte index 4c5bf722a..7b713e378 100644 --- a/frontend/src/lib/components/Projects/ProjectTable.svelte +++ b/frontend/src/lib/components/Projects/ProjectTable.svelte @@ -106,6 +106,8 @@ {:else if !project.isDraft} {$date(project.lastCommit)} + {:else} + {$t('project.awaiting_approval')} {/if} {/if} diff --git a/frontend/src/lib/components/Users/CreateUser.svelte b/frontend/src/lib/components/Users/CreateUser.svelte index 2d8ad47be..17a50e944 100644 --- a/frontend/src/lib/components/Users/CreateUser.svelte +++ b/frontend/src/lib/components/Users/CreateUser.svelte @@ -12,6 +12,8 @@ export let skipTurnstile = false; export let submitButtonText = $t('register.button_register'); export let handleSubmit: (password: string, passwordStrength: number, name: string, email: string, locale: string, turnstileToken: string) => Promise; + export let formTainted = false; + $: formTainted = !!$tainted; const dispatch = createEventDispatcher<{ submitted: LexAuthUser, @@ -41,7 +43,7 @@ locale: z.string().trim().min(2).default(userLocale), }); - let { form, errors, message, enhance, submitting } = lexSuperForm(formSchema, async () => { + let { form, errors, message, enhance, submitting, tainted } = lexSuperForm(formSchema, async () => { const { user, error } = await handleSubmit($form.password, $form.score, $form.name, $form.email, $form.locale, turnstileToken); if (error) { if (error.turnstile) { diff --git a/frontend/src/lib/components/Users/CreateUserModal.svelte b/frontend/src/lib/components/Users/CreateUserModal.svelte index e84a6e9b9..ac75bdddf 100644 --- a/frontend/src/lib/components/Users/CreateUserModal.svelte +++ b/frontend/src/lib/components/Users/CreateUserModal.svelte @@ -16,12 +16,14 @@ let createUserModal: Modal; export let handleSubmit: (password: string, passwordStrength: number, name: string, email: string, locale: string, turnstileToken: string) => Promise; + let formTainted = false; + export async function open(): Promise { await createUserModal.openModal(true, true); } - +
@@ -39,7 +41,7 @@

{$t('admin_dashboard.create_user_modal.create_user')}

- { createUserModal.submitModal(); dispatch('submitted', event.detail); diff --git a/frontend/src/lib/components/help/index.ts b/frontend/src/lib/components/help/index.ts index 142bfd6f9..599af1220 100644 --- a/frontend/src/lib/components/help/index.ts +++ b/frontend/src/lib/components/help/index.ts @@ -4,6 +4,7 @@ export const helpLinks = { helpList: 'https://scribehow.com/page/Language_Depot_How-tos__Jy5qu62XRQ-pVGGw6-Cqbw', createProject: 'https://scribehow.com/shared/Create_a_Project__3LFa5XTHSmOLbSSOm8hZKQ', addProjectMember: 'https://scribehow.com/shared/Add_Project_Member__bUJVVK2QT9KhWMqtiPYckA', + addOrgMember: 'https://scribehow.com/shared/Add_Project_Member__bUJVVK2QT9KhWMqtiPYckA', // TODO: Create Add_Org_Member help confidentiality: 'https://scribehow.com/shared/Project_Confidentiality__s6TX8_wFQ1ejVpH1s5Bsmw', bulkAddCreate: 'https://scribehow.com/shared/Bulk_AddCreate_Project_Members__3wwDKk3TTGaAwMEmT4rrXQ', projectRequest: 'https://scribehow.com/shared/Project_requests__zOdcHT8KRGygGmPgr5z2_A', diff --git a/frontend/src/lib/components/modals/FormModal.svelte b/frontend/src/lib/components/modals/FormModal.svelte index dddaac573..e9e51f7bf 100644 --- a/frontend/src/lib/components/modals/FormModal.svelte +++ b/frontend/src/lib/components/modals/FormModal.svelte @@ -89,7 +89,7 @@ reset()} bottom closeOnClickOutside={!$tainted}>
-

+

diff --git a/frontend/src/lib/email/ApproveProjectRequest.svelte b/frontend/src/lib/email/ApproveProjectRequest.svelte new file mode 100644 index 000000000..4fd13b643 --- /dev/null +++ b/frontend/src/lib/email/ApproveProjectRequest.svelte @@ -0,0 +1,16 @@ + + + + {$t('emails.approve_project_request_email.heading', {projectName})} + {$t('emails.approve_project_request_email.view_button')} + diff --git a/frontend/src/lib/email/UserAdded.svelte b/frontend/src/lib/email/UserAdded.svelte new file mode 100644 index 000000000..f1142d7ed --- /dev/null +++ b/frontend/src/lib/email/UserAdded.svelte @@ -0,0 +1,15 @@ + + + + {$t('emails.user_added.body', {projectName})} + {$t('emails.user_added.view_button')} + diff --git a/frontend/src/lib/forms/OrgRoleSelect.svelte b/frontend/src/lib/forms/OrgRoleSelect.svelte new file mode 100644 index 000000000..b322aabda --- /dev/null +++ b/frontend/src/lib/forms/OrgRoleSelect.svelte @@ -0,0 +1,18 @@ + + + diff --git a/frontend/src/lib/forms/index.ts b/frontend/src/lib/forms/index.ts index 9564bc1cc..4fb3d6635 100644 --- a/frontend/src/lib/forms/index.ts +++ b/frontend/src/lib/forms/index.ts @@ -14,6 +14,7 @@ import { lexSuperForm } from './superforms'; import type { ErrorMessage } from './types'; export * from './utils'; import SystemRoleSelect from './SystemRoleSelect.svelte'; +import OrgRoleSelect from './OrgRoleSelect.svelte'; import ProjectRoleSelect from './ProjectRoleSelect.svelte'; import ProjectTypeSelect from './ProjectTypeSelect.svelte'; import DisplayLanguageSelect from './DisplayLanguageSelect.svelte'; @@ -35,6 +36,7 @@ export { type Token, type ErrorMessage, SystemRoleSelect, + OrgRoleSelect, ProjectRoleSelect, ProjectTypeSelect, DisplayLanguageSelect, diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index eb7095e11..64ecf1390 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -1,8 +1,12 @@ { + "about": { + "title": "About" + }, "admin_dashboard": { "title": "Admin Dashboard", "column_code": "Code", "column_email": "Email", + "column_email_or_login": "Email / Login", "column_login": "Login", "column_last_change": "Last Change", "column_migrated": "Migrated", @@ -94,6 +98,7 @@ }, "appmenu": { "log_out": "Log out", + "orgs": "Organizations", "help": "Help" }, "close": "Close", @@ -104,6 +109,15 @@ "remove": "Remove {entityName}" }, "environment_warning": "This is a {environmentName} environment. Click here to go to the public site.", + "delete_org_modal": { + "title": "Delete Organization", + "submit": "Delete Organization", + "enter_to_delete": { + "label": "Enter ''{_value}'' to confirm deleting the organization ''{name}''", + "value": "DELETE ORGANIZATION" + }, + "success": "Organization ''{name}'' was deleted." + }, "delete_project_modal": { "title": "Delete Project", "submit": "Delete Project", @@ -137,10 +151,24 @@ "link_expired": "The email you clicked has expired. Please request a new one.", "welcome_header": "### Welcome to Language Depot\n", "welcome": "Language Depot is a hosting service for [FieldWorks (FLEx)](https://software.sil.org/fieldworks/), \ -[Language Forge](https://languageforge.org/), [OneStory Editor](https://onestory.org/) and [WeSay](https://software.sil.org/wesay/) projects. \ +[Language Forge](https://languageforge.org/), [OneStory Editor](https://software.sil.org/onestoryeditor) and [WeSay](https://software.sil.org/wesay/) projects. \ It is provided as a service to language communities by [SIL Language Technology](https://software.sil.org/) and \ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chiang Mai, Thailand." }, + "org": { + "table": { + "title": "Organizations", + "name": "Name", + "created_at": "Created", + "members": "Members" + }, + "create": { + "title": "Create Organization", + "name": "Name", + "name_missing": "Organization name required", + "submit": "Create Organization", + } + }, "project": { "create": { "title": "Create Project", @@ -172,6 +200,8 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia "migrated": "Migrated", "type": "Type", "users": "Users", + "members": "Members", + "__comment": "Move 'members' to 'org.table' once org page PR is merged", }, "filter": { "title": "Project filters", @@ -199,6 +229,59 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia "project_now_not_confidential": "Project is now not confidential.", } }, + "awaiting_approval": "Awaiting approval" + }, + "org_page": { + "organization": "Organization", + "add_user": { + "add_button": "Add Member", + "__comment_add_button": "Should become 'Add/Invite Member' once email invitations implemented for orgs", + "modal_title": "Add a Member to this organization", + "__comment_modal_title": "Should become 'Add or invite a Member ...' once email invitations implemented for orgs", + "submit_button": "Add Member", + "empty_user_field": "Please enter an email address or login", + "submit_button_email": "Add Member", + "__comment_submit_button_email": "Should become 'Add or invite Member' once email invitations implemented for orgs", + "org_not_found": "Organization not found. Please refresh the page.", + "username_not_found": "No user was found with this login", + "user_must_be_verified": "User needs a verified email address", + "admin_must_be_verified": "User needs a verified email address in order to be an organization admin", + "user_already_member": "User is already a member of this organization", + "user_needs_to_relogin": "Added members will need to log out and back in again before they see the new organization.", + "invalid_email_address": "Invalid email address: {email}", + }, + "change_role_modal": { + "title": "Choose role for {name}", + "button_label": "Change Role" + }, + "delete_modal": { + "submit": "Delete Organization" + }, + "details": { + "created_at": "Created", + "updated_at": "Last updated", + "member_count": "Members", + "project_count": "Projects", + }, + "notifications": { + "role_change": "Organizational role of {name} set to {role}.", + "user_delete": "{name} has been removed.", + "rename_org": "Organization name set to {name}.", + "delete_org": "Organization {name} has been deleted.", + "leave_org": "You have left the organization {name}.", + "leave_org_error": "An error occurred trying to remove you from organization {name}. Please try again later.", + "describe": "Organization description has been updated.", + "add_member": "{email} has been added to organization.", + "member_invited": "{email} has been sent an invitation email to register and join the organization." + }, + "edit_member_role": "Change Role", + "remove_member": "Remove", + "org_name_empty_error": "Organization name cannot be empty", + "projects_table_title": "Projects", + "members_table_title": "Members", + "settings_view_title": "Settings", + "history_view_title": "History", + "leave_org": "Leave Organization", }, "project_page": { "project": "Project", @@ -287,8 +370,10 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia "last_commit": "Last Commit", "members": { "title": "Members", + "filter_members_placeholder": "Filter members...", "show_all": "Show all...", "show_less": "Show less", + "no_matching": "No matching members", }, "add_description": "Add description...", "remove_project_user_title": "Member", @@ -360,6 +445,15 @@ If you don't see a dialog or already closed it, click the button below:", "leave_project": "Leave Project" }, }, + "org_role": { + "label": "Role", + "user": "Member", + "user_description": "Member (can join projects)", + "admin": "Admin", + "admin_description": "Admin (can join projects & add new members)", + "unknown": "Unknown", + "unknown_description": "Unknown" + }, "project_role": { "label": "Role", "editor": "Editor", @@ -385,7 +479,8 @@ If you don't see a dialog or already closed it, click the button below:", "projectlist": { "last_change": "Last Change: {lastChange, date, short} {lastChange, time, short}", "no_changes": "New", - "shared_with": "Shared with {memberCount, plural, one {no one} other {# people} }" + "shared_with": "Shared with {memberCount, plural, one {no one} other {# people} }", + "requested": "Requested: {requested, date, short} {requested, time, short}", }, "register": { "title": "Register", @@ -474,12 +569,22 @@ If you don't see a dialog or already closed it, click the button below:", }, "create_account_request_email": { "subject": "Project invitation: {projectName}", - "body": "{managerName} has invited you to join the {projectName} language project. Click below to join.", + "body": "{managerName} has invited you to join the project: {projectName}. Click below to join.", "join_button": "Join project" }, "create_project_request_email": { "subject": "Project request: {projectName}", "heading": "User {name} ({email}) requested that a project be created for them. Details below:" + }, + "approve_project_request_email": { + "subject": "Project approved: {projectName}", + "heading": "The project you requested, {projectName}, has been approved and created.", + "view_button": "View project" + }, + "user_added": { + "subject": "You joined project: {projectName}!", + "body": "You have been added to the project: {projectName}.", + "view_button": "View project" } }, "footer": { diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json index ab7b019d5..5002d7ff7 100644 --- a/frontend/src/lib/i18n/locales/es.json +++ b/frontend/src/lib/i18n/locales/es.json @@ -117,7 +117,7 @@ "link_expired": "El correo electrónico en el que hiciste clic ha caducado. Por favor, solicita uno nuevo.", "welcome_header": "### Bienvenido a Language Depot\n", "welcome": "Language Depot es un servicio de alojamiento para proyectos de [FieldWorks (FLEx)](https://software.sil.org/fieldworks/), \ -[Language Forge](https://languageforge.org/), [OneStory Editor](https://onestory.org/) y [WeSay](https://software.sil.org/wesay/). \ +[Language Forge](https://languageforge.org/), [OneStory Editor](https://software.sil.org/onestoryeditor) y [WeSay](https://software.sil.org/wesay/). \ Se proporciona como un servicio a comunidades lingüísticas por [SIL Language Technology](https://software.sil.org/) y \ el [Linguistics Institute at Payap University](https://li.payap.ac.th/) en Chiang Mai, Tailandia." }, diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json index ef049b0c7..4194cfc2a 100644 --- a/frontend/src/lib/i18n/locales/fr.json +++ b/frontend/src/lib/i18n/locales/fr.json @@ -45,7 +45,7 @@ "title": "Langue", "en": "English", "fr": "Français", - "not_supported": "Pas encore pris en charge", + "not_supported": "Pas encore pris en charge" }, "reset_password": "Réinitialiser votre mot de passe à la place?", "update_success": "Votre compte a été mis à jour.", @@ -117,7 +117,7 @@ "link_expired": "L'e-mail sur lequel vous avez cliqué a expiré. Veuillez en demander un nouveau.", "welcome_header": "### Bienvenue sur Language Depot\n", "welcome": "Language Depot est un service d'hébergement pour [FieldWorks (FLEx)](https://software.sil.org/fieldworks/),\ -[Language Forge](https://languageforge.org/), [OneStory Editor](https://onestory.org/) et [WeSay](https://software.sil.org/wesay/) projects.\ +[Language Forge](https://languageforge.org/), [OneStory Editor](https://software.sil.org/onestoryeditor) et [WeSay](https://software.sil.org/wesay/) projects.\ Il est fourni en tant que service aux communautés linguistiques par [SIL Language Technology](https://software.sil.org/) et\ le [Linguistics Institute at Payap University](https://li.payap.ac.th/) à Chiang Mai, en Thaïlande." }, diff --git a/frontend/src/lib/icons/HomeIcon.svelte b/frontend/src/lib/icons/HomeIcon.svelte index 26161a5dd..9104d4948 100644 --- a/frontend/src/lib/icons/HomeIcon.svelte +++ b/frontend/src/lib/icons/HomeIcon.svelte @@ -1,2 +1,5 @@ - - + + + diff --git a/frontend/src/lib/layout/AppBar.svelte b/frontend/src/lib/layout/AppBar.svelte index a8c7d6978..23ec8066f 100644 --- a/frontend/src/lib/layout/AppBar.svelte +++ b/frontend/src/lib/layout/AppBar.svelte @@ -31,7 +31,7 @@
{#if user} -