[
  {
    "path": ".credo.exs",
    "content": "# This file contains the configuration for Credo and you are probably reading\n# this after creating it with `mix credo.gen.config`.\n#\n# If you find anything wrong or unclear in this file, please report an\n# issue on GitHub: https://github.com/rrrene/credo/issues\n#\n%{\n  #\n  # You can have as many configs as you like in the `configs:` field.\n  configs: [\n    %{\n      #\n      # Run any exec using `mix credo -C <name>`. If no exec name is given\n      # \"default\" is used.\n      #\n      name: \"default\",\n      #\n      # These are the files included in the analysis:\n      files: %{\n        #\n        # You can give explicit globs or simply directories.\n        # In the latter case `**/*.{ex,exs}` will be used.\n        #\n        included: [\"lib/\", \"src/\", \"test/\", \"web/\", \"apps/\"],\n        excluded: [~r\"/_build/\", ~r\"/deps/\", ~r\"/node_modules/\"]\n      },\n      #\n      # Load and configure plugins here:\n      #\n      plugins: [],\n      #\n      # If you create your own checks, you must specify the source files for\n      # them here, so they can be loaded by Credo before running the analysis.\n      #\n      requires: [],\n      #\n      # If you want to enforce a style guide and need a more traditional linting\n      # experience, you can change `strict` to `true` below:\n      #\n      strict: true,\n      #\n      # If you want to use uncolored output by default, you can change `color`\n      # to `false` below:\n      #\n      color: true,\n      #\n      # You can customize the parameters of any check by adding a second element\n      # to the tuple.\n      #\n      # To disable a check put `false` as second element:\n      #\n      #     {Credo.Check.Design.DuplicatedCode, false}\n      #\n      checks: [\n        #\n        ## Consistency Checks\n        #\n        {Credo.Check.Consistency.ExceptionNames, []},\n        {Credo.Check.Consistency.LineEndings, []},\n        {Credo.Check.Consistency.ParameterPatternMatching, []},\n        {Credo.Check.Consistency.SpaceAroundOperators, []},\n        {Credo.Check.Consistency.SpaceInParentheses, []},\n        {Credo.Check.Consistency.TabsOrSpaces, []},\n\n        #\n        ## Design Checks\n        #\n        # You can customize the priority of any check\n        # Priority values are: `low, normal, high, higher`\n        #\n        {Credo.Check.Design.AliasUsage,\n         [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 1]},\n        # You can also customize the exit_status of each check.\n        # If you don't want TODO comments to cause `mix credo` to fail, just\n        # set this value to 0 (zero).\n        #\n        {Credo.Check.Design.TagTODO, [exit_status: 0]},\n        {Credo.Check.Design.TagFIXME, []},\n\n        #\n        ## Readability Checks\n        #\n        {Credo.Check.Readability.AliasOrder, []},\n        {Credo.Check.Readability.FunctionNames, []},\n        {Credo.Check.Readability.LargeNumbers, []},\n        {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},\n        {Credo.Check.Readability.ModuleAttributeNames, []},\n        {Credo.Check.Readability.ModuleDoc, []},\n        {Credo.Check.Readability.ModuleNames, []},\n        {Credo.Check.Readability.ParenthesesInCondition, []},\n        {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},\n        {Credo.Check.Readability.PredicateFunctionNames, []},\n        {Credo.Check.Readability.PreferImplicitTry, []},\n        {Credo.Check.Readability.RedundantBlankLines, []},\n        {Credo.Check.Readability.Semicolons, []},\n        {Credo.Check.Readability.SpaceAfterCommas, []},\n        {Credo.Check.Readability.StringSigils, []},\n        {Credo.Check.Readability.TrailingBlankLine, []},\n        {Credo.Check.Readability.TrailingWhiteSpace, []},\n        # TODO: enable by default in Credo 1.1\n        {Credo.Check.Readability.UnnecessaryAliasExpansion, false},\n        {Credo.Check.Readability.VariableNames, []},\n\n        #\n        ## Refactoring Opportunities\n        #\n        {Credo.Check.Refactor.CondStatements, []},\n        {Credo.Check.Refactor.CyclomaticComplexity, []},\n        {Credo.Check.Refactor.FunctionArity, []},\n        {Credo.Check.Refactor.LongQuoteBlocks, []},\n        {Credo.Check.Refactor.MapInto, []},\n        {Credo.Check.Refactor.MatchInCondition, []},\n        {Credo.Check.Refactor.NegatedConditionsInUnless, []},\n        {Credo.Check.Refactor.NegatedConditionsWithElse, []},\n        {Credo.Check.Refactor.Nesting, [max_nesting: 3]},\n        {Credo.Check.Refactor.UnlessWithElse, []},\n        {Credo.Check.Refactor.WithClauses, []},\n        {Credo.Check.Refactor.RedundantWithClauseResult, false},\n\n        #\n        ## Warnings\n        #\n        {Credo.Check.Warning.BoolOperationOnSameValues, []},\n        {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},\n        {Credo.Check.Warning.IExPry, []},\n        {Credo.Check.Warning.IoInspect, []},\n        {Credo.Check.Warning.LazyLogging, []},\n        {Credo.Check.Warning.OperationOnSameValues, []},\n        {Credo.Check.Warning.OperationWithConstantResult, []},\n        {Credo.Check.Warning.RaiseInsideRescue, []},\n        {Credo.Check.Warning.UnusedEnumOperation, []},\n        {Credo.Check.Warning.UnusedFileOperation, []},\n        {Credo.Check.Warning.UnusedKeywordOperation, []},\n        {Credo.Check.Warning.UnusedListOperation, []},\n        {Credo.Check.Warning.UnusedPathOperation, []},\n        {Credo.Check.Warning.UnusedRegexOperation, []},\n        {Credo.Check.Warning.UnusedStringOperation, []},\n        {Credo.Check.Warning.UnusedTupleOperation, []},\n        {Credo.Check.Warning.SpecWithStruct, false},\n        {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, false},\n\n        #\n        # Controversial and experimental checks (opt-in, just replace `false` with `[]`)\n        #\n        {Credo.Check.Consistency.MultiAliasImportRequireUse, false},\n        {Credo.Check.Consistency.UnusedVariableNames, false},\n        {Credo.Check.Design.DuplicatedCode, false},\n        {Credo.Check.Readability.AliasAs, false},\n        {Credo.Check.Readability.MultiAlias, false},\n        {Credo.Check.Readability.Specs, false},\n        {Credo.Check.Readability.SinglePipe, false},\n        {Credo.Check.Refactor.ABCSize, false},\n        {Credo.Check.Refactor.AppendSingleItem, false},\n        {Credo.Check.Refactor.DoubleBooleanNegation, false},\n        {Credo.Check.Refactor.ModuleDependencies, false},\n        {Credo.Check.Refactor.PipeChainStart, false},\n        {Credo.Check.Refactor.VariableRebinding, false},\n        {Credo.Check.Refactor.Apply, false},\n        {Credo.Check.Warning.MapGetUnsafePass, false},\n        {Credo.Check.Warning.UnsafeToAtom, false}\n\n        #\n        # Custom checks can be created using `mix credo.gen.check`.\n        #\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": ".formatter.exs",
    "content": "[\n  inputs: [\"mix.exs\", \"config/*.exs\"],\n  subdirectories: [\"apps/*\"]\n]\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: boruta-server\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deployment\n\non:\n  push:\n    branches:\n      - provider-policies-registration\n  workflow_run:\n    workflows:\n      - Continuous Integration\n    branches:\n      - master\n      - signatures-adapter\n    types:\n      - completed\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build-and-push-server-image:\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - uses: benjlevesque/short-sha@v1.2\n        id: short-sha\n        with:\n          length: 8\n\n      - name: Build and push server Docker image\n        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc\n        with:\n          file: Dockerfile.full\n          context: .\n          build-args: |\n            BORUTA_OAUTH_BASE_URL=https://oauth.boruta.patatoid.fr\n          push: true\n          tags: |\n            ${{ env.REGISTRY }}/malach-it/boruta-server:${{ steps.short-sha.outputs.sha }},\n            ${{ env.REGISTRY }}/malach-it/boruta-server:${{ github.head_ref || github.ref_name }}\n          labels: ${{ steps.meta.outputs.labels }}\n\n  build-and-push-gateway-image:\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - uses: benjlevesque/short-sha@v1.2\n        id: short-sha\n        with:\n          length: 8\n\n      - name: Build and push gateway Docker image\n        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc\n        with:\n          file: Dockerfile.gateway\n          context: .\n          push: true\n          tags: |\n            ${{ env.REGISTRY }}/malach-it/boruta-gateway:${{ steps.short-sha.outputs.sha }},\n            ${{ env.REGISTRY }}/malach-it/boruta-gateway:${{ github.head_ref || github.ref_name }}\n          labels: ${{ steps.meta.outputs.labels }}\n\n  build-and-push-auth-image:\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - uses: benjlevesque/short-sha@v1.2\n        id: short-sha\n        with:\n          length: 8\n\n      - name: Build and push auth Docker image\n        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc\n        with:\n          file: Dockerfile.auth\n          context: .\n          push: true\n          tags: |\n            ${{ env.REGISTRY }}/malach-it/boruta-auth:${{ steps.short-sha.outputs.sha }},\n            ${{ env.REGISTRY }}/malach-it/boruta-auth:${{ github.head_ref || github.ref_name }}\n          labels: ${{ steps.meta.outputs.labels }}\n\n  build-and-push-admin-image:\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - uses: benjlevesque/short-sha@v1.2\n        id: short-sha\n        with:\n          length: 8\n\n      - name: Build and push admin Docker image\n        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc\n        with:\n          file: Dockerfile.admin\n          context: .\n          push: true\n          tags: |\n            ${{ env.REGISTRY }}/malach-it/boruta-admin:${{ steps.short-sha.outputs.sha }},\n            ${{ env.REGISTRY }}/malach-it/boruta-admin:${{ github.head_ref || github.ref_name }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/elixir.yml",
    "content": "name: Continuous Integration\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n\njobs:\n  static_code_analysis:\n    name: Static Code Analysis\n    runs-on: ubuntu-22.04\n    container: elixir:1.14.5-otp-25\n    steps:\n      - name: Cancel Previous Runs\n        uses: styfle/cancel-workflow-action@0.11.0\n        with:\n          access_token: ${{ github.token }}\n\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n\n      - name: Retrieve Cached Dependencies\n        uses: actions/cache@v3\n        id: mix-cache\n        with:\n          path: |\n            deps\n            _build\n          key: ${{ runner.os }}-${{ hashFiles('mix.lock') }}\n\n            # - name: Check Code Format\n            #   run: mix format --check-formatted\n\n      - name: Elixir prerequisites\n        run: |\n          mix local.rebar --force\n          mix local.hex --force\n          mix deps.get\n\n      - name: Compilation warnings\n        run: mix compile --force --warnings-as-errors\n\n      - name: Run Credo\n        run: mix credo --strict\n\n          # - name: Run Dialyzer\n          #   run: mix dialyzer\n\n  unit_tests:\n    name: Unit Tests\n    runs-on: ubuntu-22.04\n    container: elixir:1.14.5-otp-25\n    strategy:\n      fail-fast: false\n\n    services:\n      postgres:\n        image: postgres\n        env:\n          POSTGRES_PASSWORD: postgres\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n    steps:\n      - name: Cancel Previous Runs\n        uses: styfle/cancel-workflow-action@0.6.0\n        with:\n          access_token: ${{ github.token }}\n\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n\n      - name: Retrieve Cached Dependencies\n        uses: actions/cache@v3\n        id: mix-cache\n        with:\n          path: |\n            deps\n            _build\n          key: ${{ runner.os }}-${{ hashFiles('mix.lock') }}\n\n      - name: Elixir prerequisites\n        run: |\n          mix local.rebar --force\n          mix local.hex --force\n          mix deps.get\n\n      - name: Run test\n        run: mix test --trace\n        env:\n          MIX_ENV: test\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: postgres\n          POSTGRES_DATABASE: boruta_test\n          POSTGRES_HOST: postgres\n"
  },
  {
    "path": ".gitignore",
    "content": "/_build/\n/cover/\n/deps/\n/doc/\n/tmp/\n/log/\n/.fetch\nerl_crash.dump\n*.ez\n\n.env.sh\n.env\n\n*.swp\n*.swo\n*.orig\ntags*\n\napps/*/priv/static/**\napps/*/priv/ssl/**\napps/*/log/**\n\n.vscode\n\n/ansible/group_vars\n"
  },
  {
    "path": ".gitlab-ci.yml",
    "content": "stages:\n  - test\n  - build\n  - deploy\n\nservices:\n  - postgres:latest\n\n.elixir-task:\n  image: elixir:1.12.1\n  before_script:\n  - apt-get install -y libcurl4-openssl-dev libssl-dev libevent-dev\n  - mix local.hex --force\n  - mix local.rebar --force\n  - mix deps.get\n\n    # dialyzer:\n    #   stage: test\n    #   extends: .elixir-task\n    #   cache:\n    #     paths:\n    #       - _build/\n    #   script:\n    #   - mix dialyzer\n\ncredo:\n  stage: test\n  extends: .elixir-task\n  script:\n  - mix credo --strict\n\ntest:\n  stage: test\n  extends: .elixir-task\n  script:\n  - mix test --trace\n  variables:\n    POSTGRES_DATABASE: boruta_test\n    POSTGRES_HOST: postgres\n    POSTGRES_USER: postgres\n    POSTGRES_PASSWORD: postgres\n    MIX_ENV: test\n\nbuild:\n  image: docker:19.03.12\n  stage: build\n  services:\n    - docker:19.03.12-dind\n  before_script:\n    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY\n    - docker pull $CI_REGISTRY/patatoid/boruta/app:latest\n  script:\n    - docker build --cache-from $CI_REGISTRY/patatoid/boruta/app:latest -t $CI_REGISTRY/patatoid/boruta/app:$CI_COMMIT_SHORT_SHA .\n    - docker tag $CI_REGISTRY/patatoid/boruta/app:$CI_COMMIT_SHORT_SHA $CI_REGISTRY/patatoid/boruta/app:latest\n    - docker push $CI_REGISTRY/patatoid/boruta/app:$CI_COMMIT_SHORT_SHA\n    - docker push $CI_REGISTRY/patatoid/boruta/app:latest\n  only:\n    - master\n\ndeploy:\n  stage: deploy\n  image: debian:latest\n  variables:\n    PIP_CACHE_DIR: \"$CI_PROJECT_DIR/.cache/pip\"\n  cache:\n    paths:\n    - .cache/pip\n  before_script:\n  - apt-get update\n  - apt-get install -y curl python3-pip apt-transport-https\n  - curl https://baltocdn.com/helm/signing.asc | apt-key add -\n  - echo \"deb https://baltocdn.com/helm/stable/debian/ all main\" | tee /etc/apt/sources.list.d/helm-stable-debian.list\n  - apt-get update\n  - apt-get install -y helm\n  - pip3 install --upgrade setuptools pip\n  - pip3 install ansible pyhelm grpcio requests openshift kubernetes\n  - ansible-galaxy collection install kubernetes.core\n  - curl -LO \"https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl\"\n  - curl -LO \"https://dl.k8s.io/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl.sha256\"\n  - echo \"$(<kubectl.sha256) kubectl\" | sha256sum --check\n  - mv ./kubectl /usr/bin/kubectl\n  - chmod 755 /usr/bin/kubectl\n  script:\n  - cd ansible\n  - echo $VAULT_PASSWORD > vault_pass.txt\n  - ansible-vault decrypt ./.kube/kubeconfig-k8s-boruta.yaml --vault-password-file vault_pass.txt\n  - KUBECONFIG=./.kube/kubeconfig-k8s-boruta.yaml ansible-playbook -i ./inventories/scaleway deploy.yml -e release_tag=$CI_COMMIT_SHORT_SHA --vault-password-file vault_pass.txt\n  only:\n    - master\n"
  },
  {
    "path": ".tool-versions",
    "content": "nodejs 25.2.1\nerlang 25.3.2.21\nelixir 1.14.5-otp-25\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n> Note that 0.X.X releases are reverved for the beta version of the server and may include breaking changes.\n\n## [unreleased]\n\n### Added\n\n- [ssi] code chains\n    - verify verifiable presentation from code chains\n    - issuance code chains\n    - next flow redirection in case of presentation success\n    - agent token management\n- [ssi] code metadata policies\n    - restrict issuance / presentation key usage for enabled check public client id clients\n- [ssi] server sent events verifiable presentation page navigation\n- [identity] add resource owner in credentials templates\n- [ssi] credential issuance scope restriction\n- [wallet] display credential presentation purposes\n\n### Changed\n\n- [ssi] remove resource owner constraint for openid4vc flows\n- [ssi] default backend authorization details for anonymous users\n- [ssi] issuance / presentation default templates open integrated wallet in a popup\n- [ssi] add client_id to credential offers\n- [identity] improve identity providers querying and cache\n- [admin] group direct post requests in dashboard\n\n### Fixed\n\n- [admin] add credential offer and presentation in breadcrumb\n- [admin] update identity provider title in breadcrumb\n- [admin] feedback stars display\n- [wallet] qr code scan redirection\n- [ssi] public and unknown users presentation\n\n### Security\n\n- [admin] only expose client name in templates\n- [auth] remove default client secret from seeds\n- [admin] set minimum oauth client private key modulus size\n\n## [0.8.0] - 2025-07-12\n\n### Added\n\n- [auth] agent credentials / code flows\n- [wallet] key selection\n- [ssi] verify public client id oauth client option\n\n### Changed\n\n- [auth] max authorization code ttl to 600 seconds\n- [ssi] remove authentication on siopv2 flow\n\n### Fixed\n\n- [admin] file upload text editor update\n- [ssi] expose public credential configuration for authenticated users\n- [wallet] fix presentation duplicates\n\n### Security\n\n- [auth] experimental request rate limiting\n- [auth] remove dynamic client registration\n\n## [0.7.2] - 2025-04-13\n\n### Fixed\n\n- [auth] fix boruta core migration\n\n## [0.7.1] - 2025-04-05\n\n### Fixed\n\n- [ssi] do not use ES256 alg to verify EdDSA JWTs\n- [identity] expose default templates static assets\n\n## [0.7.0] - 2025-03-26\n\n### Added\n\n- [admin] signatures adapter\n- [wallet] display an error when no credential match presentation\n- [identity] add reload button in credentials temapltes\n- [wallet] close qr code scanner on click\n\n### Security\n\n- [wallet] fix npm vulnerabilities\n- [admin] fix npm vulnerabilities\n\n## [0.6.1] - 2025-03-15\n\n### Security\n\n- [admin] update verifiable presentations default template\n\n## [0.6.0] - 2025-03-15\n\n### Added\n\n- [identity] passwordless user creation (WIP)\n- [identity] destroy user\n- [ssi] transaction code in OID4VCI preauthorized code flow\n- [ssi] vct configuration in verifiable credentials\n- [admin] feedback form\n- [wallet] web identity wallet bootstrap (PWA)\n- [identity] scope user emails per backend\n- [admin] decentralized identity example flows\n- [ssi] verifiable credentials nested claims\n\n### Changed\n\n- [identity] remove user metadata value constraints\n- [admin] verifiable credentials claim format\n\n### Fixed\n\n- [admin] defered configuration\n- [admin] example credential issuance link\n- [ssi] oauth clients did persistence\n- [admin] verifiable presentation definition text edition\n\n### Security\n\n- [admin] remove cdnjs dependency\n- [identity] remove picsum dependency\n\n## [0.5.1] - 2024-11-21\n\n### Added\n\n- [admin] user csv import metadata\n- [infra] organization creation in static configuration\n- [admin] client key pair configuration + support for EC keys\n\n### Fixed\n\n- [ssi] several verifiable credentials issuance and presentation fixes\n- [auth] configurable status display in id_token claims\n- [admin] user with empty metadata save\n- [admin] federated users deletion\n\n## [0.5.0] - 2024-10-17\n\n### Added\n\n- [ssi] OpenID for Verifiable Credentials Presentation implementation\n\n## [0.4.2] - 2024-09-20\n\n### Fixed\n\n- [auth] fix authorize entrypoint\n\n## [0.4.1] - 2024-09-18\n\n### Fixed\n\n- [admin] ipv6 log display\n\n### Security\n\n- [infra] remove .env.example.sig as suspicious file\n\n## [0.4.0] 2024-09-01\n\n### Added\n\n- [ssi] Configurable verifiable credentials issuance with oid4vci implementation\n- [ssi] Siopv2 same device implementation\n- [auth] Demonstration proof of possession implementation\n- [auth] Pushed Authorization Request implementation\n- [infra] Server ip address bindings configuration via environment variables\n- [infra]Infrastructure as Code with static file configuration\n- [admin] Admin ui improvements\n- [auth] Better identity federation\n- [identity] Webauthn integration\n- [infra] Remote IP logging\n\n### Security\n\n- [admin] instance authenticated admins are sub or organization restricted\n\n### Fixed\n\n- [infra] Fix organization and sub admin access restriction\n\n## [0.3.0] 2024-01-18\n\n### Added\n\n- [identity] user organisation management\n- [identity] TOTP second factor support\n- [identity] user roles management\n- [infra] split auth/admin/gateway/all docker images\n- [infra] split gateway, admin, auth releases\n- [infra] system wide installation script\n- [infra] gather statistical info on installation\n\n## [0.2.0] - 2023-05-17\n\n### Added\n\n- [gateway] introspected token forwarding to updatreams\n- [identity] email templates edition\n- [identity] configure, expose and edit user metadata\n- [identity] user metadata configuration\n- [gateway] static configuration\n- [gateway] microgateways\n- [identity] identity federation (login with button)\n- [auth] better well-known openid configuration\n- [auth] dynamic client registration\n- [auth] client authentication methods configuration\n- [auth] global signing key pairs\n\n\n### Security\n\n- [identity] invalidate user reset password token at use\n\n## [0.1.0] - 2022-10-25\n\nInitial beta release\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Code of Conduct\n\n## 1. Why share this code of conduct?\n\nA code of conduct aims to set the frame of community interactions around the corresponding open-source product. As collective work is at stake here, getting into it requires fitting the rules that make a safe and well-being place to work. This document aims the community wellness to give us the best opportunities to produce the relative facts and deliverables. From this perspective, the need for us to navigate with psychological safety is a must-have to deliver the best we can to improve and make evolve the code at the heart of the community.\n\n## 2. What is a violation?\n\nThe purpose of this document is to set the limits and how to mitigate when violations occur. First of all, any violation of applicable law is to be reported for the healthiness of our interactions. We would make focus on harassment and all kind of discrimination against ancestry, age, color, religion, caste, origin, race, personal appearance, nationality, socio-economic status, level of experience, gender identity and expression, sex characteristics, ethnicity, sexual identity and orientation, visible or invisible disability, body size, and other private or personal information that can be used for harming purposes.\n\n## 3. What is a contribution?\n\nAre considered contributions, the produced code and all derived material propagated under the copyright provided by this repository of code. Other material out of this scope is not considered contributions to this project and we do have not any responsibility for them. Also, any official representation of the product may be considered as a contribution. Those need a written validation from the owners of the project.\n\n## 4. In accordance with the product vision\n\nThe contributions to this project may fit its vision, all contributions that may diverge from it would be considered as violations if not consented by the owner. The vision of the project is stated as :\n\nENABLING ORGANIZATIONS THEN USERS TO MANAGE DIGITAL IDENTITIES WHERE DATA PROTECTION IMPROVE SYSTEMS SECURITY\n\n## 5. The roles and responsibilities of contributors\n\nWithin this frame arise three main responsibilities. From those can be derived roles that have to fulfill all of them within the community. The objective of them is to help community wellness.\n\nThe first responsibility is to report violations that are described above (section 2). It is an overall member's duty to report such violations to the right person following an escalation process. That person will be named here as a facilitator of escalation, described next.\n\nIn order to facilitate escalation, the facilitator starts from report and assess the situation to find the right way to mitigate the issue. By owning the report, he keeps the process alive until its resolution by facilitating interactions with the mediating people. The best is to have a written trace of the way the situation was solved step by step to reproduce if it occurs again. He is also responsible for the escalation process improvement.\n\nWithin the community, mediation of the situations of violation is made by resolving the conflicts. The responsible person for this should be trained to conflict resolution using methods like Non-Violent Communication from Rosenberg or other peaceful ways of resolution. The responsibility includes finding external resources to solve the conflict, up to contact authorities in case of grave violation or violation of applicable law.\n\nOnce again, it is an overall responsibility to report the issues as it is the basis for a healthy community. Starting by navigating within the frame, ensuring psychological safety is also needed for us to evolve as a community.\n\nFor example, the minimal roles within a community around an open-source project start with an owner. The project owner can take both mediator and facilitator of escalation responsibilities. Those can be split then and spread across the community knowing that the responsibilities do not have to be unique within the community. Finding suitable roles according to the needed responsibilities of this framework is a work that may include community members to find a collective track toward them.\n\n## 6. Escalation of violations\n\nBased on IT incident report patterns, escalation of violations aims to find the root cause, fix the issue, and prevent its reappearance. Also, one of the objectives is to avoid conflict between parties and find equitable solutions to solve the issue. For that, the mediation may be made using methodology such as Non-Violent Communication to enforce communication over violence being factual as a basis.\n\nThe escalation process may suit the community integrating it, then as finding the roles, one may find a collective track to find the best way to mitigate issues. As a minimal process, the project owner must leave an explicit way to report abuse. As a reminder, it is an overall community member's responsibility to report violations of the present code of conduct. That report is to be done in an anonymous way to prevent retaliation. The people responsible for facilitation can keep track of the report, find the best people to mitigate it within the community (it can be himself), and the mediation can begin.\n\nThen comes the mediation, for a low-importance violation, the mediator can facilitate communication and take action against the abuse. In case of public violation, the impactful content is to be removed. Actions may be temporary or permanent bans from the community for example. The issue of the mediation may be tracked by the facilitator for audit purposes but also to mediate if the violation comes up again. That record may be public or private. He can also find ways to improve the escalation process or trigger a role redefinition within the community.\n\nIn case of grave violation like any against applicable law, the authorities are to be warned and the overall community has the duty to facilitate their work.\n\n## 7. Ethical goals\n\nThis frame of contributions may help to promote ethical goals. Before this, contributions may not imply in any manner a step against those goals, knowing that going toward them aims for us to live in a better place which is the purpose of the current document. This community aims to:\n\n- fight against slavery and forced labor, empower labor law\n- empower human rights\n- empower diversity, equity, and inclusion\n\nThe frame helps to have the tools to get mutual aid to go toward those goals.\n\n> Exemplarity is not required, goodwill is.\n\nOWNER CONTACT:\npascal@malach.it\n\n<p xmlns:cc=\"http://creativecommons.org/ns#\" >This code of conduct is licensed under <a href=\"http://creativecommons.org/licenses/by-nc-sa/4.0/?ref=chooser-v1\" target=\"_blank\" rel=\"license noopener noreferrer\" style=\"display:inline-block;\">Attribution-NonCommercial-ShareAlike 4.0 International</a></p>\n"
  },
  {
    "path": "Dockerfile.admin",
    "content": "FROM node:25.2.1 AS assets\n\n# For packages not compatible with OpenSSL 3.0 https://nodejs.org/en/blog/release/v17.0.0/\nENV NODE_OPTIONS=--openssl-legacy-provider\n\nWORKDIR /app\n\nCOPY ./apps/boruta_admin/assets /app\n\nRUN npm ci\nRUN npm run build\n\nFROM elixir:1.14-otp-25-alpine AS builder\n\nRUN apk --no-cache --update add build-base git\n\nENV MIX_ENV=prod\n\nRUN mix local.hex --force\nRUN mix local.rebar --force\n\nWORKDIR /app\nCOPY . .\nCOPY --from=assets /priv/static/assets ./apps/boruta_admin/priv/static/assets\nRUN rm -rf deps\nRUN mix do clean, deps.get\nRUN mix compile\n\nWORKDIR /app/apps/boruta_admin\nRUN mix phx.digest\n\nWORKDIR /app\nRUN mix release boruta_admin --force --overwrite\n\nFROM elixir:1.14-otp-25-alpine\n\nWORKDIR /app\n\nCOPY --from=builder /app/_build/prod/rel/boruta_admin ./\n\nCMD [\"/bin/sh\", \"-c\", \"/app/bin/boruta_admin start\"]\n"
  },
  {
    "path": "Dockerfile.auth",
    "content": "FROM node:25.2.1 AS identity_assets\n\nARG BORUTA_OAUTH_BASE_URL\n\n# For packages not compatible with OpenSSL 3.0 https://nodejs.org/en/blog/release/v17.0.0/\nENV NODE_OPTIONS=--openssl-legacy-provider\n\nWORKDIR /app/wallet\n\nCOPY ./apps/boruta_identity/assets /app\n\nRUN npm ci\nRUN npm run build\n\nFROM elixir:1.14-otp-25-alpine AS builder\n\nRUN apk --no-cache --update add build-base git\n\nENV MIX_ENV=prod\n\nRUN mix local.hex --force\nRUN mix local.rebar --force\n\nWORKDIR /app\nCOPY . .\nRUN rm -rf deps\nRUN mix do clean, deps.get\nRUN mix compile\n\nCOPY --from=identity_assets /priv ./apps/boruta_identity/priv/\nWORKDIR /app/apps/boruta_identity\nRUN mix phx.digest\nWORKDIR /app/apps/boruta_web\nRUN mix phx.digest\n\nWORKDIR /app\nRUN mix release boruta_auth --force --overwrite\n\nFROM elixir:1.14-otp-25-alpine\n\nWORKDIR /app\n\nCOPY --from=builder /app/_build/prod/rel/boruta_auth ./\n\nCMD [\"/bin/sh\", \"-c\", \"/app/bin/boruta_auth start\"]\n"
  },
  {
    "path": "Dockerfile.full",
    "content": "FROM node:25.2.1 AS admin_assets\n\n# For packages not compatible with OpenSSL 3.0 https://nodejs.org/en/blog/release/v17.0.0/\nENV NODE_OPTIONS=--openssl-legacy-provider\n\nWORKDIR /app\n\nCOPY ./apps/boruta_admin/assets /app\n\nRUN npm ci\nRUN npm run build\n\nFROM node:25.2.1 AS identity_assets\n\nARG BORUTA_OAUTH_BASE_URL\n\n# For packages not compatible with OpenSSL 3.0 https://nodejs.org/en/blog/release/v17.0.0/\nENV NODE_OPTIONS=--openssl-legacy-provider\n\nWORKDIR /app/wallet\n\nCOPY ./apps/boruta_identity/assets /app\n\nRUN npm ci\nRUN npm run build\n\nFROM elixir:1.14-otp-25-alpine AS builder\n\nRUN apk --no-cache --update add build-base git\n\nENV MIX_ENV=prod\n\nRUN mix local.hex --force\nRUN mix local.rebar --force\n\nWORKDIR /app\nCOPY . .\nRUN rm -rf deps\nRUN mix do clean, deps.get\nRUN mix compile\n\nCOPY --from=admin_assets /priv/static/assets ./apps/boruta_admin/priv/static/assets\nCOPY --from=identity_assets /priv/static/wallet ./apps/boruta_identity/priv/static/wallet\n\nWORKDIR /app/apps/boruta_admin\nRUN mix phx.digest\nWORKDIR /app/apps/boruta_identity\nRUN mix phx.digest\nWORKDIR /app/apps/boruta_web\nRUN mix phx.digest\n\nWORKDIR /app\nRUN mix release boruta --force --overwrite\n\nFROM elixir:1.14-otp-25-alpine\n\nWORKDIR /app\n\nCOPY --from=builder /app/_build/prod/rel/boruta ./\n\n# File used for gateway static configuration, used in combination with `BORUTA_GATEWAY_CONFIGURATION_PATH` environment variable\nCOPY /static_config/example-gateway-configuration.yml config/example-gateway-configuration.yml\n\nCOPY /static_config/example-httpbin-configuration.yml config/example-httpbin-configuration.yml\nCOPY /static_config/example-protected-httpbin-configuration.yml config/example-protected-httpbin-configuration.yml\n\nCMD [\"/bin/sh\", \"-c\", \"/app/bin/boruta start\"]\n"
  },
  {
    "path": "Dockerfile.gateway",
    "content": "FROM elixir:1.14-otp-25-alpine AS builder\n\nRUN apk --no-cache --update add build-base git\n\nENV MIX_ENV=prod\n\nRUN mix local.hex --force\nRUN mix local.rebar --force\n\nWORKDIR /app\nCOPY . .\nRUN rm -rf deps\nRUN mix do clean, deps.get\nRUN mix compile\n\nWORKDIR /app\nRUN mix release boruta_gateway --force --overwrite\n\nFROM elixir:1.14-otp-25-alpine\n\nWORKDIR /app\n\nCOPY --from=builder /app/_build/prod/rel/boruta_gateway ./\n\n# File used for gateway static configuration, used in combination with `BORUTA_GATEWAY_CONFIGURATION_PATH` environment variable\nCOPY /static_config/example-gateway-configuration.yml config/example-gateway-configuration.yml\n\nCOPY /static_config/example-httpbin-configuration.yml config/example-httpbin-configuration.yml\nCOPY /static_config/example-protected-httpbin-configuration.yml config/example-protected-httpbin-configuration.yml\n\nCMD [\"/bin/sh\", \"-c\", \"/app/bin/boruta_gateway start\"]\n"
  },
  {
    "path": "GENERAL_TERMS_AND_CONDITIONS.md",
    "content": "### General Terms and Conditions for Boruta (Open Beta)\n\n#### **1. Introduction**\nWelcome to the open beta of Boruta, an identity and access management solution developed and maintained by Malachit EI. By participating in the beta, you agree to these General Terms and Conditions (\"Terms\"). Boruta is provided as-is for testing purposes and may not be fully optimized for all production use cases.\n\nThese Terms are supplementary to the Apache 2.0 License under which Boruta is licensed. In the event of a conflict, the Apache 2.0 License governs the open-source aspects of Boruta.\n\n---\n\n#### **2. Eligibility**\nParticipation in the Boruta beta is open to all individuals and organizations, provided they comply with applicable laws and these Terms. By using Boruta, you affirm that you are at least 18 years old or have obtained consent from a legal guardian.\n\n---\n\n#### **3. Scope of Use**\nThe beta software is made available for testing and feedback purposes. Users are encouraged to explore Boruta's features, report issues, and share suggestions. Malachit recommends against using the beta version in production environments for critical applications.\n\n---\n\n#### **4. User Responsibilities**\nUsers agree to:\n- Use Boruta solely for lawful purposes and in compliance with all applicable laws and regulations.\n- Avoid using Boruta in ways that could harm, disable, overburden, or impair the software, its infrastructure, or third-party services connected to it.\n- Refrain from introducing malicious code (e.g., viruses, worms) or attempting to exploit vulnerabilities in Boruta or its associated systems.\n- Use Boruta in compliance with its documentation and avoid reverse engineering, decompiling, or disassembling the software unless expressly permitted under the Apache 2.0 License.\n- Ensure that any identity information collected through Boruta is handled in compliance with applicable data protection and privacy laws. Users acknowledge that they are solely responsible for the proper handling, storage, and use of such information.\n\n---\n\n#### **5. Prohibited Activities**\nUsers may not:\n- Use Boruta for unlawful, unethical, or harmful activities, including but not limited to unauthorized access to systems, identity theft, fraud, or spamming.\n- Attempt to circumvent security measures or protections implemented within Boruta.\n- Misrepresent the software or its capabilities to others, especially in production environments.\n- Share or distribute Boruta in a manner inconsistent with its open-source license (Apache 2.0).\n\n---\n\n#### **6. Feedback and Contributions**\nFeedback is critical to improving Boruta. Users may report bugs, suggest features, and provide other feedback through GitHub issues. By submitting feedback:\n- You grant Malachit a non-exclusive, royalty-free, perpetual, and irrevocable license to use, modify, and distribute your contributions.\n- Significant contributors may be acknowledged publicly in the project documentation or release notes.\n\nFor security-related or sensitive issues, please report directly via email at pascal@malach.it.\n\nAdditionally, Boruta provides a feedback form under the io.malach.it [Privacy Policy](https://io.malach.it/privacy-policy.html), ensuring compliance with data protection standards.\n\n---\n\n#### **7. Stability and Updates**\nWhile Boruta is stable for general testing, it may not fully support all production environments. Users should exercise caution when deploying Boruta in critical systems.\n- **Updates**: Monthly releases are planned during the beta phase. Updates may include new features, bug fixes, or breaking changes.\n- **Changelog**: Each release will be accompanied by a detailed changelog on GitHub, outlining updates, known issues, and any migration steps for breaking changes.\n- **Data Recovery**: While Malachit will make best efforts to preserve user data during updates, some updates may cause data loss. Such instances will be noted in the changelog.\n\n---\n\n#### **8. Communication and Notifications**\n- **Updates**: All updates and announcements will be shared on the project’s GitHub repository.\n- **Future Notifications**: To receive notifications about future releases and updates, users can sign up via the contact form at [https://io.malach.it](https://io.malach.it). The contact form complies with the io.malach.it [Privacy Policy](https://io.malach.it/privacy-policy.html).\n- **Support**: For inquiries, users may contact pascal@malach.it. Malachit aims to respond within three business days but does not guarantee response times during the beta.\n\n---\n\n#### **9. Privacy and Data Use**\n- The contact form on [https://io.malach.it](https://io.malach.it) collects personal data, such as email addresses, to facilitate notifications about Boruta updates.\n- Data collected through the contact form will be handled in accordance with the io.malach.it [Privacy Policy](https://io.malach.it/privacy-policy.html).\n- Malachit does not collect or retain sensitive user data through the beta software unless explicitly stated.\n\n---\n\n#### **10. Indemnity**\nUsers agree to indemnify and hold harmless Malachit, its affiliates, and contributors from any claims, damages, liabilities, or expenses arising from:\n- Misuse of Boruta.\n- Violation of these Terms.\n- Unauthorized deployment or alteration of Boruta.\n- Improper handling, collection, or storage of identity information collected by users through Boruta.\n\n---\n\n#### **11. Liability Disclaimer**\n- Boruta is provided \"as-is\" during the beta phase without warranties of any kind, express or implied, including but not limited to fitness for a particular purpose or non-infringement.\n- Malachit disclaims liability for any damages arising from:\n  - Improper use of Boruta or failure to adhere to its documentation.\n  - Unauthorized modifications or alterations made by users.\n  - Use of Boruta in high-risk environments where failure could lead to severe damages, such as critical infrastructure or life-support systems, without explicit prior approval from Malachit.\n- Malachit reserves the right to restrict access to the beta for users engaged in misuse or harmful activities.\n\n---\n\n#### **12. Termination**\nMalachit reserves the right to terminate or modify the beta program at any time. Upon termination, Boruta will remain accessible as software under the Apache 2.0 license.\n\n---\n\n#### **13. Transition to Full Release**\n\nAt the end of the beta phase, Boruta will transition to its full release while remaining open source under the Apache 2.0 License. Malachit reserves the right to release additional features, tools, or related services under separate terms. Users will be notified of the transition and encouraged to update to the latest version.\n\n---\n\n#### **14. Governing Law**\nThese Terms are governed by the laws of France. Malachit is registered as an Entreprise Individuelle (EI) under French law. Any disputes arising from these Terms will be resolved under the exclusive jurisdiction of the courts in France.\n\n---\n\n#### **15. Contact Information**\nFor any questions or concerns regarding Boruta or these Terms, please contact:\n\nPascal Knoth, Malachit\n\nEmail: pascal@malach.it\n\nWebsite: [https://io.malach.it](https://io.malach.it)\n\n"
  },
  {
    "path": "LICENSE.md",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2022-2026 - Pascal Knoth (patatoid - malachit)\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "![logo-yellow](images/logo-yellow.png)\n\nboruta is a standalone authorization server that aims to implement OAuth 2.0 and Openid Connect up to decentralized identity specifications. It provides administration tools and a customizable identity provider out of the box to manage authorization, but also an experimental gateway to apply access rules to incoming traffic.\n\n## Status\n\nboruta is currently in an __open beta phase__, if you are interested in the project and the tools it provides, you are encourage to test the product within your context. Production readyness will depend on your feedback, helping to fix the possible bugs and improve the solution usability. While being mostly stable and security asessed for some of its parts, boruta is a work in progress, please read the [General Terms and Conditions](GENERAL_TERMS_AND_CONDITIONS.md).\n\n## Implemented specifications and certification\n\nAs it, boruta server aim to follow the RFCs from IETF:\n- [RFC 6749 - The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749)\n- [RFC 7662 - OAuth 2.0 Token Introspection](https://tools.ietf.org/html/rfc7662)\n- [RFC 7009 - OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009)\n- [RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients](https://tools.ietf.org/html/rfc7636)\n- [RFC 7521 - Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants](https://www.rfc-editor.org/rfc/rfc7521)\n- [RFC 7523 - JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://tools.ietf.org/html/rfc7523)\n- [RFC 9449 - OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop)\n- [RFC 9126 - OAuth 2.0 Pushed Authorization Requests](https://datatracker.ietf.org/doc/html/rfc9126)\n\nAnd the specifications from the OpenID Foundation:\n- [OpenID Connect core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)\n- [OpenID Connect Dynamic Client Registration 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-registration-1_0.html)\n- [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)\n- [Self-Issued OpenID Provider v2](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html)\n- [OpenID for Verifiable Presentations - draft 21](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html)\n\nThis server has been certified for the Basic, Implicit, and Hybrid OpenID Provider profiles by the OpenID Foundation on October, 18th 2022 for the tagged versions 0.1.0 and 0.5.0\n\nThis server has been certified for the Config and Dynamic OpenID Provider profiles by the OpenID Foundation on May, 16th 2023 for the tagged version 0.2.0\n\nThis server has also been certified against the [European Blockchain Service Infrastructure (EBSI)](https://ec.europa.eu/digital-building-blocks/sites/display/EBSI) issuance test suite for the tagged version 0.4.0 and for verifiable credential verification for the tagged version 0.5.0.\n\n![EBSI certified - issue](https://github.com/malach-it/boruta-server/blob/master/images/ebsi-certification-issuance.png?raw=true)\n![EBSI certified - verify](https://github.com/malach-it/boruta-server/blob/master/images/ebsi-certification-verify.png?raw=true)\n![OpenID certified](https://github.com/malach-it/boruta-server/blob/master/images/oid-certification-mark.png?raw=true)\n\n## Documentation\n\nServer documentation is available on github pages [here](https://malach-it.github.io/developers.boruta/docs/intro). It highlights how the server works, describing its architecture, parameters and the associated authentication / authorization flows. It is a Work In Progress, all feedback or contributions would be welcomed.\n\n## DID creation and resolution\n\nboruta may use [Universal resolver](https://github.com/decentralized-identity/universal-resolver) for DID resolution and [Universal registrar](https://github.com/decentralized-identity/universal-registrar) for DID creation. Those are to be configured as environment variables, respectively `DID_RESOLVER_BASE_URL` and `DID_REGISTRAR_BASE_URL`. DIDs are used in the decentralized identity flows and are present as key identifier header of the other generated JWTs.\n\n## Integrated wallet\n\nThis project includes a demo wallet implemented for testing purposes. It is compliant with the decentralized identity flows but does not have neither secure storage nor portability features which makes it not production ready. Note that even if those features come in further releases, this wallet does not target to be an EUDI wallet as the European framework states.\n\n## Installation\n\nA [loom presentation](https://www.loom.com/share/77006360fdac44bc9113fab9cf30aba5) about how to get a server up and running.\n\nNote that the easiest way to try the server is by using docker compose.\n\n### Run an instance from docker\n\n> Note this image is built for x86_64 architecture, for other architectures build yourself the image or use docker compose install that will build the image for your architecture.\n\nA docker image is available at `malachit/boruta-server` on [DockerHub](https://hub.docker.com/r/malachit/boruta-server), you will need a postgres instance installed on your system with credentials provided as environment variables in `.env.*`.\n\n1. Get environment file\n\n```bash\nwget https://raw.githubusercontent.com/malach-it/boruta-server/master/.env.dev\n```\n\nOnce done you will be able to launch the server.\n\n```bash\ndocker run -it --env-file .env.dev --network=host malachit/boruta-server:0.4.0\n```\n\nThe applications will be available on different ports (depending on the values provided in `.env.dev`):\n- http://localhost:4000 for the authorization server\n- http://localhost:4001 for the admin interface\n- http://localhost:4002 for the gateway\n- http://localhost:4003 for the microgateway\n\nAdmin credentials are the one seeded and available in environment file.\n\n### Run an instance from docker-compose\n\nYou can build and run the docker images as follow:\n\n```bash\ndocker-compose up\n```\n\nThe applications will be available on different ports (depending on the docker compose environment configuration):\n- http://localhost:8080 for the authorization server\n- http://localhost:8081 for the admin interface\n- http://localhost:8082 for the gateway\n- http://localhost:8083 for the microgateway\n\nAdmin credentials are the one seeded and available in environment file.\n\n### Requirements\n- Elixir >= 1.13\n- postgreSQL >= 13\n- node >= 16.5 (if you need to prepare assets)\n\n### Run a release from scratch\n\n1. first you need to get project dependencies\n\n```bash\nmix deps.get\n```\n\n2. you need to prepare assets in order for them to be included in the release\n\n```bash\n./scripts/prepare_assets.sh\n```\n\n3. then you can craft the release\n\n```bash\nMIX_ENV=prod mix release boruta\n```\n\nOnce done, you can run the release as follow:\n\n```bash\nenv $(cat .env.example | xargs) _build/prod/rel/boruta/bin/boruta start\n```\n\nThe applications will be available on different ports (depending on the values provided in `.env.example`):\n- http://localhost:8080 for the authorization server\n- http://localhost:8081 for the admin interface\n- http://localhost:8082 for the gateway\n- http://localhost:8083 for the microgateway\n\nAdmin credentials are the one seeded and available in environment file.\n\n### Run a development server\n\n1. first you need to get project dependencies\n\n```bash\nmix deps.get\n```\n\n2. you need to prepare assets in order to fetch javascript dependencies\n\n```bash\n./scripts/prepare_assets.sh\n```\n\n3. because of the forwarding of requests between web and identity modules, you need to add the `/accounts` path prefix in configuration\n\n```diff\n--- a/apps/boruta_identity/config/config.exs\n+++ b/apps/boruta_identity/config/config.exs\n@@ -4,8 +4,8 @@ config :boruta_identity,\n   ecto_repos: [BorutaAuth.Repo, BorutaIdentity.Repo]\n\n config :boruta_identity, BorutaIdentityWeb.Endpoint,\n-  url: [host: \"localhost\"],\n-  # url: [host: \"localhost\", path: \"/accounts\"],\n+  # url: [host: \"localhost\"],\n+  url: [host: \"localhost\", path: \"/accounts\"],\n```\n\nYou now should be able to start the development server\n\n```bash\nenv $(cat .env.dev | xargs) MIX_ENV=dev mix boruta.server\n```\n\nThe applications will be available on different ports (depending on the values provided in `.env.dev`):\n- http://localhost:4000 for the authorization server\n- http://localhost:4001 for the admin interface\n- http://localhost:4002 for the gateway\n- http://localhost:4003 for the microgateway\n\nAdmin credentials are the one seeded and available in environment file.\n\n### Default admin credentials\n\nIn order to authenticate to the administration interface you will be asked for credentials that are by default (seeded from environment variables) `admin@test.test` / `imaynotknowthat`.\n\n## Environment variables\n\n| Variable name                      | description         |\n| ---------------------------------- | ------------------- |\n| `SECRET_KEY_BASE`                  | The Phoenix secret key base. It must be at least 64 cheracters long. |\n| `POSTGRES_USER`                    | The database user provided as credentials in postgreSQL connections. |\n| `POSTGRES_PASSWORD`                | The database password provided as credentials in postgreSQL connections. |\n| `POSTGRES_DATABASE`                | The database name provided in postgreSQL connections. |\n| `POSTGRES_HOST`                    | The database host provided in postgreSQL connections. |\n| `POOL_SIZE`                        | The postgreSQL pool size of each application, the real connection count will be twice that value. |\n| `MAX_LOG_RETENTION_DAYS`           | The number of days the logs are kept to the server. This value defaults to 60. |\n| `K8S_NAMESPACE`                    | If set along with K8S_SELECTOR, it setups libcluster in order to connect boruta erlang nodes in kubernetes together. |\n| `K8S_SELECTOR`                     | If set along with K8S_NAMESPACE, it setups libcluster in order to connect boruta erlang nodes in kubernetes together. |\n| `BORUTA_ADMIN_OAUTH_CLIENT_ID`     | An uuidv4 string representing the admin oauth client id. It will be part of the client seeded in the setup task. |\n| `BORUTA_ADMIN_OAUTH_CLIENT_SECRET` | The admin oauth client secret. It will be part of the client seeded in the setup task. |\n| `BORUTA_ADMIN_OAUTH_BASE_URL`      | The URL base URL of the authorization server admin will use (linked to above client_id and secret, without trailing slash). |\n| `BORUTA_ADMIN_EMAIL`               | The first admin email. It will be part of the user seeded in the setup task. |\n| `BORUTA_ADMIN_PASSWORD`            | The first admin password. It will be part of the user seeded in the setup task. |\n| `BORUTA_ADMIN_HOST`                | The host that represent the host where boruta admin server will be deployed to. |\n| `BORUTA_ADMIN_BIND`                | The IP address the boruta admin server will be bound to. |\n| `BORUTA_ADMIN_PORT`                | The port where boruta admin server will be exposed on. |\n| `BORUTA_ADMIN_BASE_URL`            | The base URL where boruta admin server http endpoint will be deployed to (without trailing slash). |\n| `BORUTA_OAUTH_SCHEME`              | The scheme that will be used for URL building, default to https. |\n| `BORUTA_OAUTH_HOST`                | The host where boruta oauth server will be deployed to. |\n| `BORUTA_OAUTH_BIND`                | The IP address the boruta oauth server will be bound to. |\n| `BORUTA_OAUTH_PORT`                | The port where boruta oauth server will be exposed on. |\n| `BORUTA_OAUTH_BASE_URL`            | The base URL where boruta oauth server http endpoint will be deployed to (without trailing slash). |\n| `BORUTA_GATEWAY_PORT`              | The port where boruta gateway will be exposed on. |\n| `BORUTA_GATEWAY_SIDECAR_PORT`      | The port where boruta microgateway will be exposed on. |\n| `BORUTA_GATEWAY_CONFIGURATION_PATH`| The path containing the gateway static configuration. |\n| `BORUTA_CONFIGURATION_PATH`        | The path containing the boruta static configuration. |\n| `BORUTA_SUB_RESTRICTED`            | If set, the uid of the only user to have access to the administration interface. |\n| `BORUTA_ORGANIZATION_RESTRICTED`   | If set, the uid of the only organization to have access to the administration interface. |\n| `DID_RESOLVER_BASE_URL`            | Did resolver API endpoint, accroding to the [W3C DID resolution specification](https://w3c.github.io/did-resolution/) |\n| `DID_REGISTRAR_BASE_URL`           | Did registrar API endpoint, accroding to the [W3C DID registration specification](https://identity.foundation/did-registration/) |\n| `DID_SERVICES_API_KEY`             | API key granting access to DID revolver and registrar services. |\n\n## Code of Conduct\n\nThis product community follows the code of conduct available [here](CODE_OF_CONDUCT.md)\n\n## License\n\nThis code is released under the [Apache 2.0](LICENSE.md) license.\n\n## General Terms and Conditions\n\nBy using Boruta, you agree to the [General Terms and Conditions](GENERAL_TERMS_AND_CONDITIONS.md), which complement the software's Apache 2.0 License.\n\n## About boruta\n\nThe name boruta comes from a polish legend where he is a gentle devil (an angel, maybe) that is such evil that having him at home makes you safe. He was living during the middle ages in the castle of the little town of Leczyca, since then the people from there have a little figurine of him at home helping the house to be protected from bad fate.\n"
  },
  {
    "path": "ansible/.kube/kubeconfig-k8s-boruta.yaml",
    "content": "$ANSIBLE_VAULT;1.1;AES256\n61666431343966366336333965363036373037353566316363333465633762663931623438373666\n6238346264616633303238616462393462373662343139360a613866366365613266373236383066\n34663165333365633762623961333565396361353439303538363466376463383133653333663130\n6236346365633562390a646266363130666465393530653864363832613533393862353931366238\n31383135303361633766643963633436356331316333343164346563623532393235373432356261\n31313035313235666337383030643766626236366263313537666163653436633332396531626430\n34643964393564343732356262303962633764323330363738633634373436303434313239303833\n66373835623834363165303062626236323034386464373236303233653263633661663465323237\n37636463636363313132663434386134363433643464373131363765633562316366313537346130\n36373065383430643731356362613166613432616336353538336137326331646564376334316430\n37353761633665653937623138333931613564633366346331343438613665346638323762353266\n32363834643236323937393364666531666432393666346634643861613134393137633338666431\n62336361663731356139373433303130643334333831343537326238323863653465323265623963\n64383766656436393336343838313634353731313133363335393235616664346564393862653334\n61323961613132633036326138363166376534616331303433633435313237626133646665656230\n35653264353137643530336365623937653662306535343863336230333732366333343166643930\n62666561383331613335356331393738626163613432393630343431623738656439633536393364\n33636634363663313763613535336264303465386539636530633463623931313730646439353939\n38613434663137643530383165633666323963626262366162333831626363613965313631346163\n35333930633435646239376134356138623431663530323330363335326237626365383436323836\n65646363346137303032363666326134316332313962326438353336643233316239313535396435\n37333937326334653432333931376630363339346435666632386138373230626562366666313163\n31323931383066383762313266393535383165363464386439666461393538353138366265656331\n30323362383634336334376466336565643430613436666265313939646534386563376336646562\n38356562383464326162336639626265393133323533383338613036636237353264393665333862\n34666434323133623030396263653230303534376239363866336366626364323632373663303437\n36363466326533633361373465313635623432363864613866633739656232663038623730353736\n35653338633561376235653761663433353034633630666564303238386437633137373034373836\n39373537323539653832383635353231663162356139373065336430376330373532346164336536\n61343165353365666535393534613366396439653338616333373836376234666335313866386636\n64616636333635303230313632316162353033626364613865333934313764643839353963336561\n38353238396138333066386530643762303064326363396638343731646465643731653335373330\n34643035326533383633333465353132303235663532636239633935386339613037393437356434\n37383538613065356133343038376534346665353136633530343763363935616437616632393438\n32396562626165386633646436626262646132326161636163313231396533633866333564653066\n37326236323335366638633234343763383665616265373064353033396234333937316661326637\n38653434386463363637636566353563626539366637636635366362386561356461303961303830\n64623334303564663931633431616439313939633562343561386665353264663631346634316234\n62643762636531306163656436353664663231316339326663643064376464306433363332333864\n32356161336635626566316362623332356636623062326333383936333130336163663965373134\n34386136303962316131373165656562623766633339373039313931376463626463343366303061\n31373734303034613565373431313736376463316435666136323536633661363433633237333739\n37366638633066643930656133363137396662353765633132623766386339636639393465383133\n61353938373736393439303461373133656537313361383839343038323232363130643333663564\n35353963373136646332323436626337353962343766356235373633353936376333366164333934\n39366366353966656237356438353432666534383039353931376435393761363634323166326336\n66373738356331356132646435333964363230393361366335363633666132386339373138623133\n33363431636333646638636162643434643039346239653930326636303132303535323866666264\n33623834343230353232313832623437303065306637653733383364323332306361383232303831\n39663361386333313238316364396162626161346333343934333565323833383162353734343962\n38373731663034396563613130363861366664323961373437343739386436656432653765313437\n61613966626536653766363162356132643263646235656263343538643035623938353064643431\n34336431346165646339656633336130643336343134633633663638393964326666383231633835\n39313966663064386234643464623966386436666464383636633837373433313663323165383739\n64653963356565363433643338633039316566366630393764343165343134366264393933306339\n34313536343632386565326235613664633261626263623931353938623265323237353338383535\n38363633343838393163636361383835653334303162336632313038316235323565653166646131\n32333462663964306436306461313330353932313034656334303839643135623135323832363232\n34613234316530363439346165373539393231616534303639346163303135313462616231363034\n38366266396238333366363936373763303537353737656330363333616239653430363861386538\n35353232316435663130316134333363366564396639313166396439303065613637356434383666\n34323230636235366262656238646263303936643434363337323561633063303134636134353137\n33306633636135396361373364383533393761633533396361643131346564623562653762303235\n32386262663936333436333634386338333739613665373439333533306362346264343431633339\n34663864313231323565386131653537343037353261616561656635353363626133323362646136\n61353961336136323363646132336266336637616436663535393963366362316634393430383731\n61663065383461643636333938323533616630306332353831663933653062653365646666383030\n39336534373831613834626664363132346530326431323866323163633630373061666436656230\n31633735613130343961323061356461303464643839393565303837303639303864393433663436\n30663834316538353934363439396632306564313631663431353638323633653434343936353936\n62363939316464326366396464313039353266306533306365663963386666643633306238643436\n64323337336130366564303766383137633562373837383963326539383733363139356135663766\n39653631393839363530313034313866383033333861313239346236623935623264313333376330\n62326233393863323365323537653336303661643131316665373538343332396165306463653064\n61393463386331313336613164613630393031633865316139663932323465623263316234623563\n66356363326562336133343163363661373062323531616333376363653362316630616631616335\n66306266623730383935396134666639666266306636353338366332386164656135373166643838\n64386235666265613463343631343164363133613063643466303435313934343965383337303137\n36323133633330303539346539313164313863323363353462303635396233666638353164303834\n63396539356439636432666562333339663666396139373661383836373439356332663337643539\n33366631396662323936636238626266653333393362383963376336386530326664396538316334\n63323263666161666461343334323966376639623439366231643137373733343436623564326332\n32386461613238636664656635613530626239303862316566303530326233363666663162323334\n64643161656166613963633733363636373736656166653062643765373731306533333964333264\n36343063326466613230323638373635636538323932316231346230373963633238646239643134\n32323635383035323638316231653736626666643939363839333461636332386338313532323065\n63363663333764323734663665313036326438313330316338666665326261316132653362303037\n65656138383961373664396463373562663438323864363739373464643632353632366266666465\n30353733643264333837633161653932316135326665643434333230376366643039363165313530\n36356636653965623761303833303533346630656561663531666132396633633861376637333939\n61316536666335653563613032653839343437333664313364373836363263376465383963363139\n34386561326634323062333265333736356232623630356666623061656261373136336566323961\n35303764626462346361396438356634353430333661666138366362303039353066363637346164\n64663232316333393334376335326163333861303536636163353364363362303566383063646465\n30306538623231343137316362656632343032616432323636653638363463626437356538663033\n37643666343166636638636639316331376463636362336531306461323935346236343163356330\n30666662393938653766613336353134656636373366373538363463303861636164\n"
  },
  {
    "path": "ansible/deploy.yml",
    "content": "- hosts: localhost\n  tasks:\n    - name: Add stable bitnami repo\n      kubernetes.core.helm_repository:\n        name: bitnami\n        repo_url: https://charts.bitnami.com/bitnami\n\n    - name: Add stable jetstack repo\n      kubernetes.core.helm_repository:\n        name: jetstack\n        repo_url: https://charts.jetstack.io\n\n    - name: Create a k8s namespace\n      kubernetes.core.k8s:\n        api_version: v1\n        name: boruta-staging\n        kind: Namespace\n        state: present\n\n          # - name: Install cert-manager helm package\n          #   kubernetes.core.helm:\n          #     name: cert-manager\n          #     chart_ref: jetstack/cert-manager\n          #     chart_version: 1.11.1\n          #     release_namespace: cert-manager\n          #     create_namespace: true\n          #     values:\n          #       installCRDs: true\n\n            # upgrade looks to be complicated, variable names contain breaking changes\n            # the master configuration file - https://github.com/bitnami/charts/blob/main/bitnami/postgresql/values.yaml\n            #\n            # - name: Install postgres helm package\n            #   kubernetes.core.helm:\n            #     name: postgres\n            #     chart_ref: bitnami/postgresql\n            #     chart_version: \"12.2.7\"\n            #     release_namespace: boruta-staging\n            #     values:\n            #       global.postgresql.auth.postgresPassword: \"{{ postgresql_password }}\"\n            #       primary.extendedConfiguration: |\n            #         max_connections = 1024\n\n    - name: Create libcluster role\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: rbac.authorization.k8s.io/v1\n          kind: Role\n          metadata:\n            name: libcluster-role\n            namespace: boruta-staging\n          rules:\n          - apiGroups: [\"\"]\n            resources: [\"pods\"]\n            verbs: [\"get\", \"list\", \"watch\"]\n\n    - name: Create libcluster binding\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: rbac.authorization.k8s.io/v1\n          kind: RoleBinding\n          metadata:\n            name: libcluster-bindings\n            namespace: boruta-staging\n          subjects:\n          - kind: ServiceAccount\n            name: default\n            namespace: boruta-staging\n          roleRef:\n            kind: Role\n            name: libcluster-role\n            apiGroup: rbac.authorization.k8s.io\n\n    - name: Create SSL let's encrypt certificate\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: cert-manager.io/v1\n          kind: ClusterIssuer\n          metadata:\n            name: letsencrypt-production\n          spec:\n            acme:\n              email: io.pascal.knoth@gmail.com\n              server: https://acme-v02.api.letsencrypt.org/directory\n              privateKeySecretRef:\n                name: boruta-staging-ssl-account-key\n              solvers:\n              - http01:\n                  ingress:\n                    class: nginx\n                selector:\n                  dnsZones:\n                  - 'boruta.patatoid.fr'\n\n    - name: Create Boruta Ingress\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: networking.k8s.io/v1\n          kind: Ingress\n          metadata:\n            name: main-ingress\n            namespace: boruta-staging\n            annotations:\n              kubernetes.io/ingress.class: \"nginx\"\n              cert-manager.io/cluster-issuer: letsencrypt-production\n            #   nginx.ingress.kubernetes.io/rewrite-target: /\n          spec:\n            rules:\n            - host: \"{{ gateway_host }}\"\n              http:\n                paths:\n                - path: /\n                  pathType: Prefix\n                  backend:\n                    service:\n                      name: boruta-gateway\n                      port:\n                        number: 5000\n            - host: \"{{ httpbin_sidecar_host }}\"\n              http:\n                paths:\n                - path: /\n                  pathType: Prefix\n                  backend:\n                    service:\n                      name: boruta-httpbin-sidecar\n                      port:\n                        number: 5001\n            - host: \"{{ protected_httpbin_sidecar_host }}\"\n              http:\n                paths:\n                - path: /\n                  pathType: Prefix\n                  backend:\n                    service:\n                      name: boruta-protected-httpbin-sidecar\n                      port:\n                        number: 5001\n            - host: \"{{ oauth_host }}\"\n              http:\n                paths:\n                - path: /\n                  pathType: Prefix\n                  backend:\n                    service:\n                      name: boruta-oauth\n                      port:\n                        number: 4001\n            - host: \"{{ admin_host }}\"\n              http:\n                paths:\n                - path: /\n                  pathType: Prefix\n                  backend:\n                    service:\n                      name: boruta-admin\n                      port:\n                        number: 4002\n            tls:\n              - hosts:\n                - \"{{ oauth_host }}\"\n                - \"{{ admin_host }}\"\n                - \"{{ gateway_host }}\"\n                - \"{{ httpbin_sidecar_host }}\"\n                - \"{{ protected_httpbin_sidecar_host }}\"\n                secretName: boruta-staging-cert\n\n    - name: create boruta app ConfigMap\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: v1\n          kind: ConfigMap\n          metadata:\n            name: boruta-env\n            namespace: boruta-staging\n          data:\n            SECRET_KEY_BASE: \"{{ secret_key_base }}\"\n            POSTGRES_USER: postgres\n            POSTGRES_PASSWORD: \"{{ postgresql_password }}\"\n            POSTGRES_DATABASE: boruta\n            POSTGRES_HOST: postgres-postgresql\n            MAX_LOG_RETENTION_DAYS: \"180\"\n            K8S_NAMESPACE: boruta-staging\n            K8S_SELECTOR: app=boruta\n            BORUTA_ADMIN_OAUTH_CLIENT_ID: \"{{ admin_client_id }}\"\n            BORUTA_ADMIN_OAUTH_CLIENT_SECRET: \"{{ admin_client_secret }}\"\n            BORUTA_ADMIN_OAUTH_BASE_URL: \"{{ oauth_base_url }}\"\n            BORUTA_ADMIN_EMAIL: \"{{ admin_email }}\"\n            BORUTA_ADMIN_PASSWORD: \"{{ admin_password }}\"\n            BORUTA_ADMIN_HOST: \"{{ admin_host }}\"\n            BORUTA_ADMIN_PORT: \"4002\"\n            BORUTA_ADMIN_BASE_URL: \"{{ boruta_base_url }}\"\n            BORUTA_OAUTH_HOST: \"{{ oauth_host }}\"\n            BORUTA_OAUTH_PORT: \"4001\"\n            BORUTA_OAUTH_BASE_URL: \"{{ oauth_base_url }}\"\n            BORUTA_GATEWAY_PORT: \"5000\"\n            BORUTA_GATEWAY_SIDECAR_PORT: \"5001\"\n            BORUTA_ORGANIZATION_RESTRICTED: \"{{ boruta_organization_restricted }}\"\n            DID_SERVICES_API_KEY: \"{{ did_services_api_key }}\"\n            POOL_SIZE: \"5\"\n\n    - name: Setup boruta auth database\n      register: database_setup\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: batch/v1\n          kind: Job\n          metadata:\n            name: boruta-auth-setup\n            namespace: boruta-staging\n          backoffLimit: 3\n          spec:\n            template:\n              spec:\n                containers:\n                  - image: \"ghcr.io/malach-it/boruta-server:master\"\n                    command: [\"/app/bin/boruta\"]\n                    args: [\"eval\", \"BorutaWeb.Release.setup\"]\n                    envFrom:\n                    - configMapRef:\n                        name: boruta-env\n                    imagePullPolicy: Always\n                    name: boruta\n                restartPolicy: OnFailure\n                imagePullSecrets:\n                  - name: dockerconfigjson-github-com\n\n    - name: Create Boruta logs persistent volume claim\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: v1\n          kind: PersistentVolumeClaim\n          metadata:\n            name: logs-pvc\n            namespace: boruta-staging\n          spec:\n            storageClassName: standard-rwo\n            accessModes:\n              - ReadWriteOnce # TODO will cause issues when running on multiple nodes\n            resources:\n              requests:\n                storage: 5Gi\n\n    - name: Get blue/green deployment status\n      kubernetes.core.k8s_info:\n        kind: Service\n        namespace: boruta-staging\n        label_selectors:\n          - \"app=boruta\"\n      register: boruta_services\n\n    - name: Create Boruta blue/green deployment\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: apps/v1\n          kind: Deployment\n          metadata:\n            name: \"{{ 'boruta-green' if boruta_services.resources[0].spec.selector.deployment == 'boruta-blue' else 'boruta-blue' }}\"\n            namespace: boruta-staging\n          spec:\n            replicas: 1\n            strategy:\n              type: Recreate\n              rollingUpdate: null\n            selector:\n              matchLabels:\n                deployment: \"{{ 'boruta-green' if boruta_services.resources[0].spec.selector.deployment == 'boruta-blue' else 'boruta-blue' }}\"\n            template:\n              metadata:\n                name: boruta\n                labels:\n                  deployment: \"{{ 'boruta-green' if boruta_services.resources[0].spec.selector.deployment == 'boruta-blue' else 'boruta-blue' }}\"\n                  app: boruta\n              spec:\n                volumes:\n                  - name: logs\n                    persistentVolumeClaim:\n                      claimName: logs-pvc\n                containers:\n                  - image: \"ghcr.io/malach-it/boruta-server:{{ release_tag }}\"\n                    readinessProbe:\n                      httpGet:\n                        path: /healthcheck\n                        port: 4001\n                    env:\n                      - name: BORUTA_GATEWAY_CONFIGURATION_PATH\n                        value: \"./config/example-gateway-configuration.yml\"\n                    envFrom:\n                      - configMapRef:\n                          name: boruta-env\n                    imagePullPolicy: Always\n                    name: boruta\n                    volumeMounts:\n                      - mountPath: \"/app/log\"\n                        name: logs\n                imagePullSecrets:\n                - name: dockerconfigjson-github-com\n\n    - name: Wait for deployment readyness\n      shell: \"kubectl -n boruta-staging wait --for=condition=Ready po --all -l deployment={{ 'boruta-green' if boruta_services.resources[0].spec.selector.deployment == 'boruta-blue' else 'boruta-blue' }}\"\n      retries: 6\n      delay: 5\n      register: result\n      until: result.rc == 0\n\n    - name: Create OAuth Service\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: v1\n          kind: Service\n          metadata:\n            name: boruta-oauth\n            namespace: boruta-staging\n            labels:\n              app: boruta\n          spec:\n            selector:\n              deployment: \"{{ 'boruta-green' if boruta_services.resources[0].spec.selector.deployment == 'boruta-blue' else 'boruta-blue' }}\"\n            ports:\n            - protocol: TCP\n              targetPort: 4001\n              name: oauth-tcp\n              port: 4001\n\n    - name: Create Admin Service\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: v1\n          kind: Service\n          metadata:\n            name: boruta-admin\n            namespace: boruta-staging\n            labels:\n              app: boruta\n          spec:\n            selector:\n              deployment: \"{{ 'boruta-green' if boruta_services.resources[0].spec.selector.deployment == 'boruta-blue' else 'boruta-blue' }}\"\n            ports:\n            - protocol: TCP\n              targetPort: 4002\n              name: admin-tcp\n              port: 4002\n\n    - name: Create Gateway Service\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: v1\n          kind: Service\n          metadata:\n            name: boruta-gateway\n            namespace: boruta-staging\n            labels:\n              app: boruta\n          spec:\n            selector:\n              deployment: \"{{ 'boruta-green' if boruta_services.resources[0].spec.selector.deployment == 'boruta-blue' else 'boruta-blue' }}\"\n            ports:\n            - protocol: TCP\n              targetPort: 5000\n              name: gateway-tcp\n              port: 5000\n\n    - name: Create httpbin sidecar Service\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: v1\n          kind: Service\n          metadata:\n            name: boruta-httpbin-sidecar\n            namespace: boruta-staging\n            labels:\n              app: boruta-httpbin\n          spec:\n            selector:\n              deployment: \"{{ 'boruta-httpbin-green' if boruta_services.resources[0].spec.selector.deployment == 'boruta-httpbin-blue' else 'boruta-httpbin-blue' }}\"\n            ports:\n            - protocol: TCP\n              targetPort: 5001\n              name: httpbin-tcp\n              port: 5001\n\n    - name: Create protected httpbin sidecar Service\n      kubernetes.core.k8s:\n        state: present\n        definition:\n          apiVersion: v1\n          kind: Service\n          metadata:\n            name: boruta-protected-httpbin-sidecar\n            namespace: boruta-staging\n            labels:\n              app: boruta-protected-httpbin\n          spec:\n            selector:\n              deployment: \"{{ 'boruta-protected-httpbin-green' if boruta_services.resources[0].spec.selector.deployment == 'boruta-protected-httpbin-blue' else 'boruta-protected-httpbin-blue' }}\"\n            ports:\n            - protocol: TCP\n              targetPort: 5001\n              name: gateway-tcp\n              port: 5001\n\n    - name: Delete Boruta blue/green deployment\n      kubernetes.core.k8s:\n        state: absent\n        definition:\n          apiVersion: apps/v1\n          kind: Deployment\n          metadata:\n            name: \"{{ 'boruta-blue' if boruta_services.resources[0].spec.selector.deployment == 'boruta-blue' else 'boruta-green' }}\"\n            namespace: boruta-staging\n"
  },
  {
    "path": "ansible/hosts",
    "content": "[local]\nlocalhost\n"
  },
  {
    "path": "ansible/inventories/gke",
    "content": "[gke]\nlocalhost\n\n[gke:vars]\nansible_connection=local\n"
  },
  {
    "path": "ansible/inventories/local",
    "content": "[local]\nlocalhost\n\n[local:vars]\nansible_connection=local\n"
  },
  {
    "path": "ansible/inventories/scaleway",
    "content": "[scaleway]\nlocalhost\n\n[scaleway:vars]\nansible_connection=local\n"
  },
  {
    "path": "apps/boruta_admin/.formatter.exs",
    "content": "[\n  import_deps: [:ecto, :phoenix],\n  inputs: [\"*.{ex,exs}\", \"priv/*/seeds.exs\", \"{config,lib,test}/**/*.{ex,exs}\"],\n  subdirectories: [\"priv/*/migrations\"]\n]\n"
  },
  {
    "path": "apps/boruta_admin/.gitignore",
    "content": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up here.\n/cover/\n\n# The directory Mix downloads your dependencies sources to.\n/deps/\n\n# Where 3rd-party dependencies like ExDoc output generated docs.\n/doc/\n\n# Ignore .fetch files in case you like to edit your project deps locally.\n/.fetch\n\n# If the VM crashes, it generates a dump, let's ignore it too.\nerl_crash.dump\n\n# Also ignore archive artifacts (built via \"mix archive.build\").\n*.ez\n\n# Ignore package tarball (built via \"mix hex.build\").\nboruta_admin-*.tar\n\n# If NPM crashes, it generates a log, let's ignore it too.\nnpm-debug.log\n\n# The directory NPM downloads your dependencies sources to.\n/assets/node_modules/\n\n# Since we are building assets from assets/,\n# we ignore priv/static. You may want to comment\n# this depending on your deployment strategy.\n/priv/static/\n"
  },
  {
    "path": "apps/boruta_admin/assets/.browserslistrc",
    "content": "> 1%\nlast 2 versions\n"
  },
  {
    "path": "apps/boruta_admin/assets/.editorconfig",
    "content": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": "apps/boruta_admin/assets/.eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  env: {\n    node: true\n  },\n  'extends': [\n    'plugin:vue/essential',\n    '@vue/standard'\n  ],\n  plugins: ['prettier'],\n  rules: {\n    'prettier/prettier': 'error',\n    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',\n    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',\n    'camelcase': 'off',\n    'prefer-promise-reject-errors': 'off'\n  },\n  parserOptions: {\n    parser: '@babel/eslint-parser'\n  },\n  requireConfigFile: false\n}\n"
  },
  {
    "path": "apps/boruta_admin/assets/.gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "apps/boruta_admin/assets/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Phoenix Framework</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha512-8bHTC73gkZ7rZ7vpqUQThUDhqcNFyYi2xgDgPDHc+GXVGHXq+xPjynxIopALmOPqzo9JZj0k6OqqewdGO3EsrQ==\" crossorigin=\"anonymous\" />\n    <script>\n      window.env = {\n        BORUTA_ADMIN_OAUTH_CLIENT_ID: '6a2f41a3-c54c-fce8-32d2-0324e1c32e20',\n        BORUTA_ADMIN_OAUTH_BASE_URL: 'http://localhost:4000',\n        BORUTA_ADMIN_BASE_URL: 'http://localhost:4001',\n        BORUTA_ADMIN_BASE_SOCKET_URL: 'ws://localhost:4001'\n      }\n    </script>\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but boruta-admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n    <script type=\"module\" src=\"/src/main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_admin/assets/jsconfig.json",
    "content": "{\n  \"include\": [\n    \"./src/**/*\"\n  ]\n}\n"
  },
  {
    "path": "apps/boruta_admin/assets/package.json",
    "content": "{\n  \"name\": \"boruta-admin\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vite\",\n    \"build\": \"vite build --emptyOutDir\",\n    \"build:watch\": \"vite build --watch --emptyOutDir\",\n    \"deploy\": \"vite build\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^1.15.2\",\n    \"boruta-client\": \"github:malach-it/boruta-client\",\n    \"chartjs-adapter-moment\": \"^1.0.0\",\n    \"codejar\": \"^3.5.0\",\n    \"core-js\": \"^2.6.5\",\n    \"google-palette\": \"^1.1.0\",\n    \"jwt-decode\": \"^3.1.2\",\n    \"lodash\": \"^4.18.1\",\n    \"moment\": \"^2.29.4\",\n    \"phoenix\": \"^1.5.7\",\n    \"phoenix-socket\": \"^1.2.3\",\n    \"prismjs\": \"^1.26.0\",\n    \"semantic-ui-css\": \"^2.4.1\",\n    \"socket.io-client\": \"^4.2.0\",\n    \"vue\": \"^3.2.13\",\n    \"vue-chart-3\": \"^3.1.8\",\n    \"vue-router\": \"^4.0.12\",\n    \"vuex\": \"^4.0.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/eslint-parser\": \"^7.18.9\",\n    \"@sveltejs/vite-plugin-svelte\": \"^5.0.3\",\n    \"@vitejs/plugin-vue\": \"^5.2.3\",\n    \"@vue/eslint-config-standard\": \"^4.0.0\",\n    \"autoprefixer\": \"^10.4.2\",\n    \"chai\": \"^4.1.2\",\n    \"eslint\": \"^8.7.0\",\n    \"eslint-plugin-prettier\": \"^4.2.1\",\n    \"eslint-plugin-vue\": \"^8.3.0\",\n    \"prettier\": \"2.7.1\",\n    \"sass\": \"^1.48.0\",\n    \"vite\": \"^6.3.4\",\n    \"vite-plugin-node-polyfills\": \"^0.23.0\",\n    \"vite-plugin-singlefile\": \"^2.2.0\"\n  }\n}\n"
  },
  {
    "path": "apps/boruta_admin/assets/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    autoprefixer: {}\n  }\n}\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/App.vue",
    "content": "<template>\n  <router-view></router-view>\n</template>\n\n<script>\nexport default {\n  name: 'App'\n}\n</script>\n\n<style lang=\"scss\">\nbody {\n  min-height: 100%;\n  height: auto;\n  width: 100%;\n  overflow-x: hidden;\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Breadcrumb.vue",
    "content": "<template>\n  <div class=\"ui breadcrumb\">\n    <span v-for=\"(item, index) in items\" :key=\"item.path\">\n      <span v-if=\"index + 1 < items.length\">\n        <router-link :to=\"item.path\" class=\"section\">{{ item.label }}</router-link>\n        <i class=\"right angle icon divider\"></i>\n      </span>\n      <span class=\"section\" v-else>\n        {{ item.label }}\n      </span>\n    </span>\n  </div>\n  <hr />\n</template>\n\n<script>\nconst labels = {\n  'root': 'Home',\n  'not-found': 'Not found',\n  'bad-request': 'Bad request',\n  'dashboard': 'Dashboard',\n  'request-logs': 'Requests',\n  'business-event-logs': 'Business Events',\n  'identity-providers': \"Identity providers\",\n  'new-identity-provider': 'Create',\n  'identity-provider': ({ params }) => params.identityProviderId,\n  'edit-layout-template': 'Edit layout template',\n  'edit-session-template': 'Edit login template',\n  'edit-totp-registration-template': 'Edit TOTP registration template',\n  'edit-totp-authentication-template': 'Edit TOTP authentication template',\n  'edit-webauthn-registration-template': 'Edit Webauthn registration template',\n  'edit-webauthn-authentication-template': 'Edit Webauthn authentication template',\n  'edit-choose-session-template': 'Edit choose session template',\n  'edit-registration-template': 'Edit registration template',\n  'edit-edit-user-template': 'Edit user edition template',\n  'edit-new-reset-password-template': 'Edit send reset password instructions template',\n  'edit-edit-reset-password-template': 'Edit reset password template',\n  'edit-new-confirmation-template': 'Edit send confirmation instructions template',\n  'edit-new-consent-template': 'Edit consent template',\n  'edit-credential-offer-template': 'Edit credential offer template',\n  'edit-cross-device-presentation-template': 'Edit verifiable presentation template',\n  'backends': 'Backends',\n  'backend': ({ params }) => params.backendId,\n  'new-backend': 'Create',\n  'edit-confirmation-instructions-email-template': 'Edit confirmation instructions email template',\n  'edit-reset-password-instructions-email-template': 'Edit reset password instructions email template',\n  'edit-tx-code-email-template': 'Edit transaction code email template',\n  'users': 'Users',\n  'new-user': 'Create',\n  'user-import': 'Import',\n  'edit-user': ({ params }) => params.userId,\n  'organizations': 'Organizations',\n  'new-organization': 'Create',\n  'edit-organization': ({ params }) => params.organizationId,\n  'clients': 'Clients',\n  'key-pair-list': 'Key pairs',\n  'new-client': 'Create',\n  'client': ({ params }) => params.clientId,\n  'upstreams': 'Upstreams',\n  'new-upstream': 'Create',\n  'upstream': ({ params }) => params.upstreamId,\n  'scopes': 'Scopes',\n  'scope': ({ params }) => params.scopeId,\n  'roles': 'Roles',\n  'new-role': 'Create',\n  'role': ({ params }) => params.roleId,\n  'configuration': 'Configuration',\n  'configuration-file-upload': 'Configuration file upload',\n  'error-template-list': 'Error templates',\n  'edit-bad-request-template': 'Edit bad request template',\n  'edit-forbidden-template': 'Edit forbidden template',\n  'edit-not-found-template': 'Edit not found template',\n  'edit-internal-server-error-template': 'Edit internal server error template'\n}\n\nexport default {\n  name: 'breadcrumb',\n  computed: {\n    items () {\n      const items = this.$route.matched\n        .filter(({ name }) => name)\n        .filter(({ name }) => labels[name])\n        .map((route) => {\n          const { name } = route\n          const label = labels[name]\n\n          return {\n            label: (label instanceof Function) ? label(this.$route) : label,\n            path: { name, params: this.$route.params }\n          }\n        })\n\n      return items\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.ui.breadcrumb {\n  font-size: 1em;\n  line-height: 1em;\n  padding: 1rem 1rem 0 1rem;\n  .section {\n    display: inline;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Feedback.vue",
    "content": "<template>\n  <div class=\"feedback\">\n    <div @click=\"toggle()\" class=\"ui yellow feedback button\">Feedback</div>\n    <div class=\"feedback overlay\" v-if=\"show\">\n      <div class=\"ui feedback wrapper massive center aligned segment\">\n        <i class=\"ui white close icon\" @click=\"toggle()\"></i>\n        <h2>Your feedback has value</h2>\n        <div class=\"rating\">\n          <a v-for=\"rating in ratings\" @click=\"setRating(rating)\">\n            <span class=\"star\" v-if=\"isRatingActive(rating)\">&#9733;</span>\n            <span class=\"star\" v-if=\"!isRatingActive(rating)\">&#9734;</span>\n          </a>\n        </div>\n        <form target=\"_blank\" class=\"ui large form\" action=\"https://gateway.boruta.patatoid.fr/store\" method=\"POST\">\n          <input type=\"hidden\" name=\"rating\" :value=\"rating\" />\n          <div class=\"field\">\n            <textarea name=\"feedback\" v-model=\"feedback\" placeholder=\"Say something (optional)\" />\n          </div>\n          <div class=\"field\">\n            <div class=\"ui checkbox\">\n              <input name=\"_privacy_policy\" type=\"checkbox\" v-model=\"privacy\">\n              <label>I have read the <a href=\"https://io.malach.it/privacy-policy.html\" target=\"_blank\"> Privacy policy</a></label>\n            </div>\n          </div>\n          <button :disabled=\"!privacy\" class=\"ui fluid violet button\" type=\"submit\">Submit</button>\n        </form>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport oauth from '../services/oauth.service'\n\nexport default {\n  data () {\n    const ratings = ['horrible', 'poor', 'neutral', 'good', 'excellent']\n    return {\n      show: false,\n      ratings,\n      rating: null,\n      feedback: null,\n      privacy: false\n    }\n  },\n  computed: {\n    isRatingActive () {\n      return (rating) => {\n        return this.ratings.indexOf(rating) <= this.ratings.indexOf(this.rating)\n      }\n    }\n  },\n  methods: {\n    toggle () {\n      this.show = !this.show\n    },\n    setRating (rating) {\n      this.rating = rating\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.feedback.button {\n  position: absolute;\n  right: 9em;\n  top: .5em;\n}\n.feedback.overlay {\n  position: fixed;\n  right: 1rem;\n  bottom: calc(40px + 1rem);\n  z-index: 1000;\n}\n.feedback.wrapper {\n  position: relative;\n  .close {\n    position: absolute;\n    top: -1.2em;\n    right: 0;\n    cursor: pointer;\n  }\n  .rating {\n    margin-bottom: 1em;\n    font-size: 2rem;\n    height: 2.4rem;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n\n    .star {\n      display: block;\n      width: 2.4rem;\n      cursor: pointer;\n      font-weight: bold;\n      font-size: 1em;\n      color: #fbbd08!important;\n      &:hover {\n        font-size: 1.4em;\n        transition: all .2s ease-in-out;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/BackendForm.vue",
    "content": "<template>\n  <div class=\"ui backend-form segment\">\n    <FormErrors :errors=\"backend.errors\" v-if=\"backend.errors\" />\n    <form class=\"ui form\" @submit.prevent=\"submit\">\n      <div ref=\"tabularMenu\" class=\"ui top attached stackable tabular menu\">\n        <a id=\"general-configuration\" @click=\"openTab\" class=\"active item\">General configuration</a>\n        <a id=\"type\" @click=\"openTab\" class=\"item\">Type</a>\n        <a id=\"email-configuration\" @click=\"openTab\" class=\"item\">Email configuration</a>\n        <a id=\"identity-federation\" @click=\"openTab\" class=\"item\">Identity federation</a>\n        <a id=\"verifiable-credentials\" @click=\"openTab\" class=\"item\">Verifiable credentials</a>\n        <a id=\"user-metadata\" @click=\"openTab\" class=\"item\">User metadata</a>\n      </div>\n      <div ref=\"form\">\n        <div ref=\"general-configuration\" data-tab=\"general-configuration\" class=\"ui bottom attached active tab segment\">\n          <h2>General configuration</h2>\n          <div class=\"field\" :class=\"{ 'error': backend.errors?.name }\">\n            <label>Name</label>\n            <input type=\"text\" v-model=\"backend.name\" placeholder=\"Shiny new backend\">\n          </div>\n          <div class=\"ui info message\">\n            Default backend will be used in case of resource owner password credentials requests.\n          </div>\n          <div class=\"field\" :class=\"{ 'error': backend.errors?.is_default }\">\n            <div class=\"ui toggle checkbox\">\n              <input type=\"checkbox\" v-model=\"backend.is_default\">\n              <label>Default</label>\n            </div>\n          </div>\n          <div class=\"ui info message\">\n            Newly created users will have a default organization along with them.\n          </div>\n          <div class=\"field\" :class=\"{ 'error': backend.errors?.create_default_organization }\">\n            <div class=\"ui toggle checkbox\">\n              <input type=\"checkbox\" v-model=\"backend.create_default_organization\">\n              <label>Create default organization</label>\n            </div>\n          </div>\n          <h2>Roles</h2>\n          <RolesField :currentRoles=\"backend.roles\" @delete-role=\"deleteRole\" @add-role=\"addRole\" />\n        </div>\n        <div ref=\"type\" data-tab=\"type\" class=\"ui bottom attached tab segment\">\n          <h2>Backend type</h2>\n          <div class=\"field\" :class=\"{ 'error': backend.errors?.type }\">\n            <label>Type</label>\n            <select v-model=\"backend.type\">\n              <option value=\"Elixir.BorutaIdentity.Accounts.Internal\">Internal</option>\n              <option value=\"Elixir.BorutaIdentity.Accounts.Ldap\">LDAP</option>\n            </select>\n          </div>\n          <section v-if=\"backend.type == 'Elixir.BorutaIdentity.Accounts.Internal'\">\n            <h3>Internal configuration</h3>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.password_hashing_alg }\">\n              <label>Password hashing algorithm</label>\n              <select v-model=\"backend.password_hashing_alg\" @change=\"onAlgorithmChange\">\n                <option :value=\"alg.name\" v-for=\"alg in passwordHashingAlgorithms\" :key=\"alg\">{{ alg.label }}</option>\n              </select>\n            </div>\n            <h4>Password hashing algorithm options</h4>\n            <div class=\"field\" v-for=\"opt in passwordHashingOpts\" :key=\"opt.name\" :class=\"{ 'error': backend.errors?.password_hashing_opts }\">\n              <label>{{ opt.label }}</label>\n              <input :type=\"opt.type\" v-model=\"backend.password_hashing_opts[opt.name]\" :placeholder=\"opt.default\">\n            </div>\n          </section>\n          <section v-if=\"backend.type == 'Elixir.BorutaIdentity.Accounts.Ldap'\">\n            <h3>LDAP configuration</h3>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.ldap_host }\">\n              <label>Host</label>\n              <input type=\"text\" v-model=\"backend.ldap_host\" placeholder=\"example.com\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.ldap_user_rdn_attribute }\">\n              <label>User RDN attribute</label>\n              <input :type=\"text\" v-model=\"backend.ldap_user_rdn_attribute\" placeholder=\"sn\" />\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.ldap_base_dn }\">\n              <label>Base distinguished name (dn)</label>\n              <input type=\"text\" v-model=\"backend.ldap_base_dn\" placeholder=\"dc=example,dc=com\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.ldap_ou }\">\n              <label>Users organization unit (ou)</label>\n              <input type=\"text\" v-model=\"backend.ldap_ou\" placeholder=\"ou=People\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.ldap_master_dn }\">\n              <label>Master distinguished name <i>(needed only for user edition)</i></label>\n              <input type=\"text\" v-model=\"backend.ldap_master_dn\" placeholder=\"cn=admin,dc=ldap,dc=test\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.ldap_master_password }\">\n              <label>Master password <i>(needed only for user edition)</i></label>\n              <div class=\"ui left icon input\">\n                <input :type=\"ldapMasterPasswordVisible ? 'text' : 'password'\" autocomplete=\"new-password\" v-model=\"backend.ldap_master_password\" />\n                <i class=\"eye icon\" :class=\"{ 'slash': ldapMasterPasswordVisible }\" @click=\"ldapMasterPasswordVisibilityToggle()\"></i>\n              </div>\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.ldap_pool_size }\">\n              <label>Pool size</label>\n              <input type=\"number\" v-model=\"backend.ldap_pool_size\" placeholder=\"5\">\n            </div>\n          </section>\n        </div>\n        <div ref=\"email-configuration\" data-tab=\"email-configuration\" class=\"ui bottom attached tab segment\">\n          <h2>Email configuration</h2>\n          <h3>SMTP configuration</h3>\n          <div class=\"field\" :class=\"{ 'error': backend.errors?.smtp_from }\">\n            <label>From</label>\n            <input type=\"text\" v-model=\"backend.smtp_from\" placeholder=\"from@mail.example\">\n          </div>\n          <div class=\"field\" :class=\"{ 'error': backend.errors?.smtp_relay }\">\n            <label>Relay</label>\n            <input type=\"text\" v-model=\"backend.smtp_relay\" placeholder=\"smtp.example.com\">\n          </div>\n          <div class=\"field\" :class=\"{ 'error': backend.errors?.smtp_username }\">\n            <label>Username</label>\n            <input type=\"text\" v-model=\"backend.smtp_username\" placeholder=\"username\">\n          </div>\n          <div class=\"field\" :class=\"{ 'error': backend.errors?.smtp_password }\">\n            <label>Password</label>\n            <div class=\"ui left icon input\">\n              <input :type=\"smtpPasswordVisible ? 'text' : 'password'\" autocomplete=\"new-password\" v-model=\"backend.smtp_password\" />\n              <i class=\"eye icon\" :class=\"{ 'slash': smtpPasswordVisible }\" @click=\"smtpPasswordVisibilityToggle()\"></i>\n            </div>\n          </div>\n          <div class=\"field\" :class=\"{ 'error': backend.errors?.smtp_ssl }\">\n            <div class=\"ui toggle checkbox\">\n              <input type=\"checkbox\" v-model=\"backend.smtp_ssl\">\n              <label>SSL</label>\n            </div>\n          </div>\n          <div class=\"field\" :class=\"{ 'error': backend.errors?.smtp_tls }\">\n            <label>TLS</label>\n            <select v-model=\"backend.smtp_tls\">\n              <option value=\"always\">Always</option>\n              <option value=\"never\">Never</option>\n              <option value=\"if_available\">If available</option>\n            </select>\n          </div>\n          <div class=\"field\" :class=\"{ 'error': backend.errors?.smtp_port }\">\n            <label>Port</label>\n            <input type=\"number\" v-model=\"backend.smtp_port\" placeholder=\"25\">\n          </div>\n          <h3>Email templates</h3>\n          <div v-if=\"backend.isPersisted\" class=\"ui segment\">\n            <router-link\n              :to=\"{ name: 'edit-confirmation-instructions-email-template', params: { backendId: backend.id } }\"\n              class=\"ui fluid blue button\">Edit confirmation template</router-link>\n          </div>\n          <div v-if=\"backend.isPersisted\" class=\"ui segment\">\n            <router-link\n              :to=\"{ name: 'edit-reset-password-instructions-email-template', params: { backendId: backend.id } }\"\n              class=\"ui fluid blue button\">Edit reset password template</router-link>\n          </div>\n          <div v-if=\"backend.isPersisted\" class=\"ui segment\">\n            <router-link\n              :to=\"{ name: 'edit-tx-code-email-template', params: { backendId: backend.id } }\"\n              class=\"ui fluid blue button\">Edit transaction code template</router-link>\n          </div>\n        </div>\n        <div ref=\"identity-federation\" data-tab=\"identity-federation\" class=\"ui bottom attached tab segment\">\n          <h2>Identity federation (login with)</h2>\n          <div v-for=\"federatedServer in backend.federated_servers\" class=\"ui federated-server-field segment\">\n            <i class=\"ui large close icon\" @click=\"deleteFederatedServer(federatedServer)\"></i>\n            <h3>Federated server</h3>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.federated_servers }\">\n              <label>Server name</label>\n              <input type=\"text\" v-model=\"federatedServer.name\" placeholder=\"boruta\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.federated_servers }\">\n              <label>Client ID</label>\n              <input type=\"text\" v-model=\"federatedServer.client_id\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.federated_servers }\">\n              <label>Client secret</label>\n              <div class=\"ui left icon input\">\n                <input :type=\"federatedServer.clientSecretVisible ? 'text' : 'password'\" autocomplete=\"new-password\" v-model=\"federatedServer.client_secret\" />\n                <i class=\"eye icon\" :class=\"{ 'slash': federatedServer.clientSecretVisible }\" @click=\"federatedServerVisibilityToggle(federatedServer)\"></i>\n              </div>\n            </div>\n            <div class=\"ui info message\">\n              You'll need to fill the redirect uri on the remote server `${BORUTA_OAUTH_HOST}/accounts/backends/:backend_id/:federated_server_name/callback`\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.federated_servers }\">\n              <label>Base URL</label>\n              <input type=\"text\" v-model=\"federatedServer.base_url\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.federated_servers }\">\n              <label>scope <i>(separated with a whitespace)</i></label>\n              <input type=\"text\" v-model=\"federatedServer.scope\">\n            </div>\n            <h4>Federated metadata</h4>\n            <div class=\"ui federated-metadata-fields segment\" v-for=\"metadataEndpoint in federatedServer.metadata_endpoints || []\">\n              <h5>Metadata endpoint configuration</h5>\n              <i class=\"ui large close icon\" @click=\"deleteMetadataEndpoint(federatedServer, metadataEndpoint)\"></i>\n              <div class=\"field\" :class=\"{ 'error': backend.errors?.federated_servers }\">\n                <label>Metadata endpoint URL</label>\n                <input type=\"text\" v-model=\"metadataEndpoint.endpoint\">\n              </div>\n              <div class=\"field\" :class=\"{ 'error': backend.errors?.federated_servers }\">\n                <label>Metadata endpoint claims <i>(separated with a whitespace)</i></label>\n                <input type=\"text\" v-model=\"metadataEndpoint.claims\">\n              </div>\n            </div>\n            <div class=\"field\">\n              <a class=\"ui blue fluid button\" @click=\"addMetadataEndpoint(federatedServer)\">Add a federated metadata endpoint</a>\n            </div>\n            <h4>Federated server endpoints</h4>\n            <div class=\"field\">\n              <div class=\"ui toggle checkbox\">\n                <input type=\"checkbox\" v-model=\"federatedServer.isDiscovery\">\n                <label>Use OpenID discovery</label>\n              </div>\n            </div>\n            <div v-if=\"federatedServer.isDiscovery\">\n              <div class=\"field\" :class=\"{ 'error': backend.errors?.federated_servers }\">\n                <label>Discovery path</label>\n                <input type=\"text\" v-model=\"federatedServer.discovery_path\">\n              </div>\n            </div>\n            <div v-else>\n              <div class=\"field\" :class=\"{ 'error': backend.errors?.federated_servers }\">\n                <label>Userinfo path</label>\n                <input type=\"text\" v-model=\"federatedServer.userinfo_path\">\n              </div>\n              <div class=\"field\" :class=\"{ 'error': backend.errors?.federated_servers }\">\n                <label>Authorize path</label>\n                <input type=\"text\" v-model=\"federatedServer.authorize_path\">\n              </div>\n              <div class=\"field\" :class=\"{ 'error': backend.errors?.federated_servers }\">\n                <label>Token path</label>\n                <input type=\"text\" v-model=\"federatedServer.token_path\">\n              </div>\n            </div>\n          </div>\n          <div class=\"field\">\n            <a class=\"ui blue fluid button\" @click=\"addFederatedServer()\">Add a federated server</a>\n          </div>\n        </div>\n        <div ref=\"verifiable-credentials\" data-tab=\"verifiable-credentials\" class=\"ui bottom attached tab segment\">\n          <h2>Verifiable credentials</h2>\n          <div v-for=\"credential in backend.verifiable_credentials\" class=\"ui credential-field segment\">\n            <i class=\"ui large close icon\" @click=\"deleteVerifiableCredential(credential)\"></i>\n            <h3>Verifiable credential</h3>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Credential identifier</label>\n              <input type=\"text\" v-model=\"credential.credential_identifier\" placeholder=\"BorutaCredential\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Verifiable credential type</label>\n              <input type=\"text\" v-model=\"credential.vct\" placeholder=\"urn:boruta:verifiable-credential-type\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Version</label>\n              <select v-model=\"credential.version\">\n                <option value=\"11\">11</option>\n                <option value=\"13\">13</option>\n              </select>\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Format</label>\n              <select v-model=\"credential.format\">\n                <option value=\"jwt_vc_json\">jwt_vc_json</option>\n                <option value=\"jwt_vc\">jwt_vc</option>\n                <option value=\"vc+sd-jwt\">vc+sd-jwt</option>\n              </select>\n            </div>\n            <div class=\"field\">\n              <div class=\"ui toggle checkbox\">\n                <input type=\"checkbox\" v-model=\"credential.defered\">\n                <label>Defered</label>\n              </div>\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Types <i>(separated with a whitespace)</i></label>\n              <input type=\"text\" v-model=\"credential.types\" placeholder=\"VerifiableCredential BorutaCredential\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Time to live <i>(in seconds)</i></label>\n              <input type=\"number\" v-model=\"credential.time_to_live\" placeholder=\"31536000\">\n            </div>\n            <h4>Claims</h4>\n            <div class=\"claim\"  v-for=\"claim in credential.claims\">\n              <VerifiableCredentialClaim :format=\"credential.format\" :credential=\"credential\" :claim=\"claim\" :errors=\"backend.errors\"></VerifiableCredentialClaim>\n            </div>\n            <div class=\"field\">\n              <a class=\"ui blue fluid button\" @click=\"addVerifiableCredentialClaim(credential)\">Add a claim</a>\n            </div>\n            <h4>Display</h4>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Name</label>\n              <input type=\"text\" v-model=\"credential.display.name\" placeholder=\"Boruta Credential\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Background color</label>\n              <input type=\"text\" v-model=\"credential.display.background_color\" placeholder=\"#53b29f\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Text color</label>\n              <input type=\"text\" v-model=\"credential.display.text_color\" placeholder=\"#ffffff\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Logo URL</label>\n              <input type=\"text\" v-model=\"credential.display.logo.url\" placeholder=\"https://io.malach.it/assets/images/logo.png\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Logo alt text</label>\n              <input type=\"text\" v-model=\"credential.display.logo.alt_text\" placeholder=\"Boruta credential logo\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_credentials }\">\n              <label>Scope restriction <i>(leave blank for no restriction)</i></label>\n              <ScopesFieldByName :currentScopes=\"credential.scopes\" :scopes=\"scopes\" @add-scope=\"addCredentialScope(credential)\" @delete-scope=\"scope => deleteCredentialScope(credential, scope)\" />\n            </div>\n          </div>\n          <div class=\"field\">\n            <a class=\"ui blue fluid button\" @click=\"addVerifiableCredential()\">Add a verifiable credential</a>\n          </div>\n          <h2>Verifiable presentations</h2>\n          <div v-for=\"presentation in backend.verifiable_presentations\" class=\"ui presentation-field segment\">\n            <i class=\"ui large close icon\" @click=\"deleteVerifiablePresentation(presentation)\"></i>\n            <h3>Verifiable presentation</h3>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_presentations }\">\n              <label>Presentation identifier</label>\n              <input type=\"text\" v-model=\"presentation.presentation_identifier\" placeholder=\"BorutaPresentation\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.verifiable_presentations }\">\n              <label>Presentation definition</label>\n\n              <TextEditor :content=\"presentation.presentation_definition\" @codeUpdate=\"setPresentationDefitiion($event, presentation)\" />\n            </div>\n          </div>\n          <div class=\"field\">\n            <a class=\"ui blue fluid button\" @click=\"addVerifiablePresentation()\">Add a verifiable presentation</a>\n          </div>\n        </div>\n        <div ref=\"user-metadata\" data-tab=\"user-metadata\" class=\"ui bottom attached tab segment\">\n          <h2>User metadata configuration</h2>\n          <div v-for=\"field in backend.metadata_fields\" class=\"ui metadata-field segment\">\n            <i class=\"ui large close icon\" @click=\"deleteMetadataField(field)\"></i>\n            <h3>Metadata field</h3>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.metadata_fields }\">\n              <div class=\"ui toggle checkbox\">\n                <input type=\"checkbox\" v-model=\"field.user_editable\">\n                <label>User editable</label>\n              </div>\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.metadata_fields }\">\n              <label>Scope restriction <i>(leave blank for no restriction)</i></label>\n              <ScopesFieldByName :currentScopes=\"field.scopes\" :scopes=\"scopes\" @add-scope=\"addMetadataFieldScope(field)\" @delete-scope=\"scope => deleteMetadataFieldScope(field, scope)\" />\n            </div>\n            <div class=\"field\" :class=\"{ 'error': backend.errors?.metadata_fields }\">\n              <label>Attribute name</label>\n              <input type=\"text\" v-model=\"field.attribute_name\" placeholder=\"family_name\">\n            </div>\n          </div>\n          <div class=\"field\">\n            <a class=\"ui blue fluid button\" @click=\"addMetadataField()\">Add a metadata field</a>\n          </div>\n        </div>\n      </div>\n      <div class=\"actions\">\n        <button class=\"ui right floated violet button\" type=\"submit\">{{ action }}</button>\n      </div>\n    </form>\n  </div>\n</template>\n\n<script>\nimport Backend, { Claim } from '../../models/backend.model'\nimport RolesField from './RolesField.vue'\nimport Role from '../../models/role.model'\nimport Scope from '../../models/scope.model'\nimport FormErrors from './FormErrors.vue'\nimport ScopesFieldByName from './ScopesFieldByName.vue'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport VerifiableCredentialClaim from '../../components/VerifiableCredentialClaim.vue'\n\nexport default {\n  name: 'backend-form',\n  props: ['backend', 'action'],\n  components: {\n    FormErrors,\n    RolesField,\n    ScopesFieldByName,\n    TextEditor,\n    VerifiableCredentialClaim\n  },\n  data () {\n    return {\n      passwordHashingAlgorithms: Backend.passwordHashingAlgorithms,\n      ldapMasterPasswordVisible: false,\n      smtpPasswordVisible: false,\n      scopes: []\n    }\n  },\n  computed: {\n    passwordHashingOpts () {\n      return Backend.passwordHashingOpts[this.backend.password_hashing_alg]\n    }\n  },\n  mounted () {\n    Scope.all().then(scopes => {\n      this.scopes = scopes\n    })\n  },\n  methods: {\n    onAlgorithmChange () {\n      this.backend.resetPasswordAlgorithmOpts()\n      if (this.backend.isPersisted) {\n        alert('Changing the password hashing algorithm may invalidate already saved passwords. Use this feature with care.')\n      }\n    },\n    ldapMasterPasswordVisibilityToggle () {\n      this.ldapMasterPasswordVisible = !this.ldapMasterPasswordVisible\n    },\n    smtpPasswordVisibilityToggle () {\n      this.smtpPasswordVisible = !this.smtpPasswordVisible\n    },\n    federatedServerVisibilityToggle (federatedServer) {\n      federatedServer.clientSecretVisible = !federatedServer.clientSecretVisible\n    },\n    addFederatedServer () {\n      this.backend.federated_servers.push({})\n    },\n    addMetadataEndpoint (federatedServer) {\n      federatedServer.metadata_endpoints ||= []\n      federatedServer.metadata_endpoints.push({})\n    },\n    deleteMetadataEndpoint (federatedServer, endpoint) {\n      federatedServer.metadata_endpoints.splice(\n        federatedServer.metadata_endpoints.indexOf(endpoint),\n        1\n      )\n    },\n    addVerifiableCredentialClaim (credential) {\n      credential.claims.push(Claim.build('attribute'))\n    },\n    addVerifiableCredential () {\n      this.backend.verifiable_credentials.push({defered: false, display: {logo: {}}, claims: [], scopes: []})\n    },\n    addVerifiablePresentation () {\n      this.backend.verifiable_presentations.push({})\n    },\n    addMetadataField () {\n      this.backend.metadata_fields.push({ scopes: [] })\n    },\n    deleteMetadataField (field) {\n      this.backend.metadata_fields.splice(\n        this.backend.metadata_fields.indexOf(field),\n        1\n      )\n    },\n    deleteFederatedServer (federatedServer) {\n      this.backend.federated_servers.splice(\n        this.backend.federated_servers.indexOf(federatedServer),\n        1\n      )\n    },\n    deleteVerifiableCredential (credential) {\n      this.backend.verifiable_credentials.splice(\n        this.backend.verifiable_credentials.indexOf(credential),\n        1\n      )\n    },\n    deleteVerifiablePresentation (presentation) {\n      this.backend.verifiable_presentations.splice(\n        this.backend.verifiable_presentations.indexOf(presentation),\n        1\n      )\n    },\n    setPresentationDefitiion (content, presentation) {\n      presentation.presentation_definition = content\n    },\n    addMetadataFieldScope (field) {\n      field.scopes ||= []\n      field.scopes.push({})\n    },\n    deleteMetadataFieldScope (field, scope) {\n      field.scopes.splice(field.scopes.indexOf(scope), 1)\n    },\n    addCredentialScope (credential) {\n      credential.scopes ||= []\n      credential.scopes.push({})\n    },\n    deleteCredentialScope (credential, scope) {\n      credential.scopes.splice(credential.scopes.indexOf(scope), 1)\n    },\n    back () {\n      this.$emit('back')\n    },\n    addRole () {\n      this.backend.roles.push({ model: new Role() })\n    },\n    deleteRole (role) {\n      this.backend.roles.splice(\n        this.backend.roles.indexOf(role),\n        1\n      )\n    },\n    openTab (e) {\n      const tab = e.target.id\n      Array.from(this.$refs.tabularMenu.getElementsByClassName('item')).forEach(e => {\n        if (e.id == tab) {\n          e.classList.add('active')\n          this.$refs[e.id].classList.add('active')\n        } else {\n          e.classList.remove('active')\n          this.$refs[e.id].classList.remove('active')\n        }\n      })\n    }\n  },\n  watch: {\n    'backend.errors': {\n      deep: true,\n      handler (errors) {\n        setTimeout(() => {\n          Array.from(this.$refs.tabularMenu.getElementsByClassName('error')).forEach(e => {\n            e.classList.remove('error')\n          })\n          Array.from(this.$refs.form.getElementsByClassName('error')).forEach(elt => {\n            const tab = elt.closest('.tab').getAttribute('data-tab')\n            this.$refs.tabularMenu.querySelector('#' + tab).classList.add('error')\n          })\n        }, 100)\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.metadata-field.segment, .federated-server-field.segment, .credential-field.segment, .presentation-field.segment {\n  .field {\n    margin-bottom: 1em!important;\n  }\n  .close.icon {\n    position: absolute;\n    cursor: pointer;\n    top: 1.17em;\n    right: .5em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/ClientForm.vue",
    "content": "<template>\n  <div class=\"ui client-form segment\">\n    <FormErrors v-if=\"client.errors\" :errors=\"client.errors\" />\n    <form ref=\"form\" class=\"ui form\" @submit.prevent=\"submit\">\n      <div ref=\"tabularMenu\" class=\"ui top attached stackable tabular menu\">\n        <a id=\"general-configuration\" @click=\"openTab\" class=\"active item\">General configuration</a>\n        <a id=\"authentication\" @click=\"openTab\" class=\"item\">Authentication</a>\n        <a id=\"security\" @click=\"openTab\" class=\"item\">Security</a>\n        <a id=\"grant-types\" @click=\"openTab\" class=\"item\">Grant types</a>\n      </div>\n      <div ref=\"general-configuration\" data-tab=\"general-configuration\" class=\"ui bottom attached active tab segment\">\n        <div class=\"field\" :class=\"{ 'error': client.errors?.name }\">\n          <label>Name</label>\n          <input v-model=\"client.name\" placeholder=\"Super client\" autocomplete=\"new-password\" />\n        </div>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.id }\" v-if=\"!client.isPersisted\">\n          <label>Client ID</label>\n          <input v-model=\"client.id\" autocomplete=\"new-password\" placeholder=\"Must be an UUIDv4 - Leave blank to autogenerate\" />\n        </div>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.id }\">\n          <label>Public client ID</label>\n          <input v-model=\"client.public_client_id\" placeholder=\"https://boruta.host\" />\n        </div>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.secret }\">\n          <label>Client secret</label>\n          <div class=\"ui left icon input\">\n            <input :type=\"passwordVisible ? 'text' : 'password'\" autocomplete=\"new-password\" v-model=\"client.secret\" placeholder=\"Leave blank to autogenerate\" />\n            <i class=\"eye icon\" :class=\"{ 'slash': passwordVisible }\" @click=\"passwordVisibilityToggle()\"></i>\n          </div>\n        </div>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.access_token_ttl }\">\n          <label>Access token TTL (seconds)</label>\n          <input type=\"number\" v-model=\"client.access_token_ttl\" placeholder=\"3600\" />\n        </div>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.authorization_code_ttl }\">\n          <label>Authorization code TTL (seconds)</label>\n          <input type=\"number\" v-model=\"client.authorization_code_ttl\" placeholder=\"60\" />\n        </div>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.refresh_token_ttl }\">\n          <label>Refresh token TTL (seconds)</label>\n          <input type=\"number\" v-model=\"client.refresh_token_ttl\" placeholder=\"2592000\" />\n        </div>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.id_token_ttl }\">\n          <label>ID token TTL (seconds)</label>\n          <input type=\"number\" v-model=\"client.id_token_ttl\" placeholder=\"3600\" />\n        </div>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.authorization_request_ttl }\">\n          <label>Authorization request TTL (seconds)</label>\n          <input type=\"number\" v-model=\"client.authorization_request_ttl\" placeholder=\"60\" />\n        </div>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.redirect_uris }\">\n          <label>Redirect URIs</label>\n          <div v-for=\"(redirectUri, index) in client.redirect_uris\" class=\"field\" :key=\"index\">\n            <div class=\"ui right icon input\">\n              <input type=\"text\" v-model=\"redirectUri.uri\" placeholder=\"http://redirect.uri\" />\n              <i v-on:click=\"deleteRedirectUri(redirectUri)\" class=\"close icon\"></i>\n            </div>\n          </div>\n          <a v-on:click.prevent=\"addRedirectUri()\" class=\"ui blue fluid button\">Add a redirect uri</a>\n        </div>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.response_mode }\">\n          <label>Response mode</label>\n          <select v-model=\"client.response_mode\">\n            <option value=\"post\">post</option>\n            <option value=\"direct_post\">direct_post</option>\n          </select>\n        </div>\n      </div>\n      <div ref=\"authentication\" data-tab=\"authentication\" class=\"ui bottom attached tab segment\">\n        <h3>Client authentication</h3>\n        <div class=\"ui segment\">\n          <div class=\"inline fields\" :class=\"{ 'error': client.errors?.token_endpoint_auth_methods }\">\n            <label>Client authentication methods</label>\n            <div class=\"field\" v-for=\"method in tokenEndpointAuthMethods\" :key=\"method\">\n              <div class=\"ui checkbox\">\n                <input type=\"checkbox\" v-model=\"client.token_endpoint_auth_methods\" :value=\"method\" />\n                <label>{{ method }}</label>\n              </div>\n            </div>\n          </div>\n        </div>\n        <div class=\"ui segment\">\n          <div class=\"inline fields\" :class=\"{ 'error': client.errors?.token_endpoint_jwt_auth_alg }\">\n            <label>Client JWT authentication signature algorithm</label>\n            <div class=\"field\" v-for=\"alg in clientJwtAuthenticationSignatureAlgorithms\" :key=\"alg\">\n              <div class=\"ui radio checkbox\">\n                <label>{{ alg }}</label>\n                <input type=\"radio\" v-model=\"client.token_endpoint_jwt_auth_alg\" :value=\"alg\" />\n              </div>\n            </div>\n          </div>\n        </div>\n        <div class=\"field\" v-if=\"client.token_endpoint_jwt_auth_alg.match(/RS/)\">\n          <label>Client JWT authentication public key (pem)</label>\n          <textarea v-model=\"client.jwt_public_key\" placeholder=\"Your public key here\"></textarea>\n        </div>\n        <div class=\"ui segment\">\n          <div class=\"ui toggle checkbox\">\n            <input type=\"checkbox\" v-model=\"client.confidential\">\n            <label>Confidential</label>\n          </div>\n        </div>\n        <h3>User authentication</h3>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.identity_provider_id }\">\n          <IdentityProviderField :identityProvider=\"client.identity_provider.model\" @identityProviderChange=\"setIdentityProvider\"/>\n        </div>\n      </div>\n      <div ref=\"security\" data-tab=\"security\" class=\"ui bottom attached tab segment\">\n        <h3>Signatures adapter</h3>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.signatures_adapter }\">\n          <select v-model=\"client.signatures_adapter\">\n            <option v-for=\"adapter in signaturesAdapters\" :value=\"adapter\">{{ adapter }}</option>\n          </select>\n        </div>\n\n        <div class=\"ui segment\" v-if=\"client.signatures_adapter == 'Elixir.Boruta.Universal.Signatures'\">\n          <div class=\"ui info message\">\n            The usage of the Universal adapter requires an account, please contact Godiddy services <a href=\"https://godiddy.com/contact\" target=\"_blank\">https://godiddy.com/contact</a> and set the API key as an environment variable.\n          </div>\n          <h4>Key management</h4>\n          <h4>Key type</h4>\n          <div class=\"field\" :class=\"{ 'error': client.errors?.key_pair_type }\">\n            <select v-model=\"client.key_pair_type.type\" @change=\"keyPairTypeChanged = true\">\n              <option value=\"universal\">\n                universal\n              </option>\n            </select>\n          </div>\n          <div class=\"field\">\n            <label>method</label>\n            <select>\n              <option value=\"key\">key</option>\n            </select>\n          </div>\n          <hr />\n          <div class=\"field\" v-if=\"client.did\">\n            <label>Client did</label>\n            <pre>{{ client.did }}</pre>\n          </div>\n          <div class=\"field\" v-if=\"clientPublicKey\">\n            <label>Client public key</label>\n            <pre>{{ clientPublicKey }}</pre>\n          </div>\n          <button type=\"button\" class=\"ui fluid orange button\" :disabled=\"keyPairTypeChanged\" @click=\"regenerateKeyPair()\" v-if=\"client.isPersisted\">Regenerate client key pair</button>\n        </div>\n        <div class=\"ui segment\" v-if=\"client.signatures_adapter == 'Elixir.Boruta.Internal.Signatures'\">\n          <h4>Key type</h4>\n          <div class=\"field\" :class=\"{ 'error': client.errors?.key_pair_type }\">\n            <select v-model=\"client.key_pair_type.type\" @change=\"keyPairTypeChanged = true\">\n              <option v-for=\"keyPairType in Object.keys(keyPairTypes)\" :value=\"keyPairType\" :key=\"keyPairType\">\n                {{ keyPairType }}\n              </option>\n            </select>\n          </div>\n          <div v-for=\"(value, param) in keyPairTypes[client.key_pair_type.type]\" class=\"field\" :class=\"{ 'error': client.errors?.key_pair_type }\">\n            <label>{{ param }}</label>\n            <select v-if=\"value instanceof Array\" v-model=\"client.key_pair_type[param]\">\n              <option v-for=\"option in value\" :value=\"option\" :key=\"option\">\n                {{ option }}\n              </option>\n            </select>\n            <input v-else type=\"text\" v-model=\"client.key_pair_type[param]\" />\n          </div>\n          <h4>Key management</h4>\n          <div class=\"field\">\n            <select v-model=\"client.key_pair_id\">\n              <option :value=\"null\">Custom key pair</option>\n              <option v-for=\"keyPair in keyPairs\" :value=\"keyPair.id\" :key=\"keyPair.id\">\n                {{ keyPair.id }}\n              </option>\n            </select>\n          </div>\n          <hr />\n          <div class=\"field\" v-if=\"client.did\">\n            <label>Client did</label>\n            <pre>{{ client.did }}</pre>\n          </div>\n          <div class=\"field\" v-if=\"clientPublicKey\">\n            <label>Client public key</label>\n            <pre>{{ clientPublicKey }}</pre>\n          </div>\n          <button type=\"button\" class=\"ui fluid orange button\" :disabled=\"keyPairTypeChanged\" @click=\"regenerateKeyPair()\" v-if=\"client.isPersisted\">Regenerate client key pair</button>\n          <hr />\n          <a class=\"ui fluid orange button\" @click=\"regenerateDid()\" v-if=\"client.isPersisted\">Regenerate client did</a>\n        </div>\n        <h3>Token signatures</h3>\n        <div class=\"ui segment\">\n          <div class=\"inline fields\" :class=\"{ 'error': client.errors?.id_token_signature_alg }\">\n            <label>ID token signature algorithm</label>\n            <div class=\"field\" v-for=\"alg in idTokenSignatureAlgorithms\" :key=\"alg\">\n              <div class=\"ui radio checkbox\">\n                <label>{{ alg }}</label>\n                <input type=\"radio\" v-model=\"client.id_token_signature_alg\" :value=\"alg\" />\n              </div>\n            </div>\n          </div>\n        </div>\n        <div class=\"ui segment\">\n          <div class=\"inline fields\" :class=\"{ 'error': client.errors?.userinfo_signed_response_alg }\">\n            <label>Userinfo response signature algorithm</label>\n            <div class=\"field\" v-for=\"alg in UserinfoResponseSignatureAlgorithms\" :key=\"alg\">\n              <div class=\"ui radio checkbox\">\n                <label>{{ alg || 'none' }}</label>\n                <input type=\"radio\" v-model=\"client.userinfo_signed_response_alg\" :value=\"alg\" />\n              </div>\n            </div>\n          </div>\n        </div>\n        <h3>Authorization</h3>\n        <div class=\"field\">\n          <div class=\"ui toggle checkbox\">\n            <input type=\"checkbox\" v-model=\"client.enforce_dpop\">\n            <label>Enforce Demonstration Proof-of-Possession (DPoP)</label>\n          </div>\n        </div>\n        <div class=\"field\">\n          <div class=\"ui toggle checkbox\">\n            <input type=\"checkbox\" v-model=\"client.enforce_tx_code\">\n            <label>Enforce pre-authorized code transaction code</label>\n          </div>\n        </div>\n        <div class=\"field\">\n          <div class=\"ui toggle checkbox\">\n            <input type=\"checkbox\" v-model=\"client.authorize_scope\">\n            <label>Authorize scopes</label>\n          </div>\n        </div>\n        <div class=\"field\">\n          <div class=\"ui toggle checkbox\">\n            <input type=\"checkbox\" v-model=\"client.check_public_client_id\">\n            <label>Check public client id</label>\n          </div>\n        </div>\n        <div class=\"field\" :class=\"{ 'error': client.errors?.authorized_scopes }\">\n          <ScopesField v-if=\"client.authorize_scope\" :currentScopes=\"client.authorized_scopes\" @delete-scope=\"deleteScope\" @add-scope=\"addScope\" />\n        </div>\n        <h3>PKCE configuration</h3>\n        <div class=\"ui segment\">\n          <div class=\"ui toggle checkbox\">\n            <input type=\"checkbox\" v-model=\"client.pkce\">\n            <label>PKCE enabled</label>\n          </div>\n        </div>\n        <div class=\"ui segment\">\n          <div class=\"ui toggle checkbox\">\n            <input type=\"checkbox\" v-model=\"client.public_refresh_token\">\n            <label>Public refresh token</label>\n          </div>\n        </div>\n        <div class=\"ui segment\">\n          <div class=\"ui toggle checkbox\">\n            <input type=\"checkbox\" v-model=\"client.public_revoke\">\n            <label>Public revoke</label>\n          </div>\n        </div>\n      </div>\n      <div ref=\"grant-types\" data-tab=\"grant-types\" class=\"ui bottom attached tab segment\">\n        <h3>Supported grant types</h3>\n        <div class=\"ui segment\" v-for=\"grantType in client.grantTypes\" :key=\"grantType.label\">\n          <div class=\"ui toggle checkbox\">\n            <input type=\"checkbox\" v-model=\"grantType.value\">\n            <label>{{ grantType.label }}</label>\n          </div>\n        </div>\n      </div>\n      <div class=\"actions\">\n        <button class=\"ui violet button\" type=\"submit\">{{ action }}</button>\n      </div>\n    </form>\n  </div>\n</template>\n\n<script>\nimport Scope from '../../models/scope.model'\nimport KeyPair from '../../models/key-pair.model'\nimport Client from '../../models/client.model'\nimport ScopesField from './ScopesField.vue'\nimport IdentityProviderField from './IdentityProviderField.vue'\nimport FormErrors from './FormErrors.vue'\n\nexport default {\n  name: 'client-form',\n  props: ['client', 'action'],\n  components: {\n    ScopesField,\n    IdentityProviderField,\n    FormErrors\n  },\n  data() {\n    return {\n      keyPairTypes: Client.keyPairTypes,\n      signaturesAdapters: Client.signaturesAdapters,\n      keyPairs: [],\n      keyPairTypeChanged: false,\n      idTokenSignatureAlgorithms: Client.idTokenSignatureAlgorithms,\n      UserinfoResponseSignatureAlgorithms: Client.UserinfoResponseSignatureAlgorithms,\n      clientJwtAuthenticationSignatureAlgorithms: Client.clientJwtAuthenticationSignatureAlgorithms,\n      tokenEndpointAuthMethods: Client.tokenEndpointAuthMethods,\n      passwordVisible: false\n    }\n  },\n  mounted () {\n    KeyPair.all().then(keyPairs => {\n      this.keyPairs = keyPairs\n    })\n  },\n  methods: {\n    regenerateKeyPair () {\n      if (confirm(\"Are you sure you want to regenerate this client key pair?\")) {\n        this.client.regenerateKeyPair().then(() => {\n          this.$emit('submit')\n        })\n      }\n    },\n    regenerateDid () {\n      if (confirm(\"Are you sure you want to regenerate this client did?\")) {\n        this.client.regenerateDid().then(() => {\n          this.$emit('submit')\n        })\n      }\n    },\n    submit () {\n      this.keyPairTypeChanged = false\n      this.$emit('submit')\n    },\n    addRedirectUri () {\n      this.client.redirect_uris.push({})\n    },\n    deleteRedirectUri (redirectUri) {\n      this.client.redirect_uris.splice(\n        this.client.redirect_uris.indexOf(redirectUri),\n        1\n      )\n    },\n    setIdentityProvider (identityProvider) {\n      this.client.identity_provider = { model: identityProvider }\n    },\n    addScope () {\n      this.client.authorized_scopes.push({ model: new Scope() })\n    },\n    deleteScope (scope) {\n      this.client.authorized_scopes.splice(\n        this.client.authorized_scopes.indexOf(scope),\n        1\n      )\n    },\n    passwordVisibilityToggle () {\n      this.passwordVisible = !this.passwordVisible\n    },\n    openTab (e) {\n      const tab = e.target.id\n      Array.from(this.$refs.tabularMenu.getElementsByClassName('item')).forEach(e => {\n        if (e.id == tab) {\n          e.classList.add('active')\n          this.$refs[e.id].classList.add('active')\n        } else {\n          e.classList.remove('active')\n          this.$refs[e.id].classList.remove('active')\n        }\n      })\n    }\n  },\n  watch: {\n    '$route.hash': {\n      handler (hash) {\n        console.log(this.$refs.tabularMenu)\n        Array.from(this.$refs.tabularMenu.getElementsByClassName('item')).forEach(e => {\n          console.log(e.classList)\n          if (Array.from(e.classList).includes(hash.slice(1))) {\n            e.classList.add('active')\n          } else {\n            e.classList.remove('active')\n          }\n        })\n      }\n    },\n    'client.errors': {\n      deep: true,\n      handler (errors) {\n        setTimeout(() => {\n          Array.from(this.$refs.tabularMenu.getElementsByClassName('error')).forEach(e => {\n            e.classList.remove('error')\n          })\n          Array.from(this.$refs.form.getElementsByClassName('error')).forEach(elt => {\n            const tab = elt.closest('.tab').getAttribute('data-tab')\n            this.$refs.tabularMenu.querySelector('#' + tab).classList.add('error')\n          })\n        }, 100)\n      }\n    },\n    'client.public_key': {\n      deep: true,\n      handler (newPublicKey) {\n        this.clientPublicKey = newPublicKey\n      }\n    },\n    'client.key_pair_id': {\n      deep: true,\n      handler (newKeyPairId) {\n        if (newKeyPairId) {\n          this.clientPublicKey = this.keyPairs.find(({ id }) => {\n            return id === newKeyPairId\n          }).public_key\n        } else {\n          const keyPair = this.keyPairs.find(({ public_key }) => {\n            return public_key === this.client.public_key\n          })\n          this.client.key_pair_id = keyPair ? keyPair.id : null\n          this.clientPublicKey = this.client.public_key\n        }\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.client-form {\n  .field {\n    position: relative;\n    pre {\n      overflow: hidden;\n      text-overflow: ellipsis;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/FormErrors.vue",
    "content": "<template>\n  <div @click=\"hide()\" class=\"form-errors ui error message\" v-if=\"show\" :class=\"{ 'inline': inline }\">\n    <div class=\"header\">\n      Could not save current record\n    </div>\n    <ul class=\"list\">\n      <li v-for=\"key in Object.keys(errors)\" :key=\"key\">\n        <strong>{{ label(key) }} </strong> {{ errors[key][0] }}\n      </li>\n    </ul>\n  </div>\n</template>\n\n<script>\nconst labels = {\n  'access_token_ttl': 'Access token TTL',\n  'authorization_code_ttl': 'Authorization code TTL',\n  'authorized_scopes': 'Authorized scopes',\n  'backend_id': 'Backend',\n  'client_identity_providers': 'Client',\n  'file': 'File',\n  'forwarded_token_signature_alg': 'Forwarded token signature algorithm',\n  'forwarded_token_signature_private_key': 'ID token signature private key',\n  'forwarded_token_signature_public_key': 'ID token signature public key',\n  'forwarded_token_signature_secret': 'ID token signature secret',\n  'id_token_signature_alg': 'ID token signature algorithm',\n  'id_token_ttl': 'ID token TTL',\n  'identity_provider_id': 'identity provider',\n  'is_default': 'Default',\n  'name': 'Name',\n  'password_hashing_opts': 'Password hashing options',\n  'redirect_uris': 'Redirect URIs',\n  'refresh_token_ttl': 'Refresh token TTL',\n  'resource': 'Resource'\n}\n\nexport default {\n  name: 'FormErrors',\n  props: ['errors', 'inline'],\n  data () {\n    return {\n      show: true\n    }\n  },\n  methods: {\n    label(key) {\n      return labels[key] === undefined ? key : labels[key]\n    },\n    hide () {\n      this.show = false\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.ui.error.message:not(.inline) {\n  cursor: pointer;\n  position: fixed;\n  z-index: 1000;\n  top: 4em;\n  right: 1em;\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/GatewayScopesField.vue",
    "content": "<template>\n  <div class=\"field edit-scopes\">\n    <div v-for=\"(authorizedScope, index) in currentScopes\" class=\"field\" :key=\"index\">\n      <div class=\"fields\">\n        <div class=\"four wide field\">\n          <label>Method</label>\n          <select type=\"text\" v-model=\"authorizedScope.method\">\n            <option :value=\"method\" v-for=\"method in methods\" :key=\"method\">{{ method }}</option>\n          </select>\n        </div>\n        <div class=\"twelve wide field\">\n          <label>Scope</label>\n          <div class=\"ui scope right icon input\">\n            <select type=\"text\" v-model=\"authorizedScope.model\" class=\"authorized-scopes-select\">\n              <option :value=\"scope\" v-for=\"scope in scopeOptions(authorizedScope.model)\" :key=\"scope.id\">{{ scope.name }}</option>\n            </select>\n            <i v-on:click=\"deleteScope(authorizedScope)\" class=\"close icon\"></i>\n          </div>\n        </div>\n     </div>\n    </div>\n    <div class=\"ui info message\" v-if=\"currentScopes.length\"><i>You can use \"*\" as method wildcard</i></div>\n    <button v-on:click.prevent=\"addScope()\" class=\"ui blue fluid button\">Add a scope</button>\n  </div>\n</template>\n\n<script>\nimport Scope from '../../models/scope.model'\n\nexport default {\n  name: 'ScopesField',\n  props: {\n    currentScopes: {\n      type: Array,\n      default: () => ([])\n    }\n  },\n  data () {\n    return {\n      scopes: [],\n      methods: ['*', 'GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'PATCH', 'DELETE']\n    }\n  },\n  computed: {\n    scopeOptions () {\n      const vm = this\n      return function (authorizedScope) {\n        return vm.scopes.map((scope) => {\n          if (authorizedScope.name === scope.name) {\n            return authorizedScope\n          }\n          return scope\n        })\n      }\n    }\n  },\n  mounted () {\n    Scope.all().then((scopes) => {\n      this.scopes = scopes\n    })\n  },\n  methods: {\n    deleteScope (scope) {\n      this.$emit('delete-scope', scope)\n    },\n    addScope () {\n      this.$emit('add-scope')\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-scopes {\n  .scope.input {\n    select {\n      margin-right: 3em;\n    }\n  }\n  .ui.icon.input>i.icon.close {\n    cursor: pointer;\n    pointer-events: all;\n    position: absolute;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/IdentityProviderField.vue",
    "content": "<template>\n  <div class=\"field\">\n    <label>identity provider</label>\n    <select v-model=\"currentIdentityProviderId\">\n      <option v-for=\"identityProviderOption in identityProviders\" :value=\"identityProviderOption.id\" :key=\"identityProviderOption.id\" :selected=\"identityProviderOption.id == currentIdentityProviderId\">\n        {{ identityProviderOption.name }}\n      </option>\n    </select>\n  </div>\n</template>\n\n<script>\nimport IdentityProvider from '../../models/identity-provider.model'\n\nexport default {\n  name: 'IdentityProviderField',\n  props: ['identityProvider'],\n  mounted () {\n    IdentityProvider.all().then((identityProviders) => {\n      this.identityProviders = identityProviders\n    })\n  },\n  data () {\n    return {\n      identityProviders: []\n    }\n  },\n  computed: {\n    currentIdentityProviderId: {\n      get () {\n        return this.identityProvider.id\n      },\n      set (identityProviderId) {\n        return this.$emit('identityProviderChange', this.identityProviders.find(({ id }) => id === identityProviderId))\n      }\n    }\n  }\n}\n</script>\n\n<!-- Add \"scoped\" attribute to limit CSS to this component only -->\n<style scoped lang=\"scss\">\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/IdentityProviderForm.vue",
    "content": "<template>\n  <div class=\"ui identity-provider-form segment\">\n    <FormErrors v-if=\"identityProvider.errors\" :errors=\"identityProvider.errors\" />\n    <form ref=\"form\" class=\"ui form\" @submit.prevent=\"submit\">\n      <div ref=\"tabularMenu\" class=\"ui top attached stackable tabular menu\">\n        <a id=\"general-configuration\" @click=\"openTab\" class=\"active item\">General configuration</a>\n        <a id=\"features\" @click=\"openTab\" class=\"item\">Features</a>\n      </div>\n      <div ref=\"general-configuration\" data-tab=\"general-configuration\" class=\"ui bottom attached active tab segment\">\n        <div class=\"field\" :class=\"{ 'error': identityProvider.errors?.name }\">\n          <label>Name</label>\n          <input type=\"text\" v-model=\"identityProvider.name\" placeholder=\"Super identity provider\">\n        </div>\n        <div class=\"field\" :class=\"{ 'error': identityProvider.errors?.backend_id }\">\n          <label>Backend</label>\n          <select v-model=\"identityProvider.backend_id\">\n            <option :value=\"backend.id\" v-for=\"backend in backends\">{{ backend.name }}</option>\n          </select>\n        </div>\n        <div v-if=\"identityProvider.isPersisted\" class=\"ui segment\">\n          <router-link\n            :to=\"{ name: 'edit-layout-template', params: { identityProviderId: identityProvider.id } }\"\n            class=\"ui fluid blue button\">Edit layout template</router-link>\n        </div>\n        <section v-if=\"identityProvider.isPersisted\">\n          <h3>Sessions</h3>\n          <div class=\"ui segment\">\n            <div class=\"ui toggle checkbox\">\n              <input type=\"checkbox\" v-model=\"identityProvider.check_password\">\n              <label>check password</label>\n            </div>\n            <hr />\n            <router-link\n              :to=\"{ name: 'edit-session-template', params: { identityProviderId: identityProvider.id } }\"\n              class=\"ui fluid blue button\">Edit login template</router-link>\n          </div>\n          <div v-show=\"displayResetPassword\">\n            <div class=\"ui segment\">\n              <router-link\n                :to=\"{ name: 'edit-new-reset-password-template', params: { identityProviderId: identityProvider.id } }\"\n                class=\"ui fluid blue button\">Edit send reset password instructions template</router-link>\n            </div>\n            <div class=\"ui segment\">\n              <router-link\n                :to=\"{ name: 'edit-edit-reset-password-template', params: { identityProviderId: identityProvider.id } }\"\n                class=\"ui fluid blue button\">Edit reset password template</router-link>\n            </div>\n          </div>\n        </section>\n      </div>\n      <div ref=\"features\" data-tab=\"features\" class=\"ui bottom attached tab segment\">\n        <section v-if=\"identityProvider.isPersisted\">\n          <h3>Choose session</h3>\n          <div class=\"ui segment\">\n            <div class=\"ui toggle checkbox\">\n              <input type=\"checkbox\" v-model=\"identityProvider.choose_session\">\n              <label>choose session</label>\n            </div>\n            <p class=\"ui info message\">\n              Give the ability for the user to choose to keep current session or to create a new one on authorization\n            </p>\n            <div v-if=\"identityProvider.choose_session\">\n              <router-link\n                :to=\"{ name: 'edit-choose-session-template', params: { identityProviderId: identityProvider.id } }\"\n                class=\"ui fluid blue button\">Edit choose session template</router-link>\n            </div>\n          </div>\n        </section>\n        <section v-show=\"displayWebauthnable\">\n          <h3>Webauthn</h3>\n          <div class=\"ui segment\">\n            <div class=\"field\">\n              <div class=\"ui toggle checkbox\">\n                <input type=\"checkbox\" v-model=\"identityProvider.webauthnable\">\n                <label>enable Webauthn</label>\n              </div>\n            </div>\n            <hr />\n            <div class=\"field\">\n              <div class=\"ui toggle checkbox\">\n                <input type=\"checkbox\" v-model=\"identityProvider.enforce_webauthn\">\n                <label>enforce Webauthn</label>\n              </div>\n            </div>\n            <p class=\"ui info message\">\n              Give the ability for end users to register an authentication second factor with Passkeys.\n            </p>\n            <div v-if=\"identityProvider.webauthnable\">\n              <router-link\n                :to=\"{ name: 'edit-webauthn-registration-template', params: { identityProviderId: identityProvider.id } }\"\n                class=\"ui fluid blue button\">Edit Webauthn registration template</router-link>\n              <hr />\n              <router-link\n                :to=\"{ name: 'edit-webauthn-authentication-template', params: { identityProviderId: identityProvider.id } }\"\n                class=\"ui fluid blue button\">Edit Webauthn authentication template</router-link>\n            </div>\n          </div>\n        </section>\n        <section v-show=\"displayTotpable\">\n          <h3>Time based One Time Password</h3>\n          <div class=\"ui segment\">\n            <div class=\"field\">\n              <div class=\"ui toggle checkbox\">\n                <input type=\"checkbox\" v-model=\"identityProvider.totpable\">\n                <label>enable TOTP</label>\n              </div>\n            </div>\n            <hr />\n            <div class=\"field\">\n              <div class=\"ui toggle checkbox\">\n                <input type=\"checkbox\" v-model=\"identityProvider.enforce_totp\">\n                <label>enforce TOTP</label>\n              </div>\n            </div>\n            <p class=\"ui info message\">\n              Give the ability for end users to register an authentication second factor with TOTP.\n            </p>\n            <div v-if=\"identityProvider.totpable\">\n              <router-link\n                :to=\"{ name: 'edit-totp-registration-template', params: { identityProviderId: identityProvider.id } }\"\n                class=\"ui fluid blue button\">Edit TOTP registration template</router-link>\n              <hr />\n              <router-link\n                :to=\"{ name: 'edit-totp-authentication-template', params: { identityProviderId: identityProvider.id } }\"\n                class=\"ui fluid blue button\">Edit TOTP authentication template</router-link>\n            </div>\n          </div>\n        </section>\n        <section v-show=\"displayRegistrable\">\n          <h3>Registration</h3>\n          <div class=\"ui segment\">\n            <div class=\"ui toggle checkbox\">\n              <input type=\"checkbox\" v-model=\"identityProvider.registrable\">\n              <label>registrable</label>\n            </div>\n            <p class=\"ui info message\">\n              Give the ability for end users to register within the given identity provider. If activated the user have access to registration page and can provide its own credentials.\n            </p>\n            <div v-if=\"identityProvider.registrable\">\n              <router-link\n                :to=\"{ name: 'edit-registration-template', params: { identityProviderId: identityProvider.id } }\"\n                class=\"ui fluid blue button\">Edit registration template</router-link>\n            </div>\n          </div>\n        </section>\n        <section v-show=\"displayUserEditable\">\n          <h3>User information edition</h3>\n          <div class=\"ui segment\">\n            <div class=\"ui toggle checkbox\">\n              <input type=\"checkbox\" v-model=\"identityProvider.user_editable\">\n              <label>user editable</label>\n            </div>\n            <p class=\"ui info message\">\n              Give the ability for end users to update their account information.\n            </p>\n            <div v-if=\"identityProvider.user_editable\">\n              <router-link\n                :to=\"{ name: 'edit-edit-user-template', params: { identityProviderId: identityProvider.id } }\"\n                class=\"ui fluid blue button\">Edit user edition template</router-link>\n            </div>\n          </div>\n        </section>\n        <section v-show=\"displayConfirmable\">\n          <h3>Email confirmation</h3>\n          <div class=\"ui segment\">\n            <div class=\" field\">\n              <div class=\"ui toggle checkbox\">\n                <input type=\"checkbox\" v-model=\"identityProvider.confirmable\">\n                <label>confirmable</label>\n              </div>\n              <p class=\"ui info message\">\n                Confirm new registred accounts. sends an email in order to confirm user's email.\n              </p>\n              <div v-if=\"identityProvider.confirmable\">\n                <router-link\n                  :to=\"{ name: 'edit-new-confirmation-template', params: { identityProviderId: identityProvider.id } }\"\n                  class=\"ui fluid blue button\">Edit send confirmation template</router-link>\n              </div>\n            </div>\n          </div>\n        </section>\n        <section v-show=\"displayConsentable\">\n          <h3>User consent</h3>\n          <div class=\"ui segment\">\n            <div class=\" field\">\n              <div class=\"ui toggle checkbox\">\n                <input type=\"checkbox\" v-model=\"identityProvider.consentable\">\n                <label>user consent</label>\n              </div>\n              <p class=\"ui info message\">\n                Users have to consent requested scopes to be authorized.\n              </p>\n              <div v-if=\"identityProvider.consentable\">\n                <router-link\n                  :to=\"{ name: 'edit-new-consent-template', params: { identityProviderId: identityProvider.id } }\"\n                  class=\"ui fluid blue button\">Edit consent template</router-link>\n              </div>\n            </div>\n          </div>\n        </section>\n        <section v-show=\"identityProvider.isPersisted\">\n          <h3>Credential offer</h3>\n          <div class=\"ui segment\">\n            <div class=\" field\">\n              <p class=\"ui info message\">\n                SSI credential offer page\n              </p>\n              <div>\n                <router-link\n                  :to=\"{ name: 'edit-credential-offer-template', params: { identityProviderId: identityProvider.id } }\"\n                  class=\"ui fluid blue button\">Edit credential offer template</router-link>\n              </div>\n            </div>\n          </div>\n          <h3>Credential presentation</h3>\n          <div class=\"ui segment\">\n            <div class=\" field\">\n              <p class=\"ui info message\">\n                SSI credential presentation page\n              </p>\n              <div>\n                <router-link\n                  :to=\"{ name: 'edit-cross-device-presentation-template', params: { identityProviderId: identityProvider.id } }\"\n                  class=\"ui fluid blue button\">Edit cross device presentation template</router-link>\n              </div>\n            </div>\n          </div>\n        </section>\n      </div>\n      <div class=\"actions\">\n        <button class=\"ui violet button\" type=\"submit\">{{ action }}</button>\n      </div>\n    </form>\n  </div>\n</template>\n\n<script>\nimport Backend from '../../models/backend.model'\nimport FormErrors from './FormErrors.vue'\n\nexport default {\n  name: 'identity-provider-form',\n  props: ['identityProvider', 'action'],\n  components: {\n    FormErrors\n  },\n  data() {\n    return {\n      backends: []\n    }\n  },\n  computed: {\n    displayConfirmable () {\n      return this.identityProvider.isPersisted && this.identityProvider.backend.features?.includes('confirmable')\n    },\n    displayResetPassword () {\n      return this.identityProvider.backend.features?.includes('reset_password')\n    },\n    displayRegistrable () {\n      return this.identityProvider.isPersisted && this.identityProvider.backend.features?.includes('registrable')\n    },\n    displayWebauthnable () {\n      return this.identityProvider.isPersisted && this.identityProvider.backend.features?.includes('webauthnable')\n    },\n    displayTotpable () {\n      return this.identityProvider.isPersisted && this.identityProvider.backend.features?.includes('totpable')\n    },\n    displayUserEditable () {\n      return this.identityProvider.isPersisted && this.identityProvider.backend.features?.includes('user_editable')\n    },\n    displayConsentable () {\n      return this.identityProvider.isPersisted && this.identityProvider.backend.features?.includes('consentable')\n    }\n  },\n  mounted () {\n    Backend.all().then(backends => this.backends = backends)\n  },\n  methods: {\n    openTab (e) {\n      const tab = e.target.id\n      Array.from(this.$refs.tabularMenu.getElementsByClassName('item')).forEach(e => {\n        if (e.id == tab) {\n          e.classList.add('active')\n          this.$refs[e.id].classList.add('active')\n        } else {\n          e.classList.remove('active')\n          this.$refs[e.id].classList.remove('active')\n        }\n      })\n    }\n  },\n  watch: {\n    identityProvider: {\n      handler ({ backend_id }) {\n        this.identityProvider.backend = this.backends.find(({ id }) => id === backend_id) || this.identityProvider.backend\n      },\n      deep: true\n    },\n    'identityProvider.errors': {\n      deep: true,\n      handler (errors) {\n        setTimeout(() => {\n          Array.from(this.$refs.tabularMenu.getElementsByClassName('error')).forEach(e => {\n            e.classList.remove('error')\n          })\n          Array.from(this.$refs.form.getElementsByClassName('error')).forEach(elt => {\n            const tab = elt.closest('.tab').getAttribute('data-tab')\n            this.$refs.tabularMenu.querySelector('#' + tab).classList.add('error')\n          })\n        }, 100)\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.identity-provider-form {\n  section {\n    margin-bottom: 1rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/OrganizationForm.vue",
    "content": "<template>\n  <div class=\"ui organization-form segment\">\n    <FormErrors :errors=\"organization.errors\" v-if=\"organization.errors\" />\n    <form class=\"ui form\" @submit.prevent=\"submit\">\n      <h2>General configuration</h2>\n      <div class=\"field\">\n        <label>Name</label>\n        <input type=\"text\" v-model=\"organization.name\" placeholder=\"wonder-organization\" />\n      </div>\n      <div class=\"field\">\n        <label>Label</label>\n        <input type=\"text\" v-model=\"organization.label\" placeholder=\"Wonder organization\" />\n      </div>\n      <div class=\"actions\">\n        <button class=\"ui violet button\" type=\"submit\">{{ action }}</button>\n      </div>\n    </form>\n  </div>\n</template>\n\n<script>\nimport FormErrors from './FormErrors.vue'\n\nexport default {\n  name: 'organization-form',\n  props: ['organization', 'action'],\n  components: {\n    FormErrors\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/OrganizationsField.vue",
    "content": "<template>\n  <div class=\"field edit-organizations\">\n    <div v-for=\"(organization, index) in currentOrganizations\" class=\"field\" :key=\"index\">\n      <div class=\"ui right icon input\">\n        <select type=\"text\" v-model=\"organization.model\" class=\"organizations-select\">\n          <option :value=\"organization\" v-for=\"organization in organizationOptions(organization.model)\" :key=\"organization.id\">{{ organization.name }}</option>\n        </select>\n        <i v-on:click=\"deleteOrganization(organization)\" class=\"close icon\"></i>\n      </div>\n    </div>\n    <a v-on:click.prevent=\"addOrganization()\" class=\"ui blue fluid button\">Add an organization</a>\n  </div>\n</template>\n\n<script>\nimport Organization from '../../models/organization.model'\n\nexport default {\n  name: 'OrganizationsField',\n  props: ['currentOrganizations'],\n  data () {\n    return {\n      organizations: []\n    }\n  },\n  computed: {\n    organizationOptions () {\n      const vm = this\n      return function (organization) {\n        return vm.organizations.map((organization) => {\n          if (organization.id === organization.id) {\n            return organization\n          }\n          return organization\n        })\n      }\n    }\n  },\n  mounted () {\n    Organization.all().then(({ data: organizations }) => {\n      this.organizations = organizations\n    })\n  },\n  methods: {\n    deleteOrganization (organization) {\n      this.$emit('delete-organization', organization)\n    },\n    addOrganization () {\n      this.$emit('add-organization')\n    }\n  }\n}\n</script>\n\n<!-- Add \"organizationd\" attribute to limit CSS to this component only -->\n<style organizationd lang=\"scss\">\n.edit-organizations {\n  .ui.icon.input>i.icon.close {\n    cursor: pointer;\n    pointer-events: all;\n  }\n  .organizations-select {\n    margin-right: 3em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/RoleForm.vue",
    "content": "<template>\n  <div class=\"role-form\">\n    <div class=\"ui segment\">\n      <FormErrors v-if=\"role.errors\" :errors=\"role.errors\" />\n      <form class=\"ui form\" @submit.prevent=\"submit\">\n        <div class=\"field\" :class=\"{ 'error': role.errors?.name }\">\n          <label>Name</label>\n          <input type=\"text\" v-model=\"role.name\" placeholder=\"administrator\">\n        </div>\n        <hr />\n        <ScopesField :currentScopes=\"role.scopes\" @addScope=\"addScope\" @deleteScope=\"deleteScope\" />\n        <button class=\"ui right floated violet button\" type=\"submit\">{{ action }}</button>\n        <a class=\"ui button\" v-on:click=\"back()\">Back</a>\n      </form>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Scope from '../../models/scope.model'\nimport Role from '../../models/role.model'\nimport FormErrors from '../../components/Forms/FormErrors.vue'\nimport ScopesField from '../../components/Forms/ScopesField.vue'\n\nexport default {\n  name: 'role-form',\n  props: ['role', 'action'],\n  components: {\n    FormErrors,\n    ScopesField\n  },\n  mounted () {\n  },\n  methods: {\n    back () {\n      this.$emit('back')\n    },\n    addScope () {\n      this.role.scopes.push({ model: new Scope(), method: 'GET' })\n    },\n    deleteScope (scope) {\n      this.role.scopes.splice(\n        this.role.scopes.indexOf(scope),\n        1\n      )\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.role-form {\n  .field {\n    position: relative;\n    &.roles input {\n      margin-right: 3em;\n    }\n  }\n  .ui.icon.input>i.icon.close {\n    cursor: pointer;\n    pointer-events: all;\n    position: absolute;\n  }\n  .authorized-scopes-select {\n    margin-right: 3em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/RolesField.vue",
    "content": "<template>\n  <div class=\"field edit-roles\">\n    <div v-for=\"(role, index) in currentRoles\" class=\"field\" :key=\"index\">\n      <div class=\"ui right icon input\">\n        <select type=\"text\" v-model=\"role.model\" class=\"roles-select\">\n          <option :value=\"role\" v-for=\"role in roleOptions(role.model)\" :key=\"role.id\">{{ role.name }}</option>\n        </select>\n        <i v-on:click=\"deleteRole(role)\" class=\"close icon\"></i>\n      </div>\n    </div>\n    <a v-on:click.prevent=\"addRole()\" class=\"ui blue fluid button\">Add a role</a>\n  </div>\n</template>\n\n<script>\nimport Role from '../../models/role.model'\n\nexport default {\n  name: 'RolesField',\n  props: ['currentRoles'],\n  data () {\n    return {\n      roles: []\n    }\n  },\n  computed: {\n    roleOptions () {\n      const vm = this\n      return function (role) {\n        return vm.roles.map((currentRole) => {\n          if (role.id === currentRole.id) {\n            return role\n          }\n          return currentRole\n        })\n      }\n    }\n  },\n  mounted () {\n    Role.all().then((roles) => {\n      this.roles = roles\n    })\n  },\n  methods: {\n    deleteRole (role) {\n      this.$emit('delete-role', role)\n    },\n    addRole () {\n      this.$emit('add-role')\n    }\n  }\n}\n</script>\n\n<!-- Add \"roled\" attribute to limit CSS to this component only -->\n<style roled lang=\"scss\">\n.edit-roles {\n  .ui.icon.input>i.icon.close {\n    cursor: pointer;\n    pointer-events: all;\n  }\n  .roles-select {\n    margin-right: 3em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/ScopesField.vue",
    "content": "<template>\n  <div class=\"field edit-scopes\">\n    <div v-for=\"(authorizedScope, index) in currentScopes\" class=\"field\" :key=\"index\">\n      <div class=\"ui right icon input\">\n        <select type=\"text\" v-model=\"authorizedScope.model\" class=\"authorized-scopes-select\">\n          <option :value=\"scope\" v-for=\"scope in scopeOptions(authorizedScope.model)\" :key=\"scope.id\">{{ scope.name }}</option>\n        </select>\n        <i v-on:click=\"deleteScope(authorizedScope)\" class=\"close icon\"></i>\n      </div>\n    </div>\n    <a v-on:click.prevent=\"addScope()\" class=\"ui blue fluid button\">Add a scope</a>\n  </div>\n</template>\n\n<script>\nimport Scope from '../../models/scope.model'\n\nexport default {\n  name: 'ScopesField',\n  props: ['currentScopes'],\n  data () {\n    return {\n      scopes: []\n    }\n  },\n  computed: {\n    scopeOptions () {\n      const vm = this\n      return function (authorizedScope) {\n        return vm.scopes.map((scope) => {\n          if (authorizedScope.name === scope.name) {\n            return authorizedScope\n          }\n          return scope\n        })\n      }\n    }\n  },\n  mounted () {\n    Scope.all().then((scopes) => {\n      this.scopes = scopes\n    })\n  },\n  methods: {\n    deleteScope (scope) {\n      this.$emit('delete-scope', scope)\n    },\n    addScope () {\n      this.$emit('add-scope')\n    }\n  }\n}\n</script>\n\n<!-- Add \"scoped\" attribute to limit CSS to this component only -->\n<style scoped lang=\"scss\">\n.edit-scopes {\n  .ui.icon.input>i.icon.close {\n    cursor: pointer;\n    pointer-events: all;\n  }\n  .authorized-scopes-select {\n    margin-right: 3em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/ScopesFieldByName.vue",
    "content": "<template>\n  <div class=\"field edit-scopes\">\n    <div v-for=\"(scope, index) in currentScopes\" class=\"field\" :key=\"index\">\n      <div class=\"ui right icon input\">\n        <select type=\"text\" v-model=\"scope.name\" class=\"scopes-select\">\n          <option :value=\"scope.name\" v-for=\"scope in scopes\" :key=\"scope.id\">{{ scope.name }}</option>\n        </select>\n        <i v-on:click=\"deleteScope(scope)\" class=\"close icon\"></i>\n      </div>\n    </div>\n    <a v-on:click.prevent=\"addScope()\" class=\"ui blue fluid button\">Add a scope</a>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'ScopesFieldByName',\n  props: ['currentScopes', 'scopes'],\n  methods: {\n    deleteScope (scope) {\n      this.$emit('delete-scope', scope)\n    },\n    addScope () {\n      this.$emit('add-scope')\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-scopes {\n  .ui.icon.input>i.icon.close {\n    cursor: pointer;\n    pointer-events: all;\n  }\n  .scopes-select {\n    margin-right: 3em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/TextEditor.vue",
    "content": "<template>\n  <div class=\"text-editor\">\n    <div class=\"editor lang-markup\" ref=\"editor\"></div>\n  </div>\n</template>\n\n<script>\nimport { CodeJar } from 'codejar'\nimport { withLineNumbers } from 'codejar/linenumbers'\nimport { highlight, languages } from 'prismjs'\n\nexport default {\n  name: 'TextEditor',\n  props: ['content'],\n  mounted () {\n    const highlightFunc = (editor) => {\n      const code = editor.textContent\n\n      editor.innerHTML = highlight(code, languages.markup, 'markup')\n    }\n\n    const editor = CodeJar(this.$refs.editor, withLineNumbers(highlightFunc), {\n      tab: ' '.repeat(2),\n      spellcheck: false\n    })\n\n    editor.onUpdate(code => {\n      this.$emit('codeUpdate', code)\n    })\n\n    this.editor = editor\n    this.editor.updateCode(this.content)\n  },\n  watch: {\n    content(newContent, content) {\n      if (!content) this.editor.updateCode(this.content)\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n.text-editor {\n  height: 100%;\n  width: 100%;\n  overflow: hidden;\n  .codejar-wrap {\n    height: 100%;\n    .codejar-linenumbers {\n      color: orange !important;\n      height: 100%;\n      padding-left: .5em !important;\n    }\n    .editor {\n      background: #f5f2f0;\n      cursor: text;\n      height: 100%;\n      border-radius: 6px;\n      box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);\n      font-family: 'Source Code Pro', monospace;\n      font-weight: 400;\n      letter-spacing: normal;\n      line-height: 20px;\n      tab-size: 4;\n      .token.attr-name {\n        color: #690;\n      }\n      .token.selector {\n        color: #c90;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/UpstreamForm.vue",
    "content": "<template>\n  <div class=\"ui upstream-form segment\">\n    <FormErrors v-if=\"upstream.errors\" :errors=\"upstream.errors\" />\n    <form class=\"ui form\" @submit.prevent=\"submit\">\n      <div ref=\"tabularMenu\" class=\"ui top attached stackable tabular menu\">\n        <a id=\"general-configuration\" @click=\"openTab\" class=\"active item\">General configuration</a>\n        <a id=\"uris\" @click=\"openTab\" class=\"item\">URIs</a>\n        <a id=\"authorization\" @click=\"openTab\" class=\"item\">Authorization</a>\n      </div>\n      <div ref=\"form\">\n        <div ref=\"general-configuration\" data-tab=\"general-configuration\" class=\"ui bottom attached active tab segment\">\n          <h2>General configuration</h2>\n          <div class=\"field\" :class=\"{ 'error': upstream.errors?.node_name }\">\n            <label>Node</label>\n            <select v-model=\"upstream.node_name\" placeholder=\"global\">\n              <option value=\"global\">global</option>\n              <option v-for=\"name in nodeNames\" :value=\"name\">{{ name }}</option>\n            </select>\n          </div>\n          <div class=\"field\" :class=\"{ 'error': upstream.errors?.scheme }\">\n            <label>Scheme</label>\n            <select v-model=\"upstream.scheme\" placeholder=\"https\">\n              <option value=\"https\">https</option>\n              <option value=\"http\">http</option>\n            </select>\n          </div>\n          <div class=\"field\" :class=\"{ 'error': upstream.errors?.host }\">\n            <label>Host</label>\n            <input type=\"text\" v-model=\"upstream.host\" placeholder=\"host.test\">\n          </div>\n          <div class=\"field\" :class=\"{ 'error': upstream.errors?.port }\">\n            <label>Port</label>\n            <input type=\"text\" v-model=\"upstream.port\" placeholder=\"443\">\n          </div>\n          <div class=\"field\" :class=\"{ 'error': upstream.errors?.pool_count }\">\n            <label>Pool count</label>\n            <input type=\"number\" v-model=\"upstream.pool_count\" placeholder=\"10\">\n          </div>\n          <div class=\"field\" :class=\"{ 'error': upstream.errors?.pool_size }\">\n            <label>Pool size</label>\n            <input type=\"number\" v-model=\"upstream.pool_size\" placeholder=\"10\">\n          </div>\n          <div class=\"field\" :class=\"{ 'error': upstream.errors?.max_idle_time }\">\n            <label>Max idle time</label>\n            <input type=\"number\" v-model=\"upstream.max_idle_time\" placeholder=\"10\">\n          </div>\n        </div>\n        <div ref=\"uris\" data-tab=\"uris\" class=\"ui bottom attached tab segment\">\n          <h2>URIs</h2>\n          <div class=\"upstreams field\" :class=\"{ 'error': upstream.errors?.uris }\">\n            <div v-for=\"(upstreamUri, index) in upstream.uris\" class=\"field\" :key=\"index\">\n              <div class=\"ui right icon input\">\n                <input type=\"text\" v-model=\"upstreamUri.uri\" placeholder=\"/matching (without trailing slash)\" />\n                <i v-on:click=\"deleteUpstreamUri(upstreamUri)\" class=\"close icon\"></i>\n              </div>\n            </div>\n            <a v-on:click.prevent=\"addUpstreamUri()\" class=\"ui blue fluid button\">Add an upstream uri</a>\n          </div>\n          <div class=\"field\">\n            <div class=\"ui toggle checkbox\">\n              <input type=\"checkbox\" v-model=\"upstream.strip_uri\">\n              <label>Strip URI</label>\n            </div>\n          </div>\n        </div>\n        <div ref=\"authorization\" data-tab=\"authorization\" class=\"ui bottom attached tab segment\">\n          <h3>Authorization</h3>\n          <div class=\"field\">\n            <div class=\"ui toggle checkbox\">\n              <input type=\"checkbox\" v-model=\"upstream.authorize\">\n              <label>Authorize</label>\n            </div>\n          </div>\n          <div class=\"field\" v-if=\"upstream.authorize\" :class=\"{ 'error': upstream.errors?.required_scopes }\">\n            <label>Required scopes <i>(leave empty to not filter)</i></label>\n            <GatewayScopesField :currentScopes=\"upstream.required_scopes\" @delete-scope=\"deleteScope\" @add-scope=\"addScope\" />\n          </div>\n          <div v-if=\"upstream.authorize\">\n            <h4>Error templates</h4>\n            <div class=\"field\" :class=\"{ 'error': upstream.errors?.error_content_type }\">\n              <label>Error content type</label>\n              <input type=\"text\" v-model=\"upstream.error_content_type\" placeholder=\"text\">\n            </div>\n            <div class=\"field\" :class=\"{ 'error': upstream.errors?.forbidden_response }\">\n              <label>Forbidden response</label>\n              <textarea v-model=\"upstream.forbidden_response\" placeholder=\"You are forbidden to access this resource.\"></textarea>\n            </div>\n            <div class=\"field\" :class=\"{ 'error': upstream.errors?.unauthorized_response }\">\n              <label>Unauthorized response</label>\n              <textarea v-model=\"upstream.unauthorized_response\" placeholder=\"You are unauthorized to access this resource.\"></textarea>\n            </div>\n          </div>\n          <h3>Forwarded authorization</h3>\n          <div class=\"ui segment\">\n            <div class=\"inline fields\" :class=\"{ 'error': upstream.errors?.forwarded_token_signature_alg }\">\n              <label>Forwarded token signature algorithm</label>\n              <div class=\"field\" v-for=\"alg in forwardedTokenSignatureAlgorithms\" :key=\"alg\">\n                <div class=\"ui radio checkbox\">\n                  <label>{{ alg }}</label>\n                  <input type=\"radio\" v-model=\"upstream.forwarded_token_signature_alg\" :value=\"alg\" />\n                </div>\n              </div>\n            </div>\n          </div>\n          <div v-if=\"isHsAlgorithm\" class=\"field\" :class=\"{ 'error': upstream.errors?.forwarded_token_secret }\">\n            <label>Forwarded token secret <em>(leave blank to autogenerate)</em></label>\n            <input type=\"text\" v-model=\"upstream.forwarded_token_secret\" placeholder=\"text\">\n          </div>\n          <div v-if=\"isRsAlgorithm\">\n            <div class=\"field\" :class=\"{ 'error': upstream.errors?.forwarded_token_private_key }\">\n              <label>Forwarded token private key <em>(leave blank to autogenerate)</em></label>\n              <textarea v-model=\"upstream.forwarded_token_private_key\"></textarea>\n            </div>\n            <div class=\"field\" :class=\"{ 'error': upstream.errors?.forwarded_token_public_key }\">\n              <label>Forwarded token public key</label>\n              <textarea v-model=\"upstream.forwarded_token_public_key\"></textarea>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div class=\"actions\">\n        <button class=\"ui right floated violet button\" type=\"submit\">{{ action }}</button>\n      </div>\n    </form>\n  </div>\n</template>\n\n<script>\nimport Scope from '../../models/scope.model'\nimport Upstream from '../../models/upstream.model'\nimport GatewayScopesField from '../../components/Forms/GatewayScopesField.vue'\nimport FormErrors from '../../components/Forms/FormErrors.vue'\n\nexport default {\n  name: 'upstream-form',\n  props: ['upstream', 'action'],\n  components: {\n    FormErrors,\n    GatewayScopesField\n  },\n  data () {\n    return {\n      nodeNames: [],\n      forwardedTokenSignatureAlgorithms: Upstream.forwardedTokenSignatureAlgorithms\n    }\n  },\n  mounted () {\n    Upstream.nodeList().then(nodes => this.nodeNames = nodes)\n  },\n  computed: {\n    isHsAlgorithm () {\n      return this.upstream.forwarded_token_signature_alg?.match(/HS/)\n    },\n    isRsAlgorithm () {\n      return this.upstream.forwarded_token_signature_alg?.match(/RS/)\n    }\n  },\n  methods: {\n    back () {\n      this.$emit('back')\n    },\n    addUpstreamUri () {\n      this.upstream.uris.push({})\n    },\n    deleteUpstreamUri (uri) {\n      this.upstream.uris.splice(\n        this.upstream.uris.indexOf(uri),\n        1\n      )\n    },\n    addScope () {\n      this.upstream.required_scopes.push({ model: new Scope(), method: 'GET' })\n    },\n    deleteScope (scope) {\n      this.upstream.required_scopes.splice(\n        this.upstream.required_scopes.indexOf(scope),\n        1\n      )\n    },\n    openTab (e) {\n      const tab = e.target.id\n      Array.from(this.$refs.tabularMenu.getElementsByClassName('item')).forEach(e => {\n        if (e.id == tab) {\n          e.classList.add('active')\n          this.$refs[e.id].classList.add('active')\n        } else {\n          e.classList.remove('active')\n          this.$refs[e.id].classList.remove('active')\n        }\n      })\n    }\n  },\n  watch: {\n    'upstream.errors': {\n      deep: true,\n      handler (errors) {\n        setTimeout(() => {\n          Array.from(this.$refs.tabularMenu.getElementsByClassName('error')).forEach(e => {\n            e.classList.remove('error')\n          })\n          Array.from(this.$refs.form.getElementsByClassName('error')).forEach(elt => {\n            const tab = elt.closest('.tab').getAttribute('data-tab')\n            this.$refs.tabularMenu.querySelector('#' + tab).classList.add('error')\n          })\n        }, 100)\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.upstream-form {\n  .field {\n    position: relative;\n    &.upstreams input {\n      margin-right: 3em;\n    }\n  }\n  .ui.icon.input>i.icon.close {\n    cursor: pointer;\n    pointer-events: all;\n    position: absolute;\n  }\n  .authorized-scopes-select {\n    margin-right: 3em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Forms/UserForm.vue",
    "content": "<template>\n  <div class=\"ui user-form segment\">\n    <FormErrors :errors=\"user.errors\" v-if=\"user.errors\" />\n    <form class=\"ui form\" @submit.prevent=\"submit\">\n      <div ref=\"tabularMenu\" class=\"ui top attached stackable tabular menu\">\n        <a id=\"general-configuration\" @click=\"openTab\" class=\"active item\">General configuration</a>\n        <a id=\"authorization\" @click=\"openTab\" class=\"item\">Authorization</a>\n      </div>\n      <div ref=\"general-configuration\" data-tab=\"general-configuration\" class=\"ui bottom attached active tab segment\">\n        <h2>General configuration</h2>\n        <div class=\"field\" v-if=\"!user.isPersisted\" :class=\"{ 'error': user.errors?.backend }\">\n          <label>Backend</label>\n          <select v-model=\"user.backend_id\">\n            <option :value=\"backend.id\" v-for=\"backend in backends\" :key=\"backend.id\">{{ backend.name }}</option>\n          </select>\n        </div>\n        <div class=\"field\" v-if=\"!user.isPersisted\" :class=\"{ 'error': user.errors?.email }\">\n          <label>Email</label>\n          <input type=\"text\" v-model=\"user.email\" placeholder=\"email@example.com\" />\n        </div>\n        <div class=\"field\" v-if=\"!user.isPersisted\" :class=\"{ 'error': user.errors?.password }\">\n          <label>Password</label>\n          <input type=\"password\" v-model=\"user.password\" />\n        </div>\n        <div class=\"field\" :class=\"{ 'error': user.errors?.group }\">\n          <label>Group</label>\n          <input type=\"text\" v-model=\"user.group\" />\n        </div>\n        <section v-if=\"user.backend.metadata_fields.length\">\n          <h3>Metadata</h3>\n          <div class=\"ui metadata segment\" v-for=\"field in user.backend.metadata_fields\">\n            <h4>{{ field.attribute_name }}</h4>\n            <div class=\"ui three column stackable grid\">\n              <div class=\"column\">\n                <div class=\"field\" :class=\"{ 'error': user.errors?.metadata }\">\n                  <label>Value</label>\n                  <input type=\"text\" v-model=\"user.metadata[field.attribute_name].value\" placeholder=\"metadata\" />\n                </div>\n              </div>\n              <div class=\"column\">\n                <div class=\"field\" :class=\"{ 'error': user.errors?.metadata }\">\n                  <label>Verifiable credential status</label>\n                  <select v-model=\"user.metadata[field.attribute_name].status\">\n                    <option value=\"valid\">valid</option>\n                    <option value=\"suspended\">suspended</option>\n                    <option value=\"revoked\">revoked</option>\n                  </select>\n                </div>\n              </div>\n              <div class=\"column\">\n                <div class=\"field\">\n                  <label>Claim format</label>\n                  <div class=\"ui toggle checkbox\">\n                    <input type=\"checkbox\" v-model=\"user.metadata[field.attribute_name].displayStatus\">\n                    <label>display status</label>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </section>\n        <h3>Organizations</h3>\n        <OrganizationsField :currentOrganizations=\"user.organizations\" @delete-organization=\"deleteOrganization\" @add-organization=\"addOrganization\" />\n      </div>\n      <div ref=\"authorization\" data-tab=\"authorization\" class=\"ui bottom attached tab segment\">\n        <h2>Authorization</h2>\n        <h3>Roles</h3>\n        <RolesField :currentRoles=\"user.roles\" @delete-role=\"deleteRole\" @add-role=\"addRole\" />\n        <h3>Authorized scopes</h3>\n        <ScopesField :currentScopes=\"user.authorized_scopes\" @delete-scope=\"deleteScope\" @add-scope=\"addScope\" />\n      </div>\n      <div class=\"actions\">\n        <button class=\"ui right floated violet button\" type=\"submit\">{{ action }}</button>\n      </div>\n    </form>\n  </div>\n</template>\n\n<script>\nimport Scope from '../../models/scope.model'\nimport Role from '../../models/role.model'\nimport Organization from '../../models/organization.model'\nimport Backend from '../../models/backend.model'\nimport ScopesField from './ScopesField.vue'\nimport RolesField from './RolesField.vue'\nimport OrganizationsField from './OrganizationsField.vue'\nimport FormErrors from './FormErrors.vue'\n\nexport default {\n  name: 'user-form',\n  props: ['user', 'action'],\n  components: {\n    ScopesField,\n    RolesField,\n    OrganizationsField,\n    FormErrors\n  },\n  data () {\n    return {\n      scopes: [],\n      backends: []\n    }\n  },\n  mounted () {\n    Scope.all().then((scopes) => {\n      this.scopes = scopes\n    })\n    Backend.all().then((backends) => {\n      this.backends = backends\n    })\n  },\n  methods: {\n    addScope () {\n      this.user.authorized_scopes.push({ model: new Scope() })\n    },\n    deleteScope (scope) {\n      this.user.authorized_scopes.splice(\n        this.user.authorized_scopes.indexOf(scope),\n        1\n      )\n    },\n    addRole () {\n      this.user.roles.push({ model: new Role() })\n    },\n    deleteRole (role) {\n      this.user.roles.splice(\n        this.user.roles.indexOf(role),\n        1\n      )\n    },\n    addOrganization () {\n      this.user.organizations.push({ model: new Organization() })\n    },\n    deleteOrganization (organization) {\n      this.user.organizations.splice(\n        this.user.organizations.indexOf(organization),\n        1\n      )\n    },\n    openTab (e) {\n      const tab = e.target.id\n      Array.from(this.$refs.tabularMenu.getElementsByClassName('item')).forEach(e => {\n        if (e.id == tab) {\n          e.classList.add('active')\n          this.$refs[e.id].classList.add('active')\n        } else {\n          e.classList.remove('active')\n          this.$refs[e.id].classList.remove('active')\n        }\n      })\n    }\n  },\n  watch: {\n    user: {\n      handler ({ backend_id }) {\n        this.user.backend = this.backends.find(({ id }) => id === backend_id) || this.user.backend\n      },\n      deep: true\n    },\n    'user.backend': {\n      handler() {\n        this.user.backend.metadata_fields.forEach((field) => {\n          this.user.metadata[field.attribute_name] ||= { status: 'valid' }\n        })\n      }\n    },\n    'user.errors': {\n      deep: true,\n      handler (errors) {\n        setTimeout(() => {\n          Array.from(this.$refs.tabularMenu.getElementsByClassName('error')).forEach(e => {\n            e.classList.remove('error')\n          })\n          Array.from(this.$refs.form.getElementsByClassName('error')).forEach(elt => {\n            const tab = elt.closest('.tab').getAttribute('data-tab')\n            this.$refs.tabularMenu.querySelector('#' + tab).classList.add('error')\n          })\n        }, 100)\n      }\n    },\n  }\n}\n</script>\n\n<style lang=\"scss\">\n.metadata .toggle {\n  padding: 8px;\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Header.vue",
    "content": "<template>\n  <div class=\"header\" :class=\"{ 'dark': darkMode }\">\n    <div class=\"ui main menu\" :class=\"{ 'inverted': darkMode }\">\n      <router-link :to=\"{ name: 'home' }\" class=\"logo item\">\n        <img src=\"../assets/images/logo-inverted.png\" v-if=\"darkMode\" />\n        <img src=\"../assets/images/logo.png\" v-else />\n      </router-link>\n      <div class=\"right menu\">\n        <span class=\"ui email item\">\n          {{ currentUser.email }}\n        </span>\n        <a v-on:click.prevent=\"logout()\" class=\"ui item\">\n          <i class=\"ui power off icon\"></i>\n        </a>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapGetters } from 'vuex'\nimport oauth from '../services/oauth.service'\n\nexport default {\n  name: 'Header',\n  props: ['darkMode'],\n  computed: {\n    currentUser() {\n      return oauth.currentUser\n    },\n    ...mapGetters(['isAuthenticated'])\n  },\n  methods: {\n    logout () {\n      this.$store.dispatch('logout')\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.header.dark {\n  background: #1b1c1d;\n  .main.menu {\n    border: 1px solid rgba(255,255,255,.08);\n  }\n}\n.header {\n  background: white;\n  max-width: 100%;\n  overflow: hidden;\n  .item.logo {\n    min-width: 199px;\n    background: inherit!important;\n    padding: 0 1rem 0 .7rem;\n    &:before {\n      display: none;\n    }\n    img {\n      max-width: calc(199px - 1.7rem);\n      max-height: 22px;\n      width: auto;\n    }\n  }\n  .power.off.icon {\n    margin: 0!important;\n  }\n  .main.menu {\n    border-radius: 0;\n  }\n  @media screen and (max-width: 768px) {\n    .item.email {\n      display: none;\n    }\n  }\n  @media screen and (max-width: 1127px) {\n    position: fixed;\n    z-index: 100;\n    width: 100%;\n    padding-left: 3.2em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/Toaster.vue",
    "content": "<template>\n  <div @mouseover=\"activate()\" @mouseleave=\"deactivate()\" class=\"ui icon hidden floating message\" :class=\"type\" ref=\"success\">\n    <i class=\"exclamation circle icon\"></i>\n    <div class=\"header\">\n      {{ message }}\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'success-toaster',\n  props: ['active', 'message', 'type'],\n  data () {\n    return {\n      timer: null\n    }\n  },\n  methods: {\n    activate() {\n      clearTimeout(this.timer)\n    },\n    deactivate() {\n      this.$refs.success.classList.add('hidden')\n    }\n  },\n  watch: {\n    active(active) {\n      if (!active) return\n\n      this.$refs.success.classList.remove('hidden')\n\n      clearTimeout(this.timer)\n      this.timer = setTimeout(() => {\n        this.$refs.success?.classList.add('hidden')\n      }, 5000)\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.ui.message {\n  transition: transform .3s ease-in-out;\n  position: fixed;\n  width: auto;\n  z-index: 1000;\n  top: 4em;\n  right: 1em;\n  cursor: pointer;\n  &.hidden {\n    display: flex!important;\n    transform: translateX(200%);\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/components/VerifiableCredentialClaim.vue",
    "content": "<template>\n  <div class=\"ui claim segment\">\n    <i class=\"ui large close icon\" @click=\"deleteVerifiableCredentialClaim(credential, claim)\"></i>\n    <h5>Claim definition</h5>\n    <div class=\"field\" :class=\"{ 'error': errors?.verifiable_credentials }\">\n      <label>Type</label>\n      <select v-model=\"claim.type\">\n        <optgroup label=\"attribute\">\n          <option :value=\"type\" v-for=\"type in attributeClaimTypes\">{{ type }}</option>\n        </optgroup>\n        <optgroup label=\"object\">\n          <option :value=\"type\" v-for=\"type in objectClaimTypes\">{{ type }}</option>\n        </optgroup>\n        <optgroup label=\"array\">\n          <option :value=\"type\" v-for=\"type in arrayClaimTypes\">{{ type }}</option>\n        </optgroup>\n      </select>\n    </div>\n    <div v-if=\"isAttribute\">\n      <div class=\"field\" :class=\"{ 'error': errors?.verifiable_credentials }\">\n        <label>Name</label>\n        <input :disabled=\"claim.freeze\" type=\"text\" v-model=\"claim.name\" placeholder=\"family_name\">\n      </div>\n      <div class=\"field\" :class=\"{ 'error': errors?.verifiable_credentials }\">\n        <label>Label</label>\n        <input :disabled=\"claim.freeze\" type=\"text\" v-model=\"claim.label\" placeholder=\"Family name\">\n      </div>\n      <div class=\"field\" :class=\"{ 'error': errors?.verifiable_credentials }\">\n        <label>Pointer</label>\n        <input type=\"text\" v-model=\"claim.pointer\" placeholder=\"family_name\">\n      </div>\n      <div class=\"field\" v-if=\"format === 'vc+sd-jwt'\" :class=\"{ 'error': errors?.verifiable_credentials }\">\n        <label>Expiration</label>\n        <input type=\"text\" v-model=\"claim.expiration\" placeholder=\"3784320000\">\n      </div>\n    </div>\n    <div v-if=\"isObject\">\n      <div class=\"field\" :class=\"{ 'error': errors?.verifiable_credentials }\">\n        <label>Name</label>\n        <input :disabled=\"claim.freeze\" type=\"text\" v-model=\"claim.name\" placeholder=\"family_name\">\n      </div>\n      <div class=\"field\">\n        <h5>Subclaims</h5>\n        <div v-for=\"childClaim in claim.claims\">\n          <VerifiableCredentialClaim :format=\"format\" :credential=\"claim\" :claim=\"childClaim\" :errors=\"errors\"></VerifiableCredentialClaim>\n        </div>\n        <div class=\"field\">\n          <a class=\"ui blue fluid button\" @click=\"addChildClaim(claim)\">Add a claim</a>\n        </div>\n      </div>\n    </div>\n    <div v-if=\"isArray\">\n      <div class=\"field\" :class=\"{ 'error': errors?.verifiable_credentials }\">\n        <label>Name</label>\n        <input :disabled=\"claim.freeze\" type=\"text\" v-model=\"claim.name\" placeholder=\"family_name\">\n      </div>\n      <div class=\"field\">\n        <h5>Items</h5>\n        <div v-for=\"childItem in claim.items\">\n          <VerifiableCredentialClaim :format=\"format\" :credential=\"claim\" :claim=\"childItem\" :errors=\"errors\"></VerifiableCredentialClaim>\n        </div>\n        <div class=\"field\">\n          <a class=\"ui blue fluid button\" @click=\"addChildItem(claim)\">Add a claim</a>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { Claim } from '../models/backend.model'\n\nexport default {\n  name: 'verifiable-credential-claim',\n  props: ['credential', 'claim', 'errors', 'format'],\n  data () {\n    return {\n      attributeClaimTypes: Claim.attributeTypes,\n      objectClaimTypes: Claim.objectTypes,\n      arrayClaimTypes: Claim.arrayTypes\n    }\n  },\n  computed: {\n    isAttribute () {\n      return this.claim.isAttribute\n    },\n    isObject () {\n      return this.claim.isObject\n    },\n    isArray () {\n      return this.claim.isArray\n    }\n  },\n  methods: {\n    deleteVerifiableCredentialClaim (credential, claim) {\n      credential.claims.splice(\n        credential.claims.indexOf(claim),\n        1\n      )\n    },\n    addChildClaim(claim) {\n      claim.claims.push(\n        Claim.build('attribute')\n      )\n    },\n    addChildItem(claim) {\n      claim.items.push(\n        Claim.build('attribute')\n      )\n    }\n  },\n  watch: {\n    'claim.type': function (claimType) {\n      this.claim.assignType(claimType)\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.claim.segment {\n  .field {\n    margin-bottom: 1em!important;\n  }\n  .close.icon {\n    position: absolute;\n    cursor: pointer;\n    top: 1.17em;\n    right: .5em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/main.js",
    "content": "import { createApp } from 'vue'\nimport App from './App.vue'\nimport router from './router'\nimport store from './store'\n\nconst app = createApp(App)\n\napp.use(store)\napp.use(router)\n\napp.mount('#app')\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/backend.model.js",
    "content": "import axios from \"axios\";\nimport { addClientErrorInterceptor } from \"./utils\";\nimport Role from './role.model'\n\nconst defaults = {\n  name: null,\n  roles: [],\n  type: \"Elixir.BorutaIdentity.Accounts.Internal\",\n  errors: null,\n  password_hashing_alg: \"argon2\",\n  password_hashing_opts: {},\n  features: [],\n  metadata_fields: [],\n  federated_servers: [],\n  verifiable_credentials: [],\n  verifiable_presentations: [],\n};\n\nconst assign = {\n  id: function ({ id }) {\n    this.id = id;\n  },\n  name: function ({ name }) {\n    this.name = name;\n  },\n  roles: function ({ roles }) {\n    this.roles = roles.map((scope) => {\n      return { model: new Role(scope) }\n    })\n  },\n  type: function ({ type }) {\n    this.type = type;\n  },\n  is_default: function ({ is_default }) {\n    this.is_default = is_default;\n  },\n  create_default_organization: function ({ create_default_organization }) {\n    this.create_default_organization = create_default_organization;\n  },\n  metadata_fields: function ({ metadata_fields }) {\n    this.metadata_fields = metadata_fields.map((field) => {\n      field.scopes ||= [];\n      field.scopes = field.scopes.map((name) => ({ name }));\n      return field;\n    });\n  },\n  federated_servers: function ({ federated_servers }) {\n    this.federated_servers = federated_servers.map((federatedServer) => {\n      return {\n        ...federatedServer,\n        isDiscovery: !!federatedServer.discovery_path,\n      };\n    });\n  },\n  verifiable_credentials: function ({ verifiable_credentials }) {\n    this.verifiable_credentials = verifiable_credentials.map(credential => {\n      return {\n        ...credential,\n        // NOTE for retrocompatibility issues\n        claims: typeof credential.claims === \"string\" ?\n          credential.claims.split(' ').map(claim => ({ pointer: claim })) :\n          credential.claims.map(claim => {\n            return new Claim(claim)\n          }),\n        scopes: credential.scopes?.map(name => ({ name }))\n      }\n    });\n  },\n  verifiable_presentations: function ({ verifiable_presentations }) {\n    this.verifiable_presentations = verifiable_presentations;\n  },\n  features: function ({ features }) {\n    this.features = features;\n  },\n  password_hashing_alg: function ({ password_hashing_alg }) {\n    this.password_hashing_alg = password_hashing_alg;\n  },\n  password_hashing_opts: function ({ password_hashing_opts }) {\n    this.password_hashing_opts = password_hashing_opts;\n  },\n  ldap_pool_size: function ({ ldap_pool_size }) {\n    this.ldap_pool_size = ldap_pool_size;\n  },\n  ldap_host: function ({ ldap_host }) {\n    this.ldap_host = ldap_host;\n  },\n  ldap_user_rdn_attribute: function ({ ldap_user_rdn_attribute }) {\n    this.ldap_user_rdn_attribute = ldap_user_rdn_attribute;\n  },\n  ldap_base_dn: function ({ ldap_base_dn }) {\n    this.ldap_base_dn = ldap_base_dn;\n  },\n  ldap_ou: function ({ ldap_ou }) {\n    this.ldap_ou = ldap_ou;\n  },\n  ldap_master_dn: function ({ ldap_master_dn }) {\n    this.ldap_master_dn = ldap_master_dn;\n  },\n  ldap_master_password: function ({ ldap_master_password }) {\n    this.ldap_master_password = ldap_master_password;\n  },\n  smtp_from: function ({ smtp_from }) {\n    this.smtp_from = smtp_from;\n  },\n  smtp_relay: function ({ smtp_relay }) {\n    this.smtp_relay = smtp_relay;\n  },\n  smtp_username: function ({ smtp_username }) {\n    this.smtp_username = smtp_username;\n  },\n  smtp_password: function ({ smtp_password }) {\n    this.smtp_password = smtp_password;\n  },\n  smtp_ssl: function ({ smtp_ssl }) {\n    this.smtp_ssl = smtp_ssl;\n  },\n  smtp_tls: function ({ smtp_tls }) {\n    this.smtp_tls = smtp_tls;\n  },\n  smtp_port: function ({ smtp_port }) {\n    this.smtp_port = smtp_port;\n  },\n};\n\nclass Backend {\n  constructor(params = {}) {\n    Object.assign(this, defaults);\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key];\n      assign[key].bind(this)(params);\n    });\n  }\n\n  get isPersisted() {\n    return this.id\n  }\n\n  save() {\n    this.errors = null;\n    // TODO trigger validate\n    let response;\n    const { id, serialized } = this;\n    if (this.isPersisted) {\n      response = this.constructor\n        .api()\n        .patch(`/${id}`, { backend: serialized });\n    } else {\n      response = this.constructor.api().post(\"/\", { backend: serialized });\n    }\n\n    return response\n      .then(({ data }) => {\n        const params = data.data;\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key];\n          assign[key].bind(this)(params);\n        });\n        return this;\n      })\n      .catch((error) => {\n        const { errors } = error.response.data;\n        this.errors = errors;\n        throw errors;\n      });\n  }\n\n  destroy() {\n    return this.constructor\n      .api()\n      .delete(`/${this.id}`)\n      .catch((error) => {\n        const { code, message, errors } = error.response.data;\n        this.errors = errors;\n        throw { code, message, errors };\n      });\n  }\n\n  get serialized() {\n    const {\n      id,\n      name,\n      type,\n      roles,\n      is_default,\n      create_default_organization,\n      password_hashing_alg,\n      password_hashing_opts,\n      metadata_fields,\n      federated_servers,\n      verifiable_credentials,\n      verifiable_presentations,\n      ldap_pool_size,\n      ldap_host,\n      ldap_user_rdn_attribute,\n      ldap_base_dn,\n      ldap_ou,\n      ldap_master_dn,\n      ldap_master_password,\n      smtp_from,\n      smtp_relay,\n      smtp_username,\n      smtp_password,\n      smtp_ssl,\n      smtp_tls,\n      smtp_port,\n    } = this;\n    const formattedPasswordHashingOpts = {};\n    Object.keys(password_hashing_opts).forEach((key) => {\n      const value = password_hashing_opts[key];\n      if (value !== \"\") {\n        formattedPasswordHashingOpts[key] = value;\n      }\n    });\n\n    function serializeClaim (claim) {\n      const { type, name, label, pointer, claims, items } = claim\n      return {\n        type,\n        name,\n        label,\n        pointer,\n        claims: claims.map(serializeClaim),\n        items: items.map(serializeClaim)\n      }\n    }\n\n    return {\n      id,\n      name,\n      roles: roles.map(({ model }) => model.serialized),\n      type,\n      is_default,\n      create_default_organization,\n      password_hashing_alg,\n      password_hashing_opts: formattedPasswordHashingOpts,\n      metadata_fields: metadata_fields.map(\n        ({ attribute_name, user_editable, scopes }) => ({\n          attribute_name,\n          user_editable,\n          scopes: scopes.map(({ name }) => name),\n        })\n      ),\n      federated_servers: federated_servers.map((federatedServer) => {\n        const federated_server = Object.assign({}, federatedServer)\n        if (!federated_server.isDiscovery) {\n          delete federated_server.discovery_path;\n        }\n        delete federated_server.clientSecretVisible\n        delete federated_server.isDiscovery;\n        return federated_server;\n      }),\n      verifiable_credentials: verifiable_credentials.map(verifiableCredential => {\n        verifiableCredential.claims = verifiableCredential.claims.map(serializeClaim)\n        verifiableCredential.scopes = verifiableCredential.scopes?.map(({ name }) => name)\n        return verifiableCredential\n      }),\n      verifiable_presentations,\n      ldap_pool_size,\n      ldap_host,\n      ldap_user_rdn_attribute,\n      ldap_base_dn,\n      ldap_ou,\n      ldap_master_dn,\n      ldap_master_password,\n      smtp_from,\n      smtp_relay,\n      smtp_username,\n      smtp_password,\n      smtp_ssl,\n      smtp_tls,\n      smtp_port,\n    };\n  }\n\n  resetPasswordAlgorithmOpts() {\n    this.password_hashing_opts = {};\n  }\n\n  static get passwordHashingAlgorithms() {\n    return [\n      { name: \"argon2\", label: \"Argon2\" },\n      { name: \"bcrypt\", label: \"Bcrypt\" },\n      { name: \"pbkdf2\", label: \"Pbkdf2\" },\n    ];\n  }\n\n  static get passwordHashingOpts() {\n    return {\n      argon2: [\n        {\n          name: \"salt_len\",\n          type: \"number\",\n          label: \"Length of the random salt (in bytes)\",\n          default: 16,\n        },\n        { name: \"t_cost\", type: \"number\", label: \"Time cost\", default: 8 },\n        { name: \"m_cost\", type: \"number\", label: \"Memory usage\", default: 16 },\n        {\n          name: \"parallelism\",\n          type: \"number\",\n          label: \"Number of parralel threads\",\n          default: 2,\n        },\n        {\n          name: \"format\",\n          type: \"text\",\n          label: \"Output format (encoded, raw_hash, or report)\",\n          default: \"encoded\",\n        },\n        {\n          name: \"hashlen\",\n          type: \"number\",\n          label: \"Length of the hash (in bytes)\",\n          default: 32,\n        },\n        {\n          name: \"argon2_type\",\n          type: \"number\",\n          label: \"Argon2 type (0 argon2d, 1 argon2i, 2 argon2id)\",\n          default: 2,\n        },\n      ],\n      bcrypt: [\n        {\n          name: \"log_rounds\",\n          type: \"number\",\n          label: \"The computational cost as number of log rounds\",\n          default: 12,\n        },\n        {\n          name: \"legacy\",\n          type: \"checkbox\",\n          label: 'Generate salts with the old \"$2a$\" prefix',\n          default: false,\n        },\n      ],\n      pbkdf2: [\n        {\n          name: \"salt_len\",\n          type: \"number\",\n          label: \"The length of the random salt\",\n          default: 16,\n        },\n        {\n          name: \"format\",\n          type: \"text\",\n          label: \"The output format of the hash (modular, django, or hex)\",\n          default: \"modular\",\n        },\n        {\n          name: \"digest\",\n          type: \"text\",\n          label: \"The sha algorithm that pbkdf2 will use\",\n          default: \"sha512\",\n        },\n        {\n          name: \"length\",\n          type: \"number\",\n          label: \"The length of the hash (in bytes)\",\n          default: 64,\n        },\n      ],\n    };\n  }\n\n  static api() {\n    const accessToken = localStorage.getItem(\"access_token\");\n\n    const instance = axios.create({\n      baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/backends`,\n      headers: { Authorization: `Bearer ${accessToken}` },\n    });\n\n    return addClientErrorInterceptor(instance);\n  }\n\n  static all() {\n    return this.api()\n      .get(\"/\")\n      .then(({ data }) => {\n        return data.data.map(\n          (identityProvider) => new Backend(identityProvider)\n        );\n      });\n  }\n\n  static get(id) {\n    return this.api()\n      .get(`/${id}`)\n      .then(({ data }) => {\n        return new Backend(data.data);\n      });\n  }\n}\n\nexport class Claim {\n  constructor (attrs = {}) {\n    const claim = Object.assign(Claim.baseClaim(attrs.type), attrs)\n    Object.assign(this, claim)\n    mapClaims(this)\n    mapItems(this)\n\n    function mapClaims(claim) {\n      if (!claim.claims.length) return claim\n\n      const result = claim.claims.map(claim => new Claim(claim))\n      claim.claims = result.map(mapClaims)\n\n      return claim\n    }\n\n    function mapItems(claim) {\n      if (!claim.items.length) return claim\n\n      const result = claim.items.map(claim => new Claim(claim))\n      claim.items = result.map(mapClaims)\n\n      return claim\n    }\n  }\n\n  static build (claimType) {\n    return Object.assign(new Claim(), Claim.baseClaim(claimType))\n  }\n\n  assignType (claimType) {\n    Object.assign(this, Claim.baseClaim(claimType))\n  }\n\n  static get attributeTypes () {\n    return ['attribute']\n  }\n\n  static get objectTypes () {\n    return ['object']\n  }\n\n  static get arrayTypes () {\n    return ['array']\n  }\n\n  get isAttribute () {\n    return Claim.attributeTypes.includes(this.type)\n  }\n\n  get isObject () {\n    return Claim.objectTypes.includes(this.type)\n  }\n\n  get isArray () {\n    return Claim.arrayTypes.includes(this.type)\n  }\n\n  static baseClaim(claimType) {\n    switch (claimType) {\n      case 'attribute':\n        return {\n          type: 'attribute',\n          name: '',\n          label: '',\n          freeze: false,\n          claims: [],\n          items: []\n        }\n      case 'object':\n        return {\n          type: 'object',\n          name: '',\n          freeze: false,\n          claims: [],\n          items: []\n        }\n      case 'array':\n        return {\n          type: 'array',\n          name: '',\n          freeze: false,\n          claims: [],\n          items: []\n        }\n      default:\n        return { claims: [], items: [] }\n    }\n  }\n}\n\nexport default Backend;\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/business-log-stats.model.js",
    "content": "import axios from 'axios'\nimport moment from 'moment'\nimport { addClientErrorInterceptor } from './utils'\n\nconst defaults = {\n  errors: null\n}\n\nconst assign = {\n  time_scale_unit: function ({ time_scale_unit }) { this.time_scale_unit = time_scale_unit },\n  overflow: function ({ overflow }) { this.overflow = overflow },\n  log_lines: function ({ log_lines }) { this.log_lines = log_lines },\n  log_count: function ({ log_count }) { this.log_count = log_count },\n  counts: function ({ counts }) { this.counts = counts },\n  business_event_counts: function ({ business_event_counts }) { this.business_event_counts = business_event_counts },\n  domains: function ({ domains }) { this.domains = domains },\n  actions: function ({ actions }) { this.actions = actions }\n\n}\n\nclass LogStats {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n}\n\nLogStats.api = function () {\n  const accessToken = localStorage.getItem('access_token')\n\n  const instance = axios.create({\n    baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/logs`,\n    headers: { 'Authorization': `Bearer ${accessToken}` }\n  })\n\n  return addClientErrorInterceptor(instance)\n}\n\nLogStats.all = function ({ startAt, endAt, application, domain, action }) {\n  const params = new URLSearchParams()\n  params.append('start_at', moment.utc(startAt).toISOString())\n  params.append('end_at', moment.utc(endAt).toISOString())\n  params.append('application', application)\n  domain && params.append('query[domain]', domain)\n  action && params.append('query[action]', action)\n  params.append('type', 'business')\n\n  return this.api().get(`?${params.toString()}`).then(({ data }) => {\n    return new LogStats(data)\n  }).catch(error => {\n    throw error.response.data\n  })\n}\n\nexport default LogStats\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/client.model.js",
    "content": "import axios from 'axios'\nimport Scope from './scope.model'\nimport IdentityProvider from './identity-provider.model'\nimport { addClientErrorInterceptor } from './utils'\n\nconst allGrantTypes = [\n  'client_credentials',\n  'agent_credentials',\n  'password',\n  'authorization_code',\n  'agent_code',\n  'refresh_token',\n  'implicit',\n  'preauthorized_code',\n  'id_token',\n  'vp_token',\n  'revoke',\n  'introspect'\n]\n\nconst keyPairTypes = {\n  'ec': { curve: ['P-256', 'P-384', 'P-512'] },\n  'rsa': { modulus_size: '2048', exponent_size: '65537' }\n}\n\nconst signaturesAdapters = [\n  'Elixir.Boruta.Internal.Signatures',\n  'Elixir.Boruta.Universal.Signatures'\n]\n\nconst defaults = {\n  errors: null,\n  key_pair_id: null,\n  key_pair_type: { type: 'rsa', modulus_size: '2048', exponent_size: '65537' },\n  signatures_adapter: 'Elixir.Boruta.Internal.Signatures',\n  authorize_scopes: false,\n  authorized_scopes: [],\n  redirect_uris: [],\n  id_token_signature_alg: 'RS512',\n  token_endpoint_jwt_auth_alg: 'HS256',\n  token_endpoint_auth_methods: [\"client_secret_basic\", \"client_secret_post\"],\n  identity_provider: { model: new IdentityProvider() },\n  grantTypes: allGrantTypes.map((label) => {\n    return {\n      value: true,\n      label\n    }\n  })\n}\n\nconst assign = {\n  id: function ({ id }) { this.id = id },\n  public_client_id: function ({ public_client_id }) { this.public_client_id = public_client_id },\n  check_public_client_id: function ({ check_public_client_id }) { this.check_public_client_id = check_public_client_id },\n  name: function ({ name }) { this.name = name },\n  confidential: function ({ confidential }) { this.confidential = confidential },\n  pkce: function ({ pkce }) { this.pkce = pkce },\n  public_key: function ({ public_key }) { this.public_key = public_key },\n  key_pair_type: function ({ key_pair_type }) { this.key_pair_type = key_pair_type },\n  signatures_adapter: function ({ signatures_adapter }) { this.signatures_adapter = signatures_adapter },\n  did: function ({ did }) { this.did = did },\n  access_token_ttl: function ({ access_token_ttl }) { this.access_token_ttl = access_token_ttl },\n  authorization_code_ttl: function ({ authorization_code_ttl }) { this.authorization_code_ttl = authorization_code_ttl },\n  refresh_token_ttl: function ({ refresh_token_ttl }) { this.refresh_token_ttl = refresh_token_ttl },\n  id_token_ttl: function ({ id_token_ttl }) { this.id_token_ttl = id_token_ttl },\n  authorization_request_ttl: function ({ authorization_request_ttl }) { this.authorization_request_ttl = authorization_request_ttl },\n  secret: function ({ secret }) { this.secret = secret },\n  redirect_uris: function ({ redirect_uris }) {\n    this.redirect_uris = redirect_uris.map((uri) => ({ uri }))\n  },\n  public_refresh_token: function ({ public_refresh_token }) { this.public_refresh_token = public_refresh_token },\n  public_revoke: function ({ public_revoke }) { this.public_revoke = public_revoke },\n  identity_provider: function ({ identity_provider }) {\n    this.identity_provider = { model: new IdentityProvider(identity_provider) }\n  },\n  authorize_scope: function ({ authorize_scope }) { this.authorize_scope = authorize_scope },\n  enforce_dpop: function ({ enforce_dpop }) { this.enforce_dpop = enforce_dpop },\n  enforce_tx_code: function ({ enforce_tx_code }) { this.enforce_tx_code = enforce_tx_code },\n  authorized_scopes: function ({ authorized_scopes }) {\n    this.authorized_scopes = authorized_scopes.map((scope) => {\n      return { model: new Scope(scope) }\n    })\n  },\n  supported_grant_types: function ({ supported_grant_types }) {\n    this.supported_grant_types = supported_grant_types\n    this.grantTypes = allGrantTypes.map((label) => {\n      return {\n        value: this.supported_grant_types.includes(label),\n        label\n      }\n    })\n  },\n  token_endpoint_jwt_auth_alg: function ({ token_endpoint_jwt_auth_alg }) { this.token_endpoint_jwt_auth_alg = token_endpoint_jwt_auth_alg },\n  token_endpoint_auth_methods: function ({ token_endpoint_auth_methods }) { this.token_endpoint_auth_methods = token_endpoint_auth_methods },\n  jwt_public_key: function ({ jwt_public_key }) { this.jwt_public_key = jwt_public_key },\n  id_token_signature_alg: function ({ id_token_signature_alg }) { this.id_token_signature_alg = id_token_signature_alg },\n  userinfo_signed_response_alg: function ({ userinfo_signed_response_alg }) { this.userinfo_signed_response_alg = userinfo_signed_response_alg },\n  response_mode: function ({ response_mode }) { this.response_mode = response_mode },\n}\n\nclass Client {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n\n  // TODO factorize with User#validate\n  validate () {\n    return new Promise((resolve, reject) => {\n      this.authorized_scopes.forEach(({ model: scope }) => {\n        if (!scope.persisted) {\n          const errors = { authorized_scopes: [ 'cannot be empty' ] }\n          this.errors = errors\n          return reject(errors)\n        }\n        if (this.authorized_scopes.filter(({ model: e }) => e.id === scope.id).length > 1) {\n          const errors = { authorized_scopes: [ 'must be unique' ] }\n          this.errors = errors\n          return reject(errors)\n        }\n      })\n      resolve()\n    })\n  }\n\n  async save () {\n    this.errors = null\n\n    await this.validate()\n\n    // TODO trigger validate\n    let response\n    const { id, isPersisted, serialized } = this\n    if (isPersisted) {\n      response = this.constructor.api().patch(`/${id}`, { client: serialized })\n    } else {\n      response = this.constructor.api().post('/', { client: serialized })\n    }\n\n    return response\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  async regenerateDid () {\n    const { id } = this\n    this.constructor.api().post(`/${id}/regenerate_did`)\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n\n        this.key_pair_id = null\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  async regenerateKeyPair () {\n    const { id } = this\n    this.constructor.api().post(`/${id}/regenerate_key_pair`)\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n\n        this.key_pair_id = null\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n\n  destroy () {\n    return this.constructor.api().delete(`/${this.id}`)\n      .catch((error) => {\n        const { code, message, errors } = error.response.data\n        this.errors = errors\n        throw { code, message, errors }\n      })\n  }\n\n  get serialized () {\n    const {\n      access_token_ttl,\n      authorization_code_ttl,\n      authorization_request_ttl,\n      authorize_scope,\n      enforce_dpop,\n      enforce_tx_code,\n      authorized_scopes,\n      confidential,\n      grantTypes,\n      id,\n      public_client_id,\n      check_public_client_id,\n      id_token_ttl,\n      name,\n      pkce,\n      public_refresh_token,\n      public_revoke,\n      redirect_uris,\n      refresh_token_ttl,\n      identity_provider,\n      secret,\n      id_token_signature_alg,\n      userinfo_signed_response_alg,\n      token_endpoint_jwt_auth_alg,\n      token_endpoint_auth_methods,\n      jwt_public_key,\n      key_pair_id,\n      key_pair_type,\n      signatures_adapter,\n      response_mode\n    } = this\n\n    return {\n      access_token_ttl,\n      authorization_code_ttl,\n      authorization_request_ttl,\n      authorize_scope,\n      enforce_dpop,\n      enforce_tx_code,\n      authorized_scopes: authorized_scopes.map(({ model }) => model.serialized),\n      confidential,\n      id,\n      public_client_id,\n      check_public_client_id,\n      id_token_ttl,\n      name,\n      pkce,\n      public_refresh_token,\n      public_revoke,\n      redirect_uris: redirect_uris.map(({ uri }) => uri),\n      refresh_token_ttl,\n      identity_provider: identity_provider.model,\n      secret,\n      supported_grant_types: grantTypes\n        .filter(({ value }) => value)\n        .map(({ label }) => label),\n      id_token_signature_alg,\n      userinfo_signed_response_alg,\n      token_endpoint_jwt_auth_alg,\n      token_endpoint_auth_methods,\n      jwt_public_key,\n      key_pair_id,\n      key_pair_type,\n      signatures_adapter,\n      response_mode\n    }\n  }\n}\n\nClient.keyPairTypes = keyPairTypes\n\nClient.signaturesAdapters = signaturesAdapters\n\nClient.api = function () {\n  const accessToken = localStorage.getItem('access_token')\n\n  const instance = axios.create({\n    baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/clients`,\n    headers: { 'Authorization': `Bearer ${accessToken}` }\n  })\n\n  return addClientErrorInterceptor(instance)\n}\n\nClient.all = function () {\n  return this.api().get('/').then(({ data }) => {\n    return data.data\n      .map((client) => new Client(client))\n      .map((client) => Object.assign(client, { isPersisted: true }))\n  })\n}\n\nClient.get = function (id) {\n  return this.api().get(`/${id}`).then(({ data }) => {\n    const client = new Client(data.data)\n    client.isPersisted = true\n    return client\n  })\n}\n\nClient.idTokenSignatureAlgorithms = [\n  \"EdDSA\",\n  \"ES256\",\n  \"ES384\",\n  \"ES512\",\n  \"HS256\",\n  \"HS384\",\n  \"HS512\",\n  \"RS256\",\n  \"RS384\",\n  \"RS512\"\n]\n\nClient.clientJwtAuthenticationSignatureAlgorithms = [\n  \"ES256\",\n  \"ES384\",\n  \"ES512\",\n  \"HS256\",\n  \"HS384\",\n  \"HS512\",\n  \"RS256\",\n  \"RS384\",\n  \"RS512\"\n]\n\nClient.UserinfoResponseSignatureAlgorithms = [\n  null,\n  \"EdDSA\",\n  \"ES256\",\n  \"ES384\",\n  \"ES512\",\n  \"HS256\",\n  \"HS384\",\n  \"HS512\",\n  \"RS256\",\n  \"RS384\",\n  \"RS512\"\n]\n\nClient.tokenEndpointAuthMethods = [\n  \"client_secret_basic\",\n  \"client_secret_post\",\n  \"client_secret_jwt\",\n  \"private_key_jwt\"\n]\n\nexport default Client\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/email-template.model.js",
    "content": "import axios from 'axios'\nimport { addClientErrorInterceptor } from './utils'\n\nconst defaults = {\n  id: null,\n  txt_content: null,\n  html_content: null,\n  type: null,\n  errors: null\n}\n\nconst assign = {\n  id: function ({ id }) { this.id = id },\n  type: function ({ type }) { this.type = type },\n  txt_content: function ({ txt_content }) { this.txt_content = txt_content },\n  html_content: function ({ html_content }) { this.html_content = html_content },\n  backend_id: function ({ backend_id }) { this.backend_id = backend_id },\n}\n\nclass EmailTemplate {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n\n  save () {\n    this.errors = null\n    // TODO trigger validate\n    const { type, backend_id: backendId, serialized } = this\n\n    return this.constructor.api().patch(`/${backendId}/email-templates/${type}`, { template: serialized })\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  destroy () {\n    const { type, backend_id: backendId } = this\n\n    return this.constructor.api().delete(`/${backendId}/email-templates/${type}`)\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  get serialized () {\n    const { txt_content, html_content } = this\n\n    return {\n      txt_content,\n      html_content\n    }\n  }\n\n  static api () {\n    const accessToken = localStorage.getItem('access_token')\n\n    const instance = axios.create({\n      baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/backends`,\n      headers: { 'Authorization': `Bearer ${accessToken}` }\n    })\n\n    return addClientErrorInterceptor(instance)\n  }\n\n  static get (backendId, type) {\n    return this.api().get(`/${backendId}/email-templates/${type}`).then(({ data }) => {\n      return new EmailTemplate(data.data)\n    })\n  }\n}\n\nexport default EmailTemplate\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/error-template.model.js",
    "content": "import axios from 'axios'\nimport { addClientErrorInterceptor } from './utils'\n\nconst templates = [\n  { type: 400, name: \"bad-request\", label: \"Bad request\" },\n  { type: 403, name: \"forbidden\", label: \"Forbidden\" },\n  { type: 404, name: \"not-found\", label: \"Not found\" },\n  { type: 500, name: \"internal-server-error\", label: \"Internal server error\" }\n]\n\nconst defaults = {\n  id: null,\n  name: null,\n  content: null,\n  type: null,\n  errors: null\n}\n\nconst assign = {\n  id: function ({ id }) { this.id = id },\n  name: function ({ name }) { this.name = name },\n  type: function ({ type }) { this.type = type },\n  content: function ({ content }) { this.content = content },\n  label: function ({ label }) { this.label = label }\n}\n\nclass ErrorTemplate {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n\n  save () {\n    this.errors = null\n    // TODO trigger validate\n    const { type, serialized } = this\n\n    return this.constructor.api().patch(`/${type}`, { template: serialized })\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  destroy () {\n    const { type } = this\n\n    return this.constructor.api().delete(`/${type}`)\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  get serialized () {\n    const { content } = this\n\n    return {\n      content\n    }\n  }\n\n  static api () {\n    const accessToken = localStorage.getItem('access_token')\n\n    const instance = axios.create({\n      baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/configuration/error-templates`,\n      headers: { 'Authorization': `Bearer ${accessToken}` }\n    })\n\n    return addClientErrorInterceptor(instance)\n  }\n\n  static get (type) {\n    return this.api().get(`/${type}`).then(({ data }) => {\n      return new ErrorTemplate(data.data)\n    })\n  }\n\n  static all () {\n    return templates.map(data => new ErrorTemplate(data))\n  }\n}\n\nexport default ErrorTemplate\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/identity-provider.model.js",
    "content": "import axios from 'axios'\nimport { addClientErrorInterceptor } from './utils'\nimport Backend from './backend.model'\n\nconst defaults = {\n  name: null,\n  type: 'internal',\n  errors: null,\n  backend: new Backend()\n}\n\nconst assign = {\n  id: function ({ id }) { this.id = id },\n  name: function ({ name }) { this.name = name },\n  type: function ({ type }) { this.type = type },\n  backend: function ({ backend }) { this.backend = new Backend(backend) },\n  backend_id: function ({ backend_id }) { this.backend_id = backend_id },\n  check_password: function ({ check_password }) { this.check_password = check_password },\n  choose_session: function ({ choose_session }) { this.choose_session = choose_session },\n  totpable: function ({ totpable }) { this.totpable = totpable },\n  enforce_totp: function ({ enforce_totp }) { this.enforce_totp = enforce_totp },\n  webauthnable: function ({ webauthnable }) { this.webauthnable = webauthnable },\n  enforce_webauthn: function ({ enforce_webauthn }) { this.enforce_webauthn = enforce_webauthn },\n  registrable: function ({ registrable }) { this.registrable = registrable },\n  user_editable: function ({ user_editable }) { this.user_editable = user_editable },\n  consentable: function ({ consentable }) { this.consentable = consentable },\n  confirmable: function ({ confirmable }) { this.confirmable = confirmable }\n}\n\nclass IdentityProvider {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n\n  get isPersisted () {\n    return this.id\n  }\n\n  save () {\n    this.errors = null\n    // TODO trigger validate\n    let response\n    const { id, serialized } = this\n    if (this.isPersisted) {\n      response = this.constructor.api().patch(`/${id}`, { identity_provider: serialized })\n    } else {\n      response = this.constructor.api().post('/', { identity_provider: serialized })\n    }\n\n    return response\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  destroy () {\n    return this.constructor.api().delete(`/${this.id}`)\n      .catch((error) => {\n        const { code, message, errors } = error.response.data\n        this.errors = errors\n        throw { code, message, errors }\n      })\n  }\n\n  get serialized () {\n    const {\n      id,\n      name,\n      backend_id,\n      check_password,\n      choose_session,\n      totpable,\n      enforce_totp,\n      webauthnable,\n      enforce_webauthn,\n      registrable,\n      user_editable,\n      consentable,\n      confirmable\n    } = this\n\n    return {\n      id,\n      name,\n      backend_id,\n      check_password,\n      choose_session,\n      user_editable,\n      totpable,\n      enforce_totp,\n      webauthnable,\n      enforce_webauthn,\n      registrable,\n      consentable,\n      confirmable\n    }\n  }\n\n  static api () {\n    const accessToken = localStorage.getItem('access_token')\n\n    const instance = axios.create({\n      baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/identity-providers`,\n      headers: { 'Authorization': `Bearer ${accessToken}` }\n    })\n\n    return addClientErrorInterceptor(instance)\n  }\n\n  static all () {\n    return this.api().get('/').then(({ data }) => {\n      return data.data.map((identityProvider) => new IdentityProvider(identityProvider))\n    })\n  }\n\n  static get (id) {\n    return this.api().get(`/${id}`).then(({ data }) => {\n      return new IdentityProvider(data.data)\n    })\n  }\n}\n\nexport default IdentityProvider\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/key-pair.model.js",
    "content": "import axios from 'axios'\nimport { addClientErrorInterceptor } from './utils'\n\nconst defaults = {\n}\n\nconst assign = {\n  id: function ({ id }) { this.id = id },\n  public_key: function ({ public_key }) { this.public_key = public_key },\n  is_default: function ({ is_default }) { this.is_default = is_default }\n}\nclass KeyPair {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n\n  get persisted () {\n    return !!this.id\n  }\n\n  rotate () {\n    return this.constructor.api().post(`/${this.id}/rotate`)\n      .then(({ data }) => Object.assign(this, data.data))\n      .catch((error) => {\n        const { code, message, errors } = error.response.data\n        this.errors = errors\n        throw { code, message, errors }\n      })\n  }\n\n  save () {\n    const { id, serialized } = this\n    let response\n\n    this.errors = null\n\n    if (id) {\n      response = this.constructor.api().patch(`/${id}`, { key_pair: serialized })\n        .then(({ data }) => Object.assign(this, data.data))\n    } else {\n      response = this.constructor.api().post('/', { key_pair: serialized })\n        .then(({ data }) => Object.assign(this, data.data))\n    }\n    return response.catch((error) => {\n      const { code, message, errors } = error.response.data\n      this.errors = errors\n      throw { code, message, errors }\n    })\n  }\n\n  destroy () {\n    return this.constructor.api().delete(`/${this.id}`)\n      .catch((error) => {\n        const { code, message, errors } = error.response.data\n        this.errors = errors\n        throw { code, message, errors }\n      })\n  }\n\n  get serialized () {\n    const { id, is_default } = this\n\n    return {\n      id,\n      is_default\n    }\n  }\n}\n\nKeyPair.api = function () {\n  const accessToken = localStorage.getItem('access_token')\n\n  const instance = axios.create({\n    baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/key-pairs`,\n    headers: { 'Authorization': `Bearer ${accessToken}` }\n  })\n\n  return addClientErrorInterceptor(instance)\n}\n\nKeyPair.all = function () {\n  return this.api().get('/').then(({ data }) => {\n    return data.data.map((keyPair) => new KeyPair(keyPair))\n  })\n}\n\nKeyPair.get = function (id) {\n  return this.api().get(`/${id}`).then(({ data }) => {\n    return new KeyPair(data.data)\n  })\n}\n\nexport default KeyPair\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/organization.model.js",
    "content": "import axios from 'axios'\nimport Scope from './scope.model'\nimport Role from './role.model'\nimport Backend from './backend.model'\nimport { addClientErrorInterceptor } from './utils'\n\nconst defaults = {\n  errors: null\n}\n\nconst assign = {\n  id: function ({ id }) { this.id = id },\n  name: function ({ name }) { this.name = name },\n  label: function ({ label }) { this.label = label }\n}\n\nclass Organization {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n\n  get isPersisted() {\n    return this.id\n  }\n\n  async save () {\n    this.errors = null\n\n    const { id, serialized } = this\n    let response\n    if (this.isPersisted) {\n      response = this.constructor.api().patch(`/${id}`, { organization: serialized })\n    } else {\n      response = this.constructor.api().post('/', { organization: serialized })\n    }\n    return response\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  destroy () {\n    return this.constructor.api().delete(`/${this.id}`)\n  }\n\n  get serialized () {\n    const { id, name, label } = this\n\n    return {\n      id,\n      name,\n      label\n    }\n  }\n}\n\nOrganization.api = function () {\n  const accessToken = localStorage.getItem('access_token')\n\n  const instance = axios.create({\n    baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/organizations`,\n    headers: { 'Authorization': `Bearer ${accessToken}` }\n  })\n\n  return addClientErrorInterceptor(instance)\n}\n\nOrganization.all = function ({ query, pageNumber } = {}) {\n  const searchParams = new URLSearchParams()\n  pageNumber && searchParams.append('page', pageNumber)\n  query && searchParams.append('q', query)\n\n  return this.api().get(`/?${searchParams.toString()}`).then(({\n    data: {\n      data,\n      total_entries: totalEntries,\n      page_number: currentPage,\n      total_pages: totalPages,\n    }\n  }) => {\n    return {\n      data: data.map((user) => new Organization(user)),\n      currentPage,\n      totalPages,\n      totalEntries\n    }\n\n  })\n}\n\nOrganization.get = function (id) {\n  return this.api().get(`/${id}`).then(({ data }) => {\n    return new Organization(data.data)\n  })\n}\n\nexport default Organization\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/request-log-stats.model.js",
    "content": "import axios from 'axios'\nimport moment from 'moment'\nimport { addClientErrorInterceptor } from './utils'\n\nconst defaults = {\n  errors: null\n}\n\nconst assign = {\n  time_scale_unit: function ({ time_scale_unit }) { this.time_scale_unit = time_scale_unit },\n  overflow: function ({ overflow }) { this.overflow = overflow },\n  log_lines: function ({ log_lines }) { this.log_lines = log_lines },\n  log_count: function ({ log_count }) { this.log_count = log_count },\n  status_codes: function ({ status_codes }) { this.status_codes = status_codes },\n  request_counts: function ({ request_counts }) { this.request_counts = request_counts },\n  request_times: function ({ request_times }) { this.request_times = request_times },\n  labels: function ({ labels }) { this.labels = labels },\n}\n\nclass LogStats {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n}\n\nLogStats.api = function () {\n  const accessToken = localStorage.getItem('access_token')\n\n  const instance = axios.create({\n    baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/logs`,\n    headers: { 'Authorization': `Bearer ${accessToken}` }\n  })\n\n  return addClientErrorInterceptor(instance)\n}\n\nLogStats.all = function ({ startAt, endAt, application, label }) {\n  const params = new URLSearchParams()\n  params.append('start_at', moment.utc(startAt).toISOString())\n  params.append('end_at', moment.utc(endAt).toISOString())\n  params.append('application', application)\n  label && params.append('query[label]', label)\n  params.append('type', 'request')\n\n  return this.api().get(`?${params.toString()}`).then(({ data }) => {\n    return new LogStats(data)\n  }).catch(error => {\n    throw error.response.data\n  })\n}\n\nexport default LogStats\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/role.model.js",
    "content": "import axios from 'axios'\nimport Scope from './scope.model'\nimport { addClientErrorInterceptor } from './utils'\n\nconst defaults = {\n  scopes: []\n}\n\nconst assign = {\n  id: function ({ id }) { this.id = id },\n  name: function ({ name }) { this.name = name },\n  scopes: function ({ scopes }) { this.scopes = scopes.map(scope => ({ model: new Scope(scope) })) }\n}\n\nclass Role {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n\n  save () {\n    this.errors = null\n    // TODO trigger validate\n    let response\n    const { id, serialized } = this\n    if (id) {\n      response = this.constructor.api().patch(`/${id}`, { role: serialized })\n    } else {\n      response = this.constructor.api().post('/', { role: serialized })\n    }\n\n    return response\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  destroy () {\n    return this.constructor.api().delete(`/${this.id}`)\n      .catch((error) => {\n        const { code, message, errors } = error.response.data\n        this.errors = errors\n        throw { code, message, errors }\n      })\n  }\n\n  get serialized () {\n    const {\n      id,\n      name,\n      scopes\n    } = this\n\n    console.log(scopes)\n    return {\n      id,\n      name,\n      scopes: scopes.map(({ model: { id, name } }) => ({ id, name }))\n    }\n  }\n}\n\nRole.api = function () {\n  const accessToken = localStorage.getItem('access_token')\n\n  const instance = axios.create({\n    baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/roles`,\n    headers: { 'Authorization': `Bearer ${accessToken}` }\n  })\n\n  return addClientErrorInterceptor(instance)\n}\n\nRole.all = function () {\n  return this.api().get('/').then(({ data }) => {\n    const result = data.data\n\n    return result.map((role) => new Role(role))\n  })\n}\n\nRole.get = function (id) {\n  return this.api().get(`/${id}`).then(({ data }) => {\n    return new Role(data.data)\n  })\n}\n\nexport default Role\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/scope.model.js",
    "content": "import axios from 'axios'\nimport { addClientErrorInterceptor } from './utils'\n\nconst defaults = {\n  name: '',\n  edit: false,\n  errors: null\n}\n\nconst assign = {\n  id: function ({ id }) { this.id = id },\n  name: function ({ name }) { this.name = name },\n  label: function ({ label }) { this.label = label },\n  edit: function ({ edit }) { this.edit = edit },\n  public: function ({ public: e }) { this.public = e }\n}\nclass Scope {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n\n  get persisted () {\n    return !!this.id\n  }\n\n  reset () {\n    return this.constructor.api().get(`/${this.id}`).then(({ data }) => {\n      Object.assign(this, defaults)\n      return Object.assign(this, data.data)\n    })\n  }\n\n  save () {\n    const { id, serialized } = this\n    let response\n\n    this.errors = null\n\n    if (id) {\n      response = this.constructor.api().patch(`/${id}`, { scope: serialized })\n        .then(({ data }) => Object.assign(this, data.data))\n    } else {\n      response = this.constructor.api().post('/', { scope: serialized })\n        .then(({ data }) => Object.assign(this, data.data))\n    }\n    return response.catch((error) => {\n      const { code, message, errors } = error.response.data\n      this.errors = errors\n      throw { code, message, errors }\n    })\n  }\n\n  destroy () {\n    return this.constructor.api().delete(`/${this.id}`)\n      .catch((error) => {\n        const { code, message, errors } = error.response.data\n        this.errors = errors\n        throw { code, message, errors }\n      })\n  }\n\n  get serialized () {\n    const { id, label, name, public: p } = this\n\n    return {\n      id,\n      label,\n      name,\n      public: p\n    }\n  }\n}\n\nScope.api = function () {\n  const accessToken = localStorage.getItem('access_token')\n\n  const instance = axios.create({\n    baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/scopes`,\n    headers: { 'Authorization': `Bearer ${accessToken}` }\n  })\n\n  return addClientErrorInterceptor(instance)\n}\n\nScope.all = function () {\n  return this.api().get('/').then(({ data }) => {\n    return data.data.map((scope) => new Scope(scope))\n  })\n}\n\nScope.get = function (id) {\n  return this.api().get(`/${id}`).then(({ data }) => {\n    return new Scope(data.data)\n  })\n}\n\nexport default Scope\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/template.model.js",
    "content": "import axios from 'axios'\nimport { addClientErrorInterceptor } from './utils'\n\nconst defaults = {\n  id: null,\n  content: null,\n  type: null,\n  errors: null\n}\n\nconst assign = {\n  id: function ({ id }) { this.id = id },\n  type: function ({ type }) { this.type = type },\n  content: function ({ content }) { this.content = content },\n  identity_provider_id: function ({ identity_provider_id }) { this.identity_provider_id = identity_provider_id },\n}\n\nclass Template {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n\n  save () {\n    this.errors = null\n    // TODO trigger validate\n    const { type, identity_provider_id: identityProviderId, serialized } = this\n\n    return this.constructor.api().patch(`/${identityProviderId}/templates/${type}`, { template: serialized })\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  destroy () {\n    const { type, identity_provider_id: identityProviderId } = this\n\n    return this.constructor.api().delete(`/${identityProviderId}/templates/${type}`)\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  get serialized () {\n    const { id, content } = this\n\n    return {\n      id,\n      content\n    }\n  }\n\n  static api () {\n    const accessToken = localStorage.getItem('access_token')\n\n    const instance = axios.create({\n      baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/identity-providers`,\n      headers: { 'Authorization': `Bearer ${accessToken}` }\n    })\n\n    return addClientErrorInterceptor(instance)\n  }\n\n  static get (identityProviderId, type) {\n    return this.api().get(`/${identityProviderId}/templates/${type}`).then(({ data }) => {\n      return new Template(data.data)\n    })\n  }\n}\n\nexport default Template\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/upstream.model.js",
    "content": "import axios from 'axios'\nimport Scope from './scope.model'\nimport { addClientErrorInterceptor } from './utils'\n\nconst defaults = {\n  errors: null,\n  node_name: 'global',\n  uris: [],\n  required_scopes: [],\n  pool_size: 10,\n  pool_count: 1,\n  max_idle_time: 10\n}\n\nconst assign = {\n  id: function ({ id }) { this.id = id },\n  node_name: function ({ node_name }) { this.node_name = node_name },\n  scheme: function ({ scheme }) { this.scheme = scheme },\n  host: function ({ host }) { this.host = host },\n  port: function ({ port }) { this.port = port },\n  pool_size: function ({ pool_size }) { this.pool_size = pool_size },\n  pool_count: function ({ pool_count }) { this.pool_count = pool_count },\n  max_idle_time: function ({ max_idle_time }) { this.max_idle_time = max_idle_time },\n  strip_uri: function ({ strip_uri }) { this.strip_uri = strip_uri },\n  forwarded_token_signature_alg: function ({ forwarded_token_signature_alg }) { this.forwarded_token_signature_alg = forwarded_token_signature_alg },\n  forwarded_token_secret: function ({ forwarded_token_secret }) { this.forwarded_token_secret = forwarded_token_secret },\n  forwarded_token_public_key: function ({ forwarded_token_public_key }) { this.forwarded_token_public_key = forwarded_token_public_key },\n  forwarded_token_private_key: function ({ forwarded_token_private_key }) { this.forwarded_token_private_key = forwarded_token_private_key },\n  uris: function ({ uris }) {\n    this.uris = uris.map((uri) => ({ uri }))\n  },\n  authorize: function ({ authorize }) { this.authorize = authorize },\n  required_scopes: function ({ required_scopes }) {\n    this.required_scopes = Object.keys(required_scopes).flatMap((method) => {\n      return required_scopes[method].map(name => ({ model: new Scope({ name }), method: method }))\n    }, {})\n  },\n  error_content_type: function ({ error_content_type }) { this.error_content_type = error_content_type },\n  forbidden_response: function ({ forbidden_response }) { this.forbidden_response = forbidden_response },\n  unauthorized_response: function ({ unauthorized_response }) { this.unauthorized_response = unauthorized_response }\n}\n\nclass Upstream {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n\n  get baseUrl () {\n    const { scheme, host, port } = this\n\n    return `${scheme}://${host}:${port}`\n  }\n\n  save () {\n    this.errors = null\n    // TODO trigger validate\n    let response\n    const { id, serialized } = this\n    if (id) {\n      response = this.constructor.api().patch(`/${id}`, { upstream: serialized })\n    } else {\n      response = this.constructor.api().post('/', { upstream: serialized })\n    }\n\n    return response\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  destroy () {\n    return this.constructor.api().delete(`/${this.id}`)\n      .catch((error) => {\n        const { code, message, errors } = error.response.data\n        this.errors = errors\n        throw { code, message, errors }\n      })\n  }\n\n  get serialized () {\n    const {\n      id,\n      node_name,\n      scheme,\n      host,\n      port,\n      pool_size,\n      pool_count,\n      max_idle_time,\n      uris,\n      strip_uri,\n      authorize,\n      required_scopes,\n      error_content_type,\n      forbidden_response,\n      unauthorized_response,\n      forwarded_token_signature_alg,\n      forwarded_token_secret,\n      forwarded_token_private_key,\n      forwarded_token_public_key\n    } = this\n\n    return {\n      id,\n      node_name,\n      scheme,\n      host,\n      port,\n      pool_size,\n      pool_count,\n      max_idle_time,\n      uris: uris.map(({ uri }) => uri),\n      required_scopes: required_scopes.reduce((acc, { model: { name }, method }) => {\n        acc[method] = acc[method] || []\n        acc[method].push(name)\n        return acc\n      }, {}),\n      strip_uri,\n      authorize,\n      error_content_type,\n      forbidden_response,\n      unauthorized_response,\n      forwarded_token_signature_alg,\n      forwarded_token_secret,\n      forwarded_token_private_key,\n      forwarded_token_public_key\n    }\n  }\n}\n\nUpstream.api = function () {\n  const accessToken = localStorage.getItem('access_token')\n\n  const instance = axios.create({\n    baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/upstreams`,\n    headers: { 'Authorization': `Bearer ${accessToken}` }\n  })\n\n  return addClientErrorInterceptor(instance)\n}\n\nUpstream.nodeList = function () {\n  return this.api().get('/nodes').then(({ data }) => {\n    return data.data\n  })\n}\n\nUpstream.all = function () {\n  return this.api().get('/').then(({ data }) => {\n    const result = data.data\n\n    Object.keys(result).forEach((nodeName) => {\n      result[nodeName] = result[nodeName].map((upstream) => new Upstream(upstream))\n    })\n\n    return result\n  })\n}\n\nUpstream.get = function (id) {\n  return this.api().get(`/${id}`).then(({ data }) => {\n    return new Upstream(data.data)\n  })\n}\n\nUpstream.forwardedTokenSignatureAlgorithms = [\n  \"HS256\",\n  \"HS384\",\n  \"HS512\",\n  \"RS256\",\n  \"RS384\",\n  \"RS512\"\n]\n\nexport default Upstream\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/user.model.js",
    "content": "import axios from 'axios'\nimport Scope from './scope.model'\nimport Role from './role.model'\nimport Organization from './organization.model'\nimport Backend from './backend.model'\nimport { addClientErrorInterceptor } from './utils'\n\nconst defaults = {\n  errors: null,\n  authorize_scopes: false,\n  authorized_scopes: [],\n  roles: [],\n  organizations: [],\n  backend_id: '',\n  backend: new Backend(),\n  metadata: {},\n  federated_metadata: {}\n}\n\nconst assign = {\n  id: function ({ id }) { this.id = id },\n  uid: function ({ uid }) { this.uid = uid },\n  backend: function ({ backend }) { this.backend = backend },\n  email: function ({ email }) { this.email = email },\n  totp_registered_at: function ({ totp_registered_at }) { this.totp_registered_at = totp_registered_at },\n  federated_metadata: function ({ federated_metadata }) { this.federated_metadata = federated_metadata },\n  metadata: function ({ metadata: rawMetadata }) {\n    const metadata = {}\n\n    for (const key in rawMetadata) {\n      metadata[key] = {\n        displayStatus: rawMetadata[key].display?.includes('status'),\n        ...rawMetadata[key]\n      }\n    }\n    this.metadata = metadata\n  },\n  group: function ({ group }) { this.group = group },\n  authorized_scopes: function ({ authorized_scopes }) {\n    this.authorized_scopes = authorized_scopes.map((scope) => {\n      return { model: new Scope(scope) }\n    })\n  },\n  roles: function ({ roles }) {\n    this.roles = roles.map((role) => {\n      return { model: new Role(role) }\n    })\n  },\n  organizations: function ({ organizations }) {\n    this.organizations = organizations.map((organization) => {\n      return { model: new Organization(organization) }\n    })\n  }\n}\n\nclass User {\n  constructor (params = {}) {\n    Object.assign(this, defaults)\n\n    Object.keys(params).forEach((key) => {\n      this[key] = params[key]\n      assign[key].bind(this)(params)\n    })\n  }\n\n  get isPersisted() {\n    return this.id\n  }\n\n  // TODO factorize with Client#validate\n  validate () {\n    return new Promise((resolve, reject) => {\n      this.authorized_scopes.forEach(({ model: scope }) => {\n        if (!scope.persisted) {\n          const errors = { authorized_scopes: [ 'cannot be empty' ] }\n          this.errors = errors\n          return reject(errors)\n        }\n        if (this.authorized_scopes.filter(({ model: e }) => e.id === scope.id).length > 1) {\n          const errors = { authorized_scopes: [ 'must be unique' ] }\n          this.errors = errors\n          return reject(errors)\n        }\n      })\n      resolve()\n    })\n  }\n\n  async save () {\n    this.errors = null\n    await this.validate()\n\n    const { id, backend_id, serialized } = this\n    let response\n    if (this.isPersisted) {\n      response = this.constructor.api().patch(`/${id}`, { user: serialized })\n    } else {\n      response = this.constructor.api().post('/', { backend_id, user: serialized })\n    }\n    return response\n      .then(({ data }) => {\n        const params = data.data\n\n        Object.keys(params).forEach((key) => {\n          this[key] = params[key]\n          assign[key].bind(this)(params)\n        })\n        return this\n      })\n      .catch((error) => {\n        const { errors } = error.response.data\n        this.errors = errors\n        throw errors\n      })\n  }\n\n  destroy () {\n    return this.constructor.api().delete(`/${this.id}`)\n  }\n\n  get serialized () {\n    const { id, email, password, metadata: rawMetadata, group, authorized_scopes, roles, organizations } = this\n\n    const metadata = {}\n\n    for (const key in rawMetadata) {\n      if (rawMetadata[key]?.value) {\n        metadata[key] = {\n          display: rawMetadata[key].displayStatus ? ['status'] : [],\n          value: rawMetadata[key].value,\n          status: rawMetadata[key].status\n        }\n      }\n    }\n\n    return {\n      id,\n      email,\n      password,\n      metadata,\n      group,\n      authorized_scopes: authorized_scopes.map(({ model }) => model.serialized),\n      roles: roles.map(({ model }) => model.serialized),\n      organizations: organizations.map(({ model }) => model.serialized)\n    }\n  }\n}\n\nUser.api = function () {\n  const accessToken = localStorage.getItem('access_token')\n\n  const instance = axios.create({\n    baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/users`,\n    headers: { 'Authorization': `Bearer ${accessToken}` }\n  })\n\n  return addClientErrorInterceptor(instance)\n}\n\nUser.all = function ({ query, pageNumber }) {\n  const searchParams = new URLSearchParams()\n  pageNumber && searchParams.append('page', pageNumber)\n  query && searchParams.append('q', query)\n\n  return this.api().get(`/?${searchParams.toString()}`).then(({\n    data: {\n      data,\n      total_entries: totalEntries,\n      page_number: currentPage,\n      total_pages: totalPages,\n    }\n  }) => {\n    return {\n      data: data.map((user) => new User(user)),\n      currentPage,\n      totalPages,\n      totalEntries\n    }\n\n  })\n}\n\nUser.upload = function ({ backendId, file, options }) {\n  const formData = new FormData()\n  formData.append(\"backend_id\", backendId)\n  formData.append(\"file\", file)\n  if (options.usernameHeader && options.usernameHeader !== '')\n    formData.append(\"options[username_header]\", options.usernameHeader)\n  if (options.passwordHeader && options.passwordHeader !== '')\n    formData.append(\"options[password_header]\", options.passwordHeader)\n  if (options.hashPassword && options.hashPassword !== '')\n    formData.append(\"options[hash_password]\", options.hashPassword)\n\n  options.metadataHeaders.forEach(header => {\n    formData.append(\"options[metadata_headers][]\", `${header.origin}>${header.target}`)\n  })\n\n  return this.api().post('/', formData, {\n    headers: {\n      'Content-Type': 'multipart/form-data'\n    }\n  })\n    .then(({ data }) => data)\n    .catch((error) => {\n      const { errors } = error.response.data\n      this.errors = errors\n      throw errors\n    })\n}\n\nUser.get = function (id) {\n  return this.api().get(`/${id}`).then(({ data }) => {\n    return new User(data.data)\n  })\n}\n\nUser.default = defaults\n\nexport default User\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/models/utils.js",
    "content": "import axios from 'axios'\nimport router from '../router'\nimport oauth from '../services/oauth.service'\n\nexport function addClientErrorInterceptor(instance) {\n  instance.interceptors.response.use(function (response) {\n      return response;\n    }, function (error) {\n      if (error.response?.status === 401) {\n        return new Promise((resolve, reject) => {\n          oauth.silentRefresh()\n          function retry() {\n            window.removeEventListener('logged_in', retry)\n            const accessToken = localStorage.getItem('access_token')\n\n            const newRequest = Object.assign(error.config, {\n              headers: {\n                'Content-Type': 'application/json',\n                'Authorization': `Bearer ${accessToken}`\n              }\n            })\n            axios.request(newRequest).then(resolve).catch(reject)\n          }\n\n          window.addEventListener('logged_in', retry)\n          setTimeout(() => {\n            oauth.logout()\n            reject()\n          }, 2000)\n        })\n      }\n\n      return Promise.reject(error)\n    }\n  )\n\n  return instance\n}\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/router.js",
    "content": "import { createWebHistory, createRouter } from \"vue-router\";\nimport oauth from \"./services/oauth.service\";\n\nimport Main from \"./views/Layouts/Main.vue\";\n\nimport Home from \"./views/Home.vue\";\n\nimport OauthCallback from \"./views/OauthCallback.vue\";\nimport NotFound from \"./views/NotFound.vue\";\nimport BadRequest from \"./views/BadRequest.vue\";\n\nimport Clients from \"./views/Clients.vue\";\nimport ClientList from \"./views/Clients/ClientList.vue\";\nimport KeyPairList from \"./views/Clients/KeyPairList.vue\";\nimport NewClient from \"./views/Clients/NewClient.vue\";\nimport Client from \"./views/Clients/Client.vue\";\nimport EditClient from \"./views/Clients/EditClient.vue\";\n\nimport Upstreams from \"./views/Upstreams.vue\";\nimport UpstreamList from \"./views/Upstreams/UpstreamList.vue\";\nimport Upstream from \"./views/Upstreams/Upstream.vue\";\nimport NewUpstream from \"./views/Upstreams/NewUpstream.vue\";\nimport EditUpstream from \"./views/Upstreams/EditUpstream.vue\";\n\nimport IdentityProviders from \"./views/IdentityProviders.vue\";\nimport IdentityProviderList from \"./views/IdentityProviders/IdentityProviderList.vue\";\nimport IdentityProvider from \"./views/IdentityProviders/IdentityProvider.vue\";\nimport EditIdentityProvider from \"./views/IdentityProviders/EditIdentityProvider.vue\";\nimport EditLayoutTemplate from \"./views/IdentityProviders/EditLayoutTemplate.vue\";\nimport EditSessionTemplate from \"./views/IdentityProviders/EditSessionTemplate.vue\";\nimport EditNewChooseSessionTemplate from \"./views/IdentityProviders/EditNewChooseSessionTemplate.vue\";\nimport EditTotpRegistrationTemplate from \"./views/IdentityProviders/EditTotpRegistrationTemplate.vue\";\nimport EditTotpAuthenticationTemplate from \"./views/IdentityProviders/EditTotpAuthenticationTemplate.vue\";\nimport EditWebauthnAuthenticationTemplate from \"./views/IdentityProviders/EditWebauthnAuthenticationTemplate.vue\";\nimport EditWebauthnRegistrationTemplate from \"./views/IdentityProviders/EditWebauthnRegistrationTemplate.vue\";\nimport EditRegistrationTemplate from \"./views/IdentityProviders/EditRegistrationTemplate.vue\";\nimport EditNewConsentTemplate from \"./views/IdentityProviders/EditNewConsentTemplate.vue\";\nimport EditNewConfirmationTemplate from \"./views/IdentityProviders/EditNewConfirmationTemplate.vue\";\nimport EditNewResetPasswordTemplate from \"./views/IdentityProviders/EditNewResetPasswordTemplate.vue\";\nimport EditEditResetPasswordTemplate from \"./views/IdentityProviders/EditEditResetPasswordTemplate.vue\";\nimport EditEditUserTemplate from \"./views/IdentityProviders/EditEditUserTemplate.vue\";\nimport EditCredentialOfferTemplate from \"./views/IdentityProviders/EditCredentialOfferTemplate.vue\";\nimport EditCrossDevicePresentationTemplate from \"./views/IdentityProviders/EditCrossDevicePresentationTemplate.vue\";\nimport NewIdentityProvider from \"./views/IdentityProviders/NewIdentityProvider.vue\";\n\nimport Users from \"./views/IdentityProviders/Users.vue\";\nimport UserList from \"./views/IdentityProviders/UserList.vue\";\nimport UserImport from \"./views/IdentityProviders/UserImport.vue\";\nimport NewUser from \"./views/IdentityProviders/NewUser.vue\";\nimport EditUser from \"./views/IdentityProviders/EditUser.vue\";\n\nimport Organizations from \"./views/IdentityProviders/Organizations.vue\";\nimport OrganizationList from \"./views/IdentityProviders/OrganizationList.vue\";\nimport NewOrganization from \"./views/IdentityProviders/NewOrganization.vue\";\nimport EditOrganization from \"./views/IdentityProviders/EditOrganization.vue\";\n\nimport Backends from \"./views/IdentityProviders/Backends.vue\";\nimport Backend from \"./views/IdentityProviders/Backends/Backend.vue\";\nimport BackendList from \"./views/IdentityProviders/BackendList.vue\";\nimport NewBackend from \"./views/IdentityProviders/NewBackend.vue\";\nimport EditBackend from \"./views/IdentityProviders/EditBackend.vue\";\nimport EditConfirmationInstructionsEmailTemplate from \"./views/IdentityProviders/Backends/EditConfirmationInstructionsEmailTemplate.vue\";\nimport EditResetPasswordInstructionsEmailTemplate from \"./views/IdentityProviders/Backends/EditResetPasswordInstructionsEmailTemplate.vue\";\nimport EditTxCodeEmailTemplate from \"./views/IdentityProviders/Backends/EditTxCodeEmailTemplate.vue\";\n\nimport Scopes from \"./views/Scopes.vue\";\nimport ScopeList from \"./views/Scopes/ScopeList.vue\";\n\nimport Roles from \"./views/Roles.vue\";\nimport RoleList from \"./views/Roles/RoleList.vue\";\nimport Role from \"./views/Roles/Role.vue\";\nimport NewRole from \"./views/Roles/NewRole.vue\";\nimport EditRole from \"./views/Roles/EditRole.vue\";\n\nimport Configuration from \"./views/Configuration.vue\";\nimport ConfigurationFileUpload from \"./views/Configuration/ConfigurationFileUpload.vue\";\nimport ErrorTemplateList from \"./views/Configuration/ErrorTemplateList.vue\";\nimport EditBadRequestTemplate from \"./views/Configuration/EditBadRequestTemplate.vue\";\nimport EditNotFoundTemplate from \"./views/Configuration/EditNotFoundTemplate.vue\";\nimport EditForbiddenTemplate from \"./views/Configuration/EditForbiddenTemplate.vue\";\nimport EditInternalServerErrorTemplate from \"./views/Configuration/EditInternalServerErrorTemplate.vue\";\n\nimport Dashboard from \"./views/Dashboard.vue\";\nimport Requests from \"./views/Dashboard/Requests.vue\";\nimport BusinessEvents from \"./views/Dashboard/BusinessEvents.vue\";\n\nconst router = createRouter({\n  history: createWebHistory(),\n  linkActiveClass: \"active\",\n  routes: [\n    {\n      path: \"/\",\n      component: Main,\n      name: \"root\",\n      redirect: \"/\",\n      children: [\n        {\n          path: \"\",\n          name: \"home\",\n          component: Home,\n        },\n        {\n          path: \"not-found\",\n          name: \"not-found\",\n          component: NotFound,\n        },\n        {\n          path: \"bad-request\",\n          name: \"bad-request\",\n          component: BadRequest,\n        },\n        {\n          path: \"/oauth-callback\",\n          name: \"oauth-callback\",\n          component: OauthCallback,\n        },\n        {\n          path: \"/dashboard\",\n          name: \"dashboard\",\n          component: Dashboard,\n          redirect: \"/dashboard/requests\",\n          children: [\n            {\n              path: \"requests\",\n              name: \"request-logs\",\n              component: Requests,\n            },\n            {\n              path: \"business-events\",\n              name: \"business-event-logs\",\n              component: BusinessEvents,\n            },\n          ],\n        },\n        {\n          path: \"/identity-providers\",\n          component: IdentityProviders,\n          name: \"identity-providers\",\n          redirect: \"/identity-providers/\",\n          children: [\n            {\n              path: \"\",\n              name: \"identity-provider-list\",\n              component: IdentityProviderList,\n            },\n            {\n              path: \"new\",\n              name: \"new-identity-provider\",\n              component: NewIdentityProvider,\n            },\n            {\n              path: \"/identity-providers/:identityProviderId\",\n              name: \"identity-provider\",\n              component: IdentityProvider,\n              redirect: (to) => ({\n                name: \"edit-identity-provider\",\n                params: { identityProviderId: to.params.identityProviderId },\n              }),\n              children: [\n                {\n                  path: \"edit\",\n                  name: \"edit-identity-provider\",\n                  component: EditIdentityProvider,\n                },\n                {\n                  path: \"edit/choose-session-template\",\n                  name: \"edit-choose-session-template\",\n                  component: EditNewChooseSessionTemplate,\n                },\n                {\n                  path: \"edit/layout-template\",\n                  name: \"edit-layout-template\",\n                  component: EditLayoutTemplate,\n                },\n                {\n                  path: \"edit/session-template\",\n                  name: \"edit-session-template\",\n                  component: EditSessionTemplate,\n                },\n                {\n                  path: \"edit/totp-registration-template\",\n                  name: \"edit-totp-registration-template\",\n                  component: EditTotpRegistrationTemplate,\n                },\n                {\n                  path: \"edit/totp-authentication-template\",\n                  name: \"edit-totp-authentication-template\",\n                  component: EditTotpAuthenticationTemplate,\n                },\n                {\n                  path: \"edit/webauthn-registration-template\",\n                  name: \"edit-webauthn-registration-template\",\n                  component: EditWebauthnRegistrationTemplate,\n                },\n                {\n                  path: \"edit/webauthn-authentication-template\",\n                  name: \"edit-webauthn-authentication-template\",\n                  component: EditWebauthnAuthenticationTemplate,\n                },\n                {\n                  path: \"edit/registration-template\",\n                  name: \"edit-registration-template\",\n                  component: EditRegistrationTemplate,\n                },\n                {\n                  path: \"edit/edit-user-template\",\n                  name: \"edit-edit-user-template\",\n                  component: EditEditUserTemplate,\n                },\n                {\n                  path: \"edit/send-reset-password-instructions-template\",\n                  name: \"edit-new-reset-password-template\",\n                  component: EditNewResetPasswordTemplate,\n                },\n                {\n                  path: \"edit/reset-password-template\",\n                  name: \"edit-edit-reset-password-template\",\n                  component: EditEditResetPasswordTemplate,\n                },\n                {\n                  path: \"edit/consent-template\",\n                  name: \"edit-new-consent-template\",\n                  component: EditNewConsentTemplate,\n                },\n                {\n                  path: \"edit/send-confirmation-instructions-template\",\n                  name: \"edit-new-confirmation-template\",\n                  component: EditNewConfirmationTemplate,\n                },\n                {\n                  path: \"edit/credential-offer-template\",\n                  name: \"edit-credential-offer-template\",\n                  component: EditCredentialOfferTemplate,\n                },\n                {\n                  path: \"edit/cross-device-presentation-template\",\n                  name: \"edit-cross-device-presentation-template\",\n                  component: EditCrossDevicePresentationTemplate,\n                },\n              ],\n            },\n            {\n              path: \"backends\",\n              name: \"backends\",\n              component: Backends,\n              redirect: \"/identity-providers/backends/\",\n              children: [\n                {\n                  path: \"\",\n                  name: \"backend-list\",\n                  component: BackendList,\n                },\n                {\n                  path: \"/backends/new\",\n                  name: \"new-backend\",\n                  component: NewBackend,\n                },\n                {\n                  path: \"/backends/:backendId\",\n                  name: \"backend\",\n                  component: Backend,\n                  redirect: (to) => ({\n                    name: \"edit-backend\",\n                    params: { backendId: to.params.backendId },\n                  }),\n                  children: [\n                    {\n                      path: \"edit\",\n                      name: \"edit-backend\",\n                      component: EditBackend,\n                    },\n                    {\n                      path: \"edit/confirmation-instructions-email-template\",\n                      name: \"edit-confirmation-instructions-email-template\",\n                      component: EditConfirmationInstructionsEmailTemplate,\n                    },\n                    {\n                      path: \"edit/reset-password-instructions-email-template\",\n                      name: \"edit-reset-password-instructions-email-template\",\n                      component: EditResetPasswordInstructionsEmailTemplate,\n                    },\n                    {\n                      path: \"edit/tx-code-email-template\",\n                      name: \"edit-tx-code-email-template\",\n                      component: EditTxCodeEmailTemplate,\n                    },\n                  ],\n                },\n              ],\n            },\n            {\n              path: \"users\",\n              name: \"users\",\n              component: Users,\n              redirect: \"/identity-providers/users/\",\n              children: [\n                {\n                  path: \"\",\n                  name: \"user-list\",\n                  component: UserList,\n                },\n                {\n                  path: \"import\",\n                  name: \"user-import\",\n                  component: UserImport,\n                },\n                {\n                  path: \"/users/new\",\n                  name: \"new-user\",\n                  component: NewUser,\n                },\n                {\n                  path: \"/users/:userId/edit\",\n                  name: \"edit-user\",\n                  component: EditUser,\n                },\n              ],\n            },\n            {\n              path: \"organizations\",\n              name: \"organizations\",\n              component: Organizations,\n              redirect: \"/identity-providers/organizations/\",\n              children: [\n                {\n                  path: \"\",\n                  name: \"organization-list\",\n                  component: OrganizationList,\n                },\n                {\n                  path: \"/organizations/new\",\n                  name: \"new-organization\",\n                  component: NewOrganization,\n                },\n                {\n                  path: \"/organizations/:organizationId/edit\",\n                  name: \"edit-organization\",\n                  component: EditOrganization,\n                },\n              ],\n            },\n          ],\n        },\n        {\n          path: \"/clients\",\n          component: Clients,\n          name: \"clients\",\n          redirect: \"/clients/\",\n          children: [\n            {\n              path: \"\",\n              name: \"client-list\",\n              component: ClientList,\n            },\n            {\n              path: \"key-pairs\",\n              name: \"key-pair-list\",\n              component: KeyPairList,\n            },\n            {\n              path: \"/clients/new\",\n              name: \"new-client\",\n              component: NewClient,\n            },\n            {\n              path: \"/clients/:clientId\",\n              name: \"client\",\n              component: Client,\n              redirect: (to) => ({\n                name: \"edit-client\",\n                params: { clientId: to.params.clientId },\n              }),\n              children: [\n                {\n                  path: \"edit\",\n                  name: \"edit-client\",\n                  component: EditClient,\n                },\n              ],\n            },\n          ],\n        },\n        {\n          path: \"/upstreams\",\n          component: Upstreams,\n          name: \"upstreams\",\n          redirect: \"/upstreams/\",\n          children: [\n            {\n              path: \"\",\n              name: \"upstream-list\",\n              component: UpstreamList,\n            },\n            {\n              path: \"/upstreams/new\",\n              name: \"new-upstream\",\n              component: NewUpstream,\n            },\n            {\n              path: \"/upstreams/:upstreamId\",\n              name: \"upstream\",\n              component: Upstream,\n              redirect: (to) => ({\n                name: \"edit-upstream\",\n                params: { upstreamId: to.params.upstreamId },\n              }),\n              children: [\n                {\n                  path: \"edit\",\n                  name: \"edit-upstream\",\n                  component: EditUpstream,\n                },\n              ],\n            },\n          ],\n        },\n        {\n          path: \"/scopes\",\n          component: Scopes,\n          name: \"scopes\",\n          redirect: \"/scopes/\",\n          children: [\n            {\n              path: \"\",\n              name: \"scope-list\",\n              component: ScopeList,\n            },\n            {\n              path: \"/roles\",\n              component: Roles,\n              name: \"roles\",\n              redirect: \"/roles/\",\n              children: [\n                {\n                  path: \"\",\n                  name: \"role-list\",\n                  component: RoleList,\n                },\n                {\n                  path: \"/roles/new\",\n                  name: \"new-role\",\n                  component: NewRole,\n                },\n                {\n                  path: \"/roles/:roleId\",\n                  name: \"role\",\n                  component: Role,\n                  redirect: (to) => ({\n                    name: \"edit-role\",\n                    params: { roleId: to.params.roleId },\n                  }),\n                  children: [\n                    {\n                      path: \"edit\",\n                      name: \"edit-role\",\n                      component: EditRole,\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n        }, {\n          path: \"/configuration\",\n          component: Configuration,\n          name: \"configuration\",\n          redirect: \"/configuration/error-template-list\",\n          children: [\n            {\n              path: \"\",\n              name: \"\",\n              component: Configuration,\n            },\n            {\n              path: \"configuration-file-upload/:type(example-configuration-file)?\",\n              name: \"configuration-file-upload\",\n              component: ConfigurationFileUpload,\n            },\n            {\n              path: \"error-template-list\",\n              name: \"error-template-list\",\n              component: ErrorTemplateList,\n            },\n            {\n              path: \"edit-bad-request-template\",\n              name: \"edit-bad-request-template\",\n              component: EditBadRequestTemplate,\n            },\n            {\n              path: \"edit-forbidden-template\",\n              name: \"edit-forbidden-template\",\n              component: EditForbiddenTemplate,\n            },\n            {\n              path: \"edit-not-found-template\",\n              name: \"edit-not-found-template\",\n              component: EditNotFoundTemplate,\n            },\n            {\n              path: \"edit-internal-server-error-template\",\n              name: \"edit-internal-server-error-template\",\n              component: EditInternalServerErrorTemplate,\n            },\n          ],\n        },\n        {\n          path: \"/:pathMatch(.*)*\",\n          name: \"not-found\",\n          component: NotFound,\n        },\n      ],\n    },\n  ],\n});\n\nrouter.beforeEach((to, _from, next) => {\n  if (to.name === \"oauth-callback\") return next();\n\n  oauth.storeLocationName(to);\n\n  if (!oauth.isAuthenticated) {\n    // TODO find a way to remove event listener once triggered\n    const continueNavigation = () => {\n      router.push(oauth.storedLocation);\n      window.removeEventListener(\"logged_in\", continueNavigation);\n    };\n    window.addEventListener(\"logged_in\", continueNavigation);\n\n    oauth.silentRefresh();\n    return next(new Error('Not logged in'));\n  } else {\n    return next();\n  }\n});\n\nexport default router;\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/services/configuration-file.service.js",
    "content": "import axios from 'axios'\nimport { addClientErrorInterceptor } from \"../models/utils\";\n\nexport default class ConfigurationFile {\n  static get api () {\n    const accessToken = localStorage.getItem(\"access_token\");\n\n    let instance = axios.create({\n      baseURL: `${window.env.BORUTA_ADMIN_BASE_URL}/api/configuration`,\n      headers: { Authorization: `Bearer ${accessToken}` },\n    });\n    return addClientErrorInterceptor(instance);\n  }\n\n  static upload (file) {\n    const formData = new FormData()\n    formData.append('file', file)\n\n    return this.api.post('/upload-configuration-file', formData, {\n      headers: { 'Content-Type': 'multipart/form-data' }\n    }).catch(({ response }) => {\n      if (response.status == 400) {\n        throw { errors: { file: ['is invalid'] } }\n      } else {\n        throw error\n      }\n    }).then(({ data }) => data)\n  }\n\n  static get (type = '') {\n    return this.api.get(`/${type}`).then(({ data }) => {\n      const configuration = data.data.find(({ name }) => name == 'configuration_file')\n      return configuration && configuration.value || this.baseConfiguration\n    }).catch(() => '')\n  }\n\n  static get baseConfiguration () {\n    return `\n---\nversion: \"1.0\"\nconfiguration:\n  client:\n  identity_provider:\n  backend:\n  role:\n  scope:\n  gateway:\n  microgateway:\n  error_template:\n    `\n  }\n}\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/services/oauth.service.js",
    "content": "import decode from 'jwt-decode'\nimport { BorutaOauth } from 'boruta-client'\n\nclass Oauth {\n  constructor () {\n    const oauth = new BorutaOauth({\n      window,\n      host: window.env.BORUTA_ADMIN_OAUTH_BASE_URL,\n      authorizePath: '/oauth/authorize',\n      revokePath: '/oauth/revoke'\n    })\n\n    this.implicitClient = new oauth.Implicit({\n      clientId: window.env.BORUTA_ADMIN_OAUTH_CLIENT_ID,\n      redirectUri: `${window.env.BORUTA_ADMIN_BASE_URL}/oauth-callback`,\n      scope: 'openid email profile scopes:manage:all clients:manage:all users:manage:all upstreams:manage:all identity-providers:manage:all configuration:manage:all logs:read:all',\n      silentRefresh: true,\n      silentRefreshCallback: this.authenticate.bind(this),\n      responseType: 'id_token token'\n    })\n\n    this.revokeClient = new oauth.Revoke({\n      clientId: window.env.BORUTA_ADMIN_OAUTH_CLIENT_ID\n    })\n  }\n\n  get idToken() {\n    return localStorage.getItem('id_token')\n  }\n\n  get currentUser() {\n    try {\n      const { email } = decode(this.idToken)\n      return { email }\n    } catch {\n      return {}\n    }\n  }\n\n  authenticate (response) {\n    if (window.frameElement) return\n\n    if (response.error) {\n      this.login()\n    }\n\n    const { access_token, id_token, expires_in } = response\n    const expires_at = new Date().getTime() + expires_in * 1000\n\n    localStorage.setItem('access_token', access_token)\n    localStorage.setItem('id_token', id_token)\n    localStorage.setItem('token_expires_at', expires_at)\n\n    setTimeout(() => {\n      const loggedIn = new Event('logged_in')\n      window.dispatchEvent(loggedIn)\n    }, 500)\n  }\n\n  login () {\n    window.location = this.implicitClient.loginUrl\n  }\n\n  silentRefresh () {\n    this.implicitClient.silentRefresh()\n  }\n\n  async callback () {\n    return this.implicitClient.callback().then(response => {\n      this.authenticate(response)\n    }).catch((error) => {\n      console.log(error)\n      this.login()\n      throw error\n    })\n  }\n\n  logout () {\n    return this.revokeClient.revoke(this.accessToken).then(() => {\n      localStorage.removeItem('access_token')\n      localStorage.removeItem('token_expires_at')\n    })\n  }\n\n  storeLocationName ({ name, params, query }) {\n    localStorage.setItem('stored_location_name', name)\n    localStorage.setItem('stored_location_params', JSON.stringify(params))\n    localStorage.setItem('stored_location_query', JSON.stringify(query))\n  }\n\n  get storedLocation () {\n    const name = localStorage.getItem('stored_location_name') || 'home'\n    const params = JSON.parse(localStorage.getItem('stored_location_params') || '{}')\n    const query = JSON.parse(localStorage.getItem('stored_location_query') || '{}')\n    return { name, params, query }\n  }\n\n  get accessToken () {\n    return localStorage.getItem('access_token')\n  }\n\n  get isAuthenticated () {\n    const accessToken = localStorage.getItem('access_token')\n    const expiresAt = localStorage.getItem('token_expires_at')\n    return accessToken && parseInt(expiresAt) > new Date().getTime()\n  }\n\n  get expiresIn () {\n    const expiresAt = localStorage.getItem('token_expires_at')\n    return parseInt(expiresAt) - new Date().getTime()\n  }\n}\n\nexport default new Oauth()\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/store.js",
    "content": "import { createStore } from 'vuex'\n\nimport oauth from './services/oauth.service'\n\nexport default createStore({\n  state: {\n    isAuthenticated: false\n  },\n  getters: {\n    isAuthenticated (state) {\n      return state.isAuthenticated\n    }\n  },\n  mutations: {\n    SET_AUTHENTICATED (state, isAuthenticated) {\n      state.isAuthenticated = isAuthenticated\n    }\n  },\n  actions: {\n    logout ({ commit }) {\n      oauth.logout().then(() => {\n        commit('SET_AUTHENTICATED', false)\n        return oauth.login()\n      })\n    }\n  }\n})\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/BadRequest.vue",
    "content": "<template>\n  <div class=\"bad-request\">\n    <div class=\"ui container\">\n      <div class=\"ui placeholder segment\">\n        <div class=\"ui icon header\">\n          <i class=\"sign question circle icon\"></i>\n          The request could not be processed\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'bad-request'\n}\n</script>\n\n<style scoped lang=\"scss\">\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Clients/Client.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Clients/ClientList.vue",
    "content": "<template>\n  <div class=\"client-list\">\n    <Toaster :active=\"deleted\" message=\"Client has been deleted\" type=\"warning\" />\n    <Toaster :active=\"errorMessage\" :message=\"errorMessage\" type=\"error\" />\n    <router-link :to=\"{ name: 'new-client' }\" class=\"ui violet main create button\">Add a client</router-link>\n    <div class=\"container\">\n      <div class=\"ui info message\">\n        Clients are here relying parties as defined in <a target=\"_blank\" href=\"https://datatracker.ietf.org/doc/html/rfc6749#section-1.1\">OAuth 2.0 RFC</a>. They are the applications that require a priviledge access to a HTTP service secured by the Boruta authorization server.\n      </div>\n      <div class=\"ui two column clients stackable grid\" v-if=\"clients.length\">\n        <div v-for=\"client in clients\" class=\"ui column\" :key=\"client.id\">\n          <div class=\"ui client highlightable segment\">\n            <div class=\"ui tiny icon message\" v-if=\"isPublic(client)\">\n              <i class=\"cloud icon\"></i>\n              <div class=\"content\">\n                <strong>Public client</strong>\n              </div>\n            </div>\n            <div class=\"actions\">\n              <router-link\n                :to=\"{ name: 'edit-client', params: { clientId: client.id } }\"\n                class=\"ui tiny blue button\">edit</router-link>\n              <a v-on:click=\"deleteClient(client)\" class=\"ui tiny red button\">delete</a>\n            </div>\n            <div class=\"ui attribute list\">\n              <div class=\"item\" v-if=\"client.name\">\n                <span class=\"header\">Name</span>\n                <span class=\"description\">{{ client.name }}</span>\n              </div>\n              <div class=\"item\">\n                <span class=\"header\">Client ID</span>\n                <span class=\"description\">{{ client.id }}</span>\n              </div>\n              <div class=\"item\">\n                <span class=\"header\">Client secret</span>\n                <span class=\"description\">\n                  <div class=\"ui form field\">\n                    <div class=\"ui left icon input\">\n                      <input disabled :type=\"client.passwordVisible ? 'text' : 'password'\" v-model=\"client.secret\" />\n                      <i class=\"eye icon\" :class=\"{ 'slash': client.passwordVisible }\" @click=\"passwordVisibilityToggle(client)\"></i>\n                    </div>\n                  </div>\n                </span>\n              </div>\n              <div class=\"item\">\n                <span class=\"header\">Public key</span>\n                <pre class=\"description\">{{ client.public_key }}</pre>\n              </div>\n              <div class=\"item\" v-if=\"client.redirect_uris.length\">\n                <span class=\"header\">Client redirect URIs</span>\n                <span class=\"description\" v-for=\"uri in client.redirect_uris\" :key=\"uri.uri\">\n                  {{ uri.uri }}\n                </span>\n              </div>\n              <div class=\"item\" v-if=\"client.authorize_scope\">\n                <span class=\"header\">Authorized scopes</span>\n                <span class=\"description\">\n                  <span v-for=\"scope in client.authorized_scopes\" class=\"ui teal label\" :key=\"scope.model.id\">\n                    {{ scope.model.name }}\n                  </span>\n                </span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Client from '../../models/client.model'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'client-list',\n  components: {\n    Toaster\n  },\n  data () {\n    return {\n      clients: [] ,\n      deleted: false,\n      errorMessage: false\n    }\n  },\n  mounted () {\n    this.getClients()\n  },\n  methods: {\n    getClients () {\n      Client.all().then((clients) => {\n        this.clients = clients\n      })\n    },\n    isPublic (client) {\n      return client.public_client_id == window.env.BORUTA_OAUTH_BASE_URL\n    },\n    deleteClient (client) {\n      if (!confirm('Are you sure ?')) return\n      this.deleted = false\n      this.errorMessage = false\n      client.destroy().then(() => {\n        this.deleted = true\n        this.clients.splice(this.clients.indexOf(client), 1)\n      }).catch((error) => {\n        this.errorMessage = error.message\n      })\n    },\n    passwordVisibilityToggle (client) {\n      client.passwordVisible = !client.passwordVisible\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.input {\n  display: flex;\n  input[disabled] {\n    opacity: 1!important;\n    border: none!important;\n    background: inherit!important;\n    padding: 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Clients/EditClient.vue",
    "content": "<template>\n  <div class=\"edit-client\">\n    <Toaster :active=\"success\" message=\"Client has been updated\" type=\"success\" />\n    <div class=\"container\">\n      <div class=\"ui stackable grid\">\n        <div class=\"four wide column\">\n          <div class=\"sidebar\">\n            <div class=\"ui segment\">\n              <div class=\"ui attribute list\">\n                <div class=\"item\">\n                  <span class=\"header\">Client ID</span>\n                  <span class=\"description\">{{ client.id }}</span>\n                </div>\n              </div>\n            </div>\n            <div class=\"ui urls info message\">\n              <div><strong>OpenIDConfiguration:</strong> {{ openidConfigurationUrl }}</div>\n              <div><strong>AuthorizeUrl:</strong> {{ authorizeUrl }}</div>\n              <div><strong>TokenUrl:</strong> {{ tokenUrl }}</div>\n            </div>\n            <router-link :to=\"{ name: 'client-list' }\" class=\"ui right floated button\">Back</router-link>\n          </div>\n        </div>\n        <div class=\"twelve wide column\">\n          <ClientForm :client=\"client\" @submit=\"updateClient()\" @back=\"back()\" action=\"Update\" />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Client from '../../models/client.model'\nimport ClientForm from '../../components/Forms/ClientForm.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'clients',\n  components: {\n    ClientForm,\n    Toaster\n  },\n  mounted () {\n    const { clientId } = this.$route.params\n    Client.get(clientId).then((client) => {\n      this.client = client\n    })\n  },\n  data () {\n    return {\n      errors: null,\n      scopes: [],\n      success: false,\n      client: new Client()\n    }\n  },\n  computed: {\n    authorizeUrl () {\n      return window.env.BORUTA_OAUTH_BASE_URL + '/oauth/authorize'\n    },\n    tokenUrl () {\n      return window.env.BORUTA_OAUTH_BASE_URL + '/oauth/token'\n    },\n    openidConfigurationUrl () {\n      return window.env.BORUTA_OAUTH_BASE_URL + '/.well-known/openid-configuration'\n    },\n  },\n  methods: {\n    updateClient () {\n      this.success = false\n      return this.client.save().then(() => {\n        this.success = true\n      }).catch()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Clients/KeyPairList.vue",
    "content": "<template>\n  <div class=\"key-pair-list\">\n    <Toaster :active=\"created\" message=\"Key pair has been created\" type=\"success\" />\n    <Toaster :active=\"rotated\" message=\"Key pair has been rotated\" type=\"success\" />\n    <Toaster :active=\"error\" :message=\"error\" type=\"error\" />\n    <Toaster :active=\"deleted\" message=\"Key pair has been deleted\" type=\"warning\" />\n    <a class=\"ui violet main create button\" v-on:click=\"createKeyPair()\">Add a key pair</a>\n    <div class=\"container\">\n      <h2>Key pairs</h2>\n      <div class=\"ui three column stackable grid\">\n        <div class=\"ui column\" v-for=\"keyPair in keyPairs\">\n          <div class=\"ui key-pair segment\">\n            <div class=\"actions\">\n              <a v-on:click=\"setDefault(keyPair)\" class=\"ui tiny blue button\">default</a>\n              <a v-on:click=\"rotate(keyPair)\" class=\"ui tiny orange button\">rotate</a>\n              <a v-on:click=\"deleteKeyPair(keyPair)\" class=\"ui tiny red button\">delete</a>\n            </div>\n            <label><strong>Key pair ID</strong> {{ keyPair.id }}</label>\n            <h3>Public key</h3>\n            <pre>{{ keyPair.public_key }}</pre>\n            <div class=\"ui default label\" v-if=\"keyPair.is_default\">default</div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport KeyPair from '../../models/key-pair.model'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'key-pair-list',\n  components: {\n    Toaster\n  },\n  data () {\n    return {\n      created: false,\n      rotated: false,\n      deleted: false,\n      error: false,\n      keyPairs: []\n    }\n  },\n  mounted () {\n    this.getKeyPairs()\n  },\n  methods: {\n    getKeyPairs () {\n      KeyPair.all().then(keyPairs => {\n        this.keyPairs = keyPairs\n      })\n    },\n    createKeyPair () {\n      const keyPair = new KeyPair()\n\n      this.created = false\n      keyPair.save().then(keyPair => {\n        this.keyPairs.push(keyPair)\n        this.created = true\n      })\n    },\n    setDefault (keyPair) {\n      keyPair.is_default = true\n      keyPair.save().then((keyPair) => {\n        if (keyPair.is_default) {\n          this.keyPairs.forEach(keyPair => keyPair.is_default = false)\n          keyPair.is_default = true\n        }\n      }).catch(() => {\n        keyPair.is_default = false\n      })\n    },\n    rotate (keyPair) {\n      if (!confirm('Are you sure?')) return\n      this.rotated = false\n      keyPair.rotate().then(() => {\n        this.rotated = true\n      })\n    },\n    deleteKeyPair (keyPair) {\n      if (!confirm('Are you sure?')) return\n\n      this.deleted = false\n      this.error = false\n      keyPair.destroy().then(() => {\n        this.deleted = true\n        this.keyPairs.splice(this.keyPairs.indexOf(keyPair), 1)\n      }).catch(error => this.error = error.message)\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.key-pair {\n  padding-bottom: 1.6em;\n  pre {\n    overflow: hidden;\n    overflow-x: scroll;\n  }\n  .default.label {\n    position: absolute;\n    bottom: 0;\n    right: 0;\n  }\n}\n</style>\n\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Clients/NewClient.vue",
    "content": "<template>\n  <div class=\"new-client\">\n    <div class=\"container\">\n      <div class=\"ui stackable grid\">\n        <div class=\"four wide column\">\n          <div class=\"sidebar\">\n            <router-link :to=\"{ name: 'client-list' }\" class=\"ui right floated button\">Back</router-link>\n          </div>\n        </div>\n        <div class=\"twelve wide column\">\n          <ClientForm :client=\"client\" @submit=\"createClient()\" @back=\"back()\" action=\"Create\" />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Client from '../../models/client.model'\nimport ClientForm from '../../components/Forms/ClientForm.vue'\n\nexport default {\n  name: 'clients',\n  components: {\n    ClientForm\n  },\n  data () {\n    return {\n      client: new Client()\n    }\n  },\n  methods: {\n    back () {\n      this.$router.push({ name: 'client-list' })\n    },\n    createClient () {\n      return this.client.save().then(() => {\n        this.$router.push({ name: 'client-list' })\n      }).catch()\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.new-client {\n  .ui.icon.input>i.icon.close {\n    cursor: pointer;\n    pointer-events: all;\n  }\n  .authorized-scopes-select {\n    margin-right: 3em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Clients.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Configuration/ConfigurationFileUpload.vue",
    "content": "<template>\n  <div class=\"configuration-file-upload\">\n    <div class=\"container\">\n      <div class=\"ui stackable grid\">\n        <div class=\"twelve wide column\">\n          <div class=\"upload-result\">\n            <div class=\"ui file-content segment\">\n              <TextEditor :content=\"fileContent\" @codeUpdate=\"setContent\" />\n            </div>\n          </div>\n        </div>\n        <div class=\"four wide column\">\n          <div class=\"sidebar\">\n            <form class=\"ui form\" @submit.prevent=\"submit\">\n              <div class=\"ui segment\">\n                <div class=\"field\">\n                  <input type=\"file\" @change=\"onFileChange\" accept=\".yml\" :key=\"fileUpdates\" />\n                </div>\n              </div>\n              <div class=\"ui segment\">\n                <button type=\"submit\" :to=\"{ name: 'new-backend' }\" class=\"ui violet fluid create button\">Upload <span v-if=\"edited\">edited </span>configuration</button>\n              </div>\n            </form>\n            <div class=\"ui results segment\">\n              <FormErrors :errors=\"errors\" v-if=\"errors\" :inline=\"true\" />\n              <div class=\"result\" v-for=\"(errors, key) in result.errors\">\n                <h3>{{ key }}</h3>\n                <FormErrors :errors=\"errors\" :inline=\"true\" v-if=\"errors\" v-for=\"errors in errors\"/>\n                <div class=\"ui success message\" v-if=\"!errors.length\">Resources have been saved.</div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ConfigurationFile from '../../services/configuration-file.service.js'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport FormErrors from '../../components/Forms/FormErrors.vue'\n\nexport default {\n  name: 'configuration-file-upload',\n  components: {\n    FormErrors,\n    TextEditor\n  },\n  data () {\n    return {\n      file: null,\n      result: {},\n      fileContent: '',\n      errors: null,\n      edited: false\n    }\n  },\n  mounted () {\n    this.fileContent = null\n    ConfigurationFile.get(this.$route.params.type).then(fileContent => {\n      this.fileContent = fileContent\n      this.file = new Blob([fileContent], {type : 'text/plain'})\n    })\n  },\n  methods: {\n    submit () {\n      this.errors = null\n      ConfigurationFile.upload(this.file).then(result => {\n        this.result = result\n        this.fileContent = result.file_content\n        this.edited = false\n      }).catch(({ errors }) => {\n        this.errors = errors\n      })\n    },\n    onFileChange (event) {\n      this.fileContent = null\n      this.file = event.target.files[0]\n      new Response(this.file).text().then(fileContent => {\n          this.fileContent = fileContent\n      })\n    },\n    setContent (content) {\n      this.file = new Blob([content], {type : 'text/plain'})\n      this.edited = true\n    }\n  },\n  watch: {\n    fileUpdates() {\n      this.file = null\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.file-content {\n  height: calc(100vh - 162px);\n  margin: 0 !important;\n}\n.results {\n  height: calc(100vh - 340px);\n  margin: 0 !important;\n  overflow: hidden;\n  overflow-y: scroll;\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Configuration/EditBadRequestTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-bad-request-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'error-template-list' }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ErrorTemplate from '../../models/error-template.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-bad-request-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    ErrorTemplate.get(400).then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      template: new ErrorTemplate(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-bad-request-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Configuration/EditForbiddenTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-forbidden-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'error-template-list' }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ErrorTemplate from '../../models/error-template.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-forbidden-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    ErrorTemplate.get(403).then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      template: new ErrorTemplate(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-forbidden-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Configuration/EditInternalServerErrorTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-internal-server-error-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'error-template-list' }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ErrorTemplate from '../../models/error-template.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-internal-server-error-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    ErrorTemplate.get(500).then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      template: new ErrorTemplate(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-forbidden-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Configuration/EditNotFoundTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-not-found-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'error-template-list' }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ErrorTemplate from '../../models/error-template.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-not-found-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    ErrorTemplate.get(404).then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      template: new ErrorTemplate(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-not-found-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Configuration/ErrorTemplateList.vue",
    "content": "<template>\n  <div class=\"error-template-list\">\n    <div class=\"container\">\n      <div class=\"ui info message\">\n        Here you can edit error pages user may encounter while navigating through the identity provider.\n      </div>\n      <div class=\"ui three column clients stackable grid\">\n        <div v-for=\"template in templates\" class=\"ui column\" :key=\"template.type\">\n          <div class=\"ui highlitable placeholder segment\">\n            <div class=\"ui icon header\">\n              <i class=\"object group icon\"></i>\n              {{ template.label }}\n            </div>\n            <router-link :to=\"{ name: `edit-${template.name}-template` }\" class=\"ui primary button\">Edit template</router-link>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ErrorTemplate from '../../models/error-template.model.js'\n\nexport default {\n  name: 'error-template-list',\n  data () {\n    return {\n      templates: ErrorTemplate.all()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Configuration.vue",
    "content": "<template>\n  <router-view></router-view>\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Dashboard/BusinessEvents.vue",
    "content": "<template>\n  <div class=\"dashboard\">\n    <div class=\"container\">\n      <div class=\"ui error message\" v-if=\"error\">\n        {{ error }}\n      </div>\n      <div class=\"ui dates form\">\n        <div class=\"ui stackable grid\">\n          <div class=\"four wide request-times column\">\n            <h1>Business events</h1>\n          </div>\n          <div class=\"five wide request-times column\">\n            <input type=\"datetime-local\" v-model=\"dateFilter.startAt\" :disabled=\"pending\" />\n          </div>\n          <div class=\"five wide request-times column\">\n            <input type=\"datetime-local\" v-model=\"dateFilter.endAt\" :disabled=\"pending\" />\n          </div>\n          <div class=\"two wide request-times column\">\n            <button class=\"ui fluid blue button\" @click=\"getLogs()\" :disabled=\"pending\">Filter</button>\n          </div>\n        </div>\n      </div>\n      <div class=\"ui segment\">\n        <div class=\"ui requests form\">\n          <div class=\"ui stackable grid\">\n            <div class=\"ten wide filter-form column\">\n              <div class=\"field\">\n                <label>Application</label>\n                <select @change=\"getLogs()\" v-model=\"businessEventFilter.application\" :disabled=\"pending\">\n                  <option :value=\"application\" v-for=\"application in businessEventFiltersData.applications\">{{ application }}</option>\n                </select>\n              </div>\n              <div class=\"field\">\n                <label>Domain</label>\n                <select @change=\"getLogs()\" v-model=\"businessEventFilter.domain\" :disabled=\"pending\">\n                  <option value=''>All domains</option>\n                  <option :value=\"domain\" v-for=\"domain in businessEventFiltersData.domains\">{{ domain }}</option>\n                </select>\n              </div>\n              <div class=\"field\">\n                <label>Action</label>\n                <select @change=\"getLogs()\" v-model=\"businessEventFilter.action\" :disabled=\"pending\">\n                  <option value=''>All actions</option>\n                  <option :value=\"action\" v-for=\"action in businessEventFiltersData.actions\">{{ action }}</option>\n                </select>\n              </div>\n            </div>\n            <div class=\"six wide log-count column\">\n              <div class=\"counts\">\n                <label>Log count <span>{{ logCount }}</span></label>\n              </div>\n            </div>\n          </div>\n        </div>\n        <div class=\"ui stackable grid\">\n          <div class=\"ten wide filter-form column\">\n            <LineChart :chartData=\"businessEventCounts\" :options=\"businessEventCountsOptions\" height=\"500\" :key=\"graphRerenders\" />\n          </div>\n          <div class=\"six wide filter-form column\">\n            <div class=\"ui business-event-counts celled list\">\n              <div class=\"item\" v-for=\"(count, label) in counts\" :key=\"label\">\n                <div class=\"content\">\n                  <div class=\"header\">\n                    <span class=\"success\">{{ count.success }}</span>\n                    <span class=\"failure\">{{ count.failure }}</span>\n                  </div>\n                  <span class=\"count\">{{ label }}</span>\n                </div>\n              </div>\n            </div>\n          </div>\n          <div v-if=\"businessEventFilter.application == 'boruta_gateway'\" class=\"sixteen wide filter-form column\">\n            <LineChart :chartData=\"gatewayTimes\" :options=\"gatewayTimesOptions\" height=\"500\" :key=\"graphRerenders\" />\n          </div>\n        </div>\n\n        <h3>Log trail</h3>\n        <div class=\"ui error message\" v-if=\"overflow\">\n          This interface is limited to read at most {{ maxLogLines }} log lines, later log lines are skipped.\n        </div>\n        <div class=\"ui logs segment\">\n          <pre>{{ businessEventLogs.join('\\n') }}</pre>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { uniq } from 'lodash'\nimport moment from 'moment'\nimport { LineChart } from \"vue-chart-3\"\nimport { Chart, registerables } from 'chart.js'\nimport BusinessLogStats from '../../models/business-log-stats.model.js'\nimport 'chartjs-adapter-moment'\n\nChart.register(...registerables)\n\nconst MAX_LOG_LINES = 10000 // from backend limit\n\nexport default {\n  name: 'business-events',\n  components: {\n    LineChart\n  },\n  data() {\n    const requestDataset = {\n       label: 'request time',\n       borderColor: stringToColor('request time'),\n       backgroundColor: stringToColor('request time'),\n       fill: false,\n       linetension: 0,\n       data: [],\n       tmp: {}\n    }\n    const upstreamDataset = {\n       label: 'upstream time',\n       borderColor: stringToColor('upstream time'),\n       backgroundColor: stringToColor('upstream time'),\n       fill: false,\n       linetension: 0,\n       data: [],\n       tmp: []\n    }\n    const gatewayDataset = {\n       label: 'gateway time',\n       borderColor: stringToColor('gateway time'),\n       backgroundColor: stringToColor('gateway time'),\n       fill: false,\n       linetension: 0,\n       data: [],\n       tmp: []\n    }\n\n    return {\n      overflow: false,\n      pending: false,\n      error: false,\n      maxLogLines: MAX_LOG_LINES,\n      timeScaleUnit: '',\n      businessEventLogs: [],\n      logCount: 0,\n      graphRerenders: 0,\n      businessEventFiltersData: {\n        applications: ['boruta_web', 'boruta_identity', 'boruta_gateway'],\n        domains: [],\n        actions: []\n      },\n      dateFilter: {\n        startAt: this.$route.query.startAt || moment().utc().startOf('hour').format(\"yyyy-MM-DDTHH:mm\"),\n        endAt: this.$route.query.endAt || moment().utc().endOf('hour').format(\"yyyy-MM-DDTHH:mm\")\n      },\n      businessEventFilter: {\n        application: this.$route.query.application || 'boruta_web',\n        domain: this.$route.query.domain || '',\n        action: this.$route.query.action || ''\n      },\n      counts: {},\n      businessEventCounts: {\n        labels: [],\n        datasets: []\n      },\n      gatewayTimes: {\n        labels: [],\n        datasets: [requestDataset, upstreamDataset, gatewayDataset]\n      }\n    }\n  },\n  computed: {\n    businessEventCountsOptions() {\n      return {\n        animation: false,\n        plugins: {\n          title: {\n            display: true,\n            text: `Business event counts per ${this.timeScaleUnit}`\n          },\n          legend: {\n            align: 'start',\n            position: 'bottom'\n          }\n        },\n        scales: {\n          x: {\n            type: 'timeseries',\n            time: {\n              unit: 'hour',\n              round: true\n            }\n          }\n        }\n      }\n    },\n    gatewayTimesOptions() {\n      return {\n        animation: false,\n        plugins: {\n          title: {\n            display: true,\n            text: `Gateway times per ${this.timeScaleUnit}`\n          },\n          legend: {\n            align: 'start',\n            position: 'bottom'\n          }\n        },\n        scales: {\n          x: {\n            type: 'timeseries',\n            time: {\n              unit: 'hour',\n              round: true\n            }\n          }\n        }\n      }\n    }\n  },\n  async mounted() {\n    this.overflow = false\n    this.resetGraphs()\n    this.resetFilters()\n    this.getLogStats()\n    this.render()\n  },\n  methods: {\n    getLogs() {\n      this.applyFilters()\n    },\n    applyFilters() {\n      const query = {\n        ...this.$route.query,\n        ...this.dateFilter,\n        ...this.businessEventFilter\n      }\n\n      this.$router.push({ path: this.$route.path, query })\n    },\n    resetGraphs() {\n      const requestDataset = {\n         label: 'request time',\n         borderColor: stringToColor('request time'),\n         backgroundColor: stringToColor('request time'),\n         fill: false,\n         linetension: 0,\n         data: [],\n         tmp: {}\n      }\n      const upstreamDataset = {\n         label: 'upstream time',\n         borderColor: stringToColor('upstream time'),\n         backgroundColor: stringToColor('upstream time'),\n         fill: false,\n         linetension: 0,\n         data: [],\n         tmp: []\n      }\n      const gatewayDataset = {\n         label: 'gateway time',\n         borderColor: stringToColor('gateway time'),\n         backgroundColor: stringToColor('gateway time'),\n         fill: false,\n         linetension: 0,\n         data: [],\n         tmp: []\n      }\n\n      this.counts = {}\n      this.businessEventCounts = {\n        labels: [],\n        datasets: []\n      }\n      this.gatewayTimes = {\n        labels: [],\n        datasets: [requestDataset, upstreamDataset, gatewayDataset]\n      }\n    },\n    resetFilters() {\n      this.businessEventFiltersData.actions = []\n      this.businessEventFiltersData.domains = []\n      if (!this.businessEventFilter.domain.match(this.businessEventFilter.application)) {\n        this.businessEventFilter.domain = ''\n      }\n      if (!this.businessEventFilter.action.match(this.businessEventFilter.application) ||\n        !this.businessEventFilter.action.match(this.businessEventFilter.domain)) {\n        this.businessEventFilter.action = ''\n      }\n    },\n    getLogStats() {\n      this.pending = true\n      this.error = false\n      BusinessLogStats.all({\n        ...this.businessEventFilter,\n        ...this.dateFilter\n      }).then(({\n        time_scale_unit,\n        overflow,\n        log_lines,\n        log_count,\n        counts,\n        business_event_counts,\n        domains,\n        actions\n      }) => {\n        this.timeScaleUnit = time_scale_unit\n        this.overflow = overflow\n        this.businessEventLogs = log_lines\n        this.logCount = log_count\n        this.populateCounts(counts)\n        this.populateBusinessEventCounts(business_event_counts)\n        this.populateGatewayTimes(log_lines)\n        this.businessEventFiltersData.domains = domains\n        this.businessEventFiltersData.actions = actions\n        this.pending = false\n      }).catch(error => this.error = error.message)\n    },\n    render() {\n      this.graphRerenders += 1\n    },\n    populateCounts(stats) {\n      Object.keys(stats).forEach(currentLabel => {\n        this.counts[currentLabel] = this.counts[currentLabel] || {}\n\n        Object.keys(stats[currentLabel]).forEach(result => {\n          this.counts[currentLabel][result] = stats[currentLabel][result]\n        })\n      })\n    },\n    populateBusinessEventCounts(stats) {\n      Object.keys(stats).forEach(currentLabel => {\n        const labels = uniq(Object.values(stats).flatMap(Object.keys)).sort()\n        this.businessEventCounts.labels = labels\n\n        let dataset = this.businessEventCounts.datasets.find(({ label }) => {\n          return label === currentLabel\n        })\n        if (!dataset) {\n          dataset = {\n            label: currentLabel,\n            borderColor: stringToColor(currentLabel),\n            backgroundColor: stringToColor(currentLabel),\n            fill: false,\n            lineTension: 0,\n            data: []\n          }\n          this.businessEventCounts.datasets.push(dataset)\n        }\n\n        Object.keys(stats[currentLabel]).map(timestamp => {\n          dataset.data[labels.indexOf(timestamp)] = stats[currentLabel][timestamp]\n        })\n\n        this.businessEventCounts.datasets.forEach(dataset => {\n          dataset.data = dataset.data.map(value => value === 0 ? NaN : value)\n        })\n      })\n    },\n    populateGatewayTimes(logLines) {\n      logLines.forEach(line => {\n        if (!(line.match(/boruta_gateway/))) return\n        if (!(line.match(/success/))) return\n\n        let unitFactor\n        if (this.timeScaleUnit == 'second') {\n          unitFactor = 1000\n        } else if (this.timeScaleUnit == 'minute') {\n          unitFactor = 1000 * 60\n        } else if (this.timeScaleUnit == 'hour') {\n          unitFactor = 1000 * 60 * 60\n        }\n        const timestamp = new Date(Math.floor(Date.parse(line.split(' ')[0]).valueOf() / unitFactor) * unitFactor).toString()\n\n        const rawAttributes = line.split(' - ')[1].split(' ')\n        const rawRequestTime = rawAttributes.find(rawAttribute => {\n          return rawAttribute.match(/request_time/)\n        })\n        const requestTime = rawRequestTime && rawRequestTime.match(/\\d+/)[0]\n\n        const rawGatewayTime = rawAttributes.find(rawAttribute => {\n          return rawAttribute.match(/gateway_time/)\n        })\n        const gatewayTime = rawGatewayTime && rawGatewayTime.match(/\\d+/)[0]\n\n        const rawUpstreamTime = rawAttributes.find(rawAttribute => {\n          return rawAttribute.match(/upstream_time/)\n        })\n        const upstreamTime = rawUpstreamTime && rawUpstreamTime.match(/\\d+/)[0]\n\n        const labels = ['Request time', 'Gateway time', 'Upstream time']\n\n        const requestDataset = this.gatewayTimes.datasets[0]\n        const upstreamDataset = this.gatewayTimes.datasets[1]\n        const gatewayDataset = this.gatewayTimes.datasets[2]\n\n        if (requestDataset.tmp[timestamp]) {\n          const lastRequestTime = requestDataset.tmp[timestamp]\n          requestDataset.tmp[timestamp] = (lastRequestTime + parseInt(requestTime) / 1000) / 2\n        } else {\n          requestDataset.tmp[timestamp] = parseInt(requestTime) / 1000\n        }\n        this.gatewayTimes.labels = Object.keys(requestDataset.tmp)\n        requestDataset.data = Object.values(requestDataset.tmp)\n\n        if (gatewayDataset.tmp[timestamp]) {\n          const lastGatewayTime = gatewayDataset.tmp[timestamp]\n          gatewayDataset.tmp[timestamp] = (lastGatewayTime + parseInt(gatewayTime) / 1000) / 2\n        } else {\n          gatewayDataset.tmp[timestamp] = parseInt(gatewayTime) / 1000\n        }\n        gatewayDataset.data = Object.values(gatewayDataset.tmp)\n\n        if (upstreamDataset.tmp[timestamp]) {\n          const lastUpstreamTime = upstreamDataset.tmp[timestamp]\n          upstreamDataset.tmp[timestamp] = (lastUpstreamTime + parseInt(upstreamTime) / 1000) / 2\n        } else {\n          upstreamDataset.tmp[timestamp] = parseInt(upstreamTime) / 1000\n        }\n        upstreamDataset.data = Object.values(upstreamDataset.tmp)\n      })\n    },\n  },\n  watch: {\n    $route(to, from) {\n      if (to.name !== 'business-event-logs') return\n\n      this.dateFilter = {\n        startAt: to.query.startAt || moment().utc().startOf('hour').format(\"yyyy-MM-DDTHH:mm\"),\n        endAt: to.query.endAt || moment().utc().endOf('hour').format(\"yyyy-MM-DDTHH:mm\")\n      }\n\n      this.businessEventFilter = {\n        application: to.query.application || 'boruta_web',\n        domain: to.query.domain || '',\n        action: to.query.action || '',\n      }\n\n      this.overflow = false\n      this.resetGraphs()\n      this.resetFilters()\n      this.getLogStats()\n      this.render()\n    }\n  },\n  beforeRouteLeave() {\n    this.resetGraphs()\n  }\n}\n\nfunction stringToColor(str) {\n  let hash = 0\n  for (let i = 0; i < str.length; i++) {\n    hash = str.charCodeAt(i) + ((hash << 5) - hash)\n  }\n  let colour = '#'\n  for (let i = 0; i < 3; i++) {\n    const value = (hash >> (i * 8)) & 0xFF\n    colour += ('00' + value.toString(16)).substr(-2)\n  }\n  return colour\n}\n</script>\n\n<style scoped lang=\"scss\">\n.dashboard {\n  position: relative;\n  .dates.form {\n    margin-bottom: 1em;\n  }\n  .logs.segment {\n    overflow: hidden;\n    overflow-x: scroll;\n    overflow-y: scroll;\n    max-height: 30vh;\n    pre {\n      overflow: visible !important;\n    }\n  }\n  .dates.form {\n    button {\n      font-size: 1.08rem!important;\n    }\n  }\n  .log-count {\n    display: flex!important;\n    align-items: center;\n    justify-content: center;\n    flex-direction: column;\n    .counts {\n      padding: 1rem;\n      text-align: center;\n    }\n    label {\n      display: block;\n      font-size: 1.3rem;\n      margin: .5rem;\n      span {\n        font-weight: bold;\n        display: block;\n        font-size: 1.5rem;\n      }\n    }\n  }\n  .business-event-counts.list {\n    font-size: 1.2em;\n    .header {\n      float: right;\n      span {\n        margin-right: .5em;\n        &.success {\n          color: green;\n        }\n        &.failure {\n          color: red;\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Dashboard/Requests.vue",
    "content": "<template>\n  <div class=\"dashboard\">\n    <div class=\"container\">\n      <div class=\"ui error message\" v-if=\"error\">\n        {{ error }}\n      </div>\n      <div class=\"ui dates form\">\n        <div class=\"ui stackable grid\">\n          <div class=\"four wide request-times column\">\n            <h1>Requests</h1>\n          </div>\n          <div class=\"five wide request-times column\">\n            <input type=\"datetime-local\" v-model=\"dateFilter.startAt\" :disabled=\"pending\" />\n          </div>\n          <div class=\"five wide request-times column\">\n            <input type=\"datetime-local\" v-model=\"dateFilter.endAt\" :disabled=\"pending\" />\n          </div>\n          <div class=\"two wide request-times column\">\n            <button class=\"ui fluid blue button\" @click=\"getLogs()\" :disabled=\"pending\">Filter</button>\n          </div>\n        </div>\n      </div>\n      <div class=\"ui error message\" v-if=\"fetchError\">\n        An error has occured when fetching logs, message: {{ fetchError }}.\n      </div>\n      <div class=\"ui segment\">\n        <div class=\"ui requests form\">\n          <div class=\"ui stackable grid\">\n            <div class=\"ten wide filter-form column\">\n              <div class=\"field\">\n                <label>Application</label>\n                <select @change=\"getLogs()\" v-model=\"requestsFilter.application\" :disabled=\"pending\">\n                  <option :value=\"application\" v-for=\"application in requestsFiltersData.applications\">{{ application }}</option>\n                </select>\n              </div>\n              <div class=\"field\">\n                <label>Request label</label>\n                <select @change=\"getLogs()\" v-model=\"requestsFilter.label\" :disabled=\"pending\">\n                  <option value=\"\">All request labels</option>\n                  <option :value=\"label\" v-for=\"label in requestsFiltersData.labels\">{{ label }}</option>\n                </select>\n              </div>\n            </div>\n            <div class=\"six wide log-count column\">\n              <div class=\"counts\">\n                <label>Log count <span>{{ logCount }}</span></label>\n              </div>\n            </div>\n          </div>\n        </div>\n        <div class=\"ui stackable grid\">\n          <div class=\"ten wide request-times column\">\n            <LineChart :chartData=\"requestCounts\" :options=\"requestCountsOptions\" height=\"500\" :key=\"graphRerenders\" />\n          </div>\n          <div class=\"six wide status-codes column\">\n            <PieChart :chart-data=\"statusCodes\" :options=\"statusCodesOptions\" :key=\"graphRerenders\" />\n          </div>\n          <div class=\"sixteen wide request-times column\">\n            <LineChart :chartData=\"requestTimes\" :options=\"requestTimesOptions\" height=\"500\" :key=\"graphRerenders\" />\n          </div>\n        </div>\n        <h3>Log trail</h3>\n        <div class=\"ui error message\" v-if=\"overflow\">\n          This interface is limited to read at most {{ maxLogLines }} log lines, later log lines are skipped.\n        </div>\n        <div class=\"ui logs segment\">\n          <pre>{{ requestLogs.join('\\n') }}</pre>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { uniq } from 'lodash'\nimport { LineChart, PieChart } from \"vue-chart-3\"\nimport { Chart, registerables } from 'chart.js'\nimport moment from 'moment'\nimport 'chartjs-adapter-moment'\nimport RequestLogStats from '../../models/request-log-stats.model'\n\nChart.register(...registerables)\n\nconst MAX_LOG_LINES = 10000 // from backend limit\n\nexport default {\n  name: 'requests',\n  components: {\n    LineChart,\n    PieChart\n  },\n  data() {\n    return {\n      overflow: false,\n      pending: false,\n      error: false,\n      maxLogLines: MAX_LOG_LINES,\n      timeScaleUnit: '',\n      requestLogs: [],\n      logCount: 0,\n      graphRerenders: 0,\n      requestsFiltersData: {\n        applications: ['boruta_web', 'boruta_identity', 'boruta_gateway', 'boruta_admin'],\n        labels: []\n      },\n      dateFilter: {\n        startAt: this.$route.query.startAt || moment().utc().startOf('hour').format(\"yyyy-MM-DDTHH:mm\"),\n        endAt: this.$route.query.endAt || moment().utc().endOf('hour').format(\"yyyy-MM-DDTHH:mm\")\n      },\n      requestsFilter: {\n        application: this.$route.query.application || 'boruta_web',\n        label: this.$route.query.label || ''\n      },\n      statusCodes: {\n        labels: [],\n        datasets: []\n      },\n      requestTimes: {\n        labels: [],\n        datasets: []\n      },\n      requestCounts: {\n        labels: [],\n        datasets: []\n      },\n      statusCodesOptions: {\n        animation: false,\n        cutout: '30%',\n        plugins: {\n          title: {\n            display: true,\n            text: 'Status codes'\n          },\n          legend: {\n            display: false\n          }\n        }\n      },\n      requestTimesOptions: {\n        animation: false,\n        plugins: {\n          title: {\n            display: true,\n            text: 'Average request time (ms)'\n          },\n          legend: {\n            align: 'start',\n            position: 'bottom'\n          }\n        },\n        scales: {\n          x: {\n            type: 'timeseries',\n            time: {\n              unit: 'hour',\n              round: true\n            }\n          }\n        }\n      }\n    }\n  },\n  computed: {\n    requestCountsOptions() {\n      return {\n        animation: false,\n        plugins: {\n          title: {\n            display: true,\n            text: `Requests per ${this.timeScaleUnit}`\n          },\n          legend: {\n            align: 'start',\n            position: 'bottom'\n          }\n        },\n        scales: {\n          x: {\n            type: 'timeseries',\n            time: {\n              unit: 'hour',\n              round: true\n            }\n          }\n        }\n      }\n    }\n  },\n  async mounted() {\n    this.overflow = false\n    this.resetGraphs()\n    this.resetFilters()\n    this.getLogStats()\n    this.render()\n  },\n  methods: {\n    getLogs() {\n      this.applyFilters()\n    },\n    applyFilters() {\n      const query = {\n        ...this.$route.query,\n        ...this.dateFilter,\n        ...this.requestsFilter\n      }\n\n      this.$router.push({ path: this.$route.path, query })\n    },\n    resetGraphs() {\n      this.requestCounts = { labels: [], datasets: [] }\n      this.statusCodes = { labels: [], datasets: [] }\n      this.requestTimes = { labels: [], datasets: [] }\n    },\n    resetFilters() {\n      this.requestsFiltersData.labels = []\n      if (!this.requestsFilter.label.match(this.requestsFilter.application)) {\n        this.requestsFilter.label = ''\n      }\n    },\n    getLogStats() {\n      this.pending = true\n      this.error = false\n      RequestLogStats.all({\n        ...this.requestsFilter,\n        ...this.dateFilter\n      }).then(({\n        time_scale_unit,\n        overflow,\n        log_lines,\n        log_count,\n        request_counts,\n        status_codes,\n        request_times,\n        labels\n      }) => {\n        this.timeScaleUnit = time_scale_unit\n        this.overflow = overflow\n        this.requestLogs = log_lines\n        this.logCount = log_count\n        this.requestsFiltersData.labels = labels\n        this.populateRequestCounts(request_counts)\n        this.populateStatusCodes(status_codes)\n        this.populateRequestTimes(request_times)\n        this.pending = false\n      }).catch(error => this.error = error.message)\n    },\n    render() {\n      this.graphRerenders += 1\n    },\n    populateRequestCounts(stats) {\n      Object.keys(stats).forEach(currentLabel => {\n        const labels = uniq(Object.values(stats).flatMap(Object.keys)).sort()\n        this.requestCounts.labels = labels\n\n        let dataset = this.requestCounts.datasets.find(({ label }) => {\n          return label === currentLabel\n        })\n        if (!dataset) {\n          dataset = {\n            label: currentLabel,\n            borderColor: stringToColor(currentLabel),\n            backgroundColor: stringToColor(currentLabel),\n            fill: false,\n            lineTension: 0,\n            data: []\n          }\n          this.requestCounts.datasets.push(dataset)\n        }\n\n        Object.keys(stats[currentLabel]).map(timestamp => {\n          dataset.data[labels.indexOf(timestamp)] = stats[currentLabel][timestamp]\n        })\n\n        this.requestCounts.datasets.forEach(dataset => {\n          dataset.data = dataset.data.map(value => value === 0 ? NaN : value)\n        })\n      })\n    },\n    populateStatusCodes(stats) {\n      const labels = uniq(Object.values(stats).flatMap(Object.keys))\n      this.statusCodes.labels = labels\n\n      Object.keys(stats).forEach(currentLabel => {\n        let dataset = this.statusCodes.datasets.find(({ label }) => {\n          return label === currentLabel\n        })\n        if (!dataset) {\n          dataset = {\n            label: currentLabel,\n            backgroundColor: stringToColor(currentLabel),\n            data: []\n          }\n          this.statusCodes.datasets.push(dataset)\n        }\n\n        Object.keys(stats[currentLabel]).map(statusCode => {\n          dataset.data[labels.indexOf(statusCode)] = stats[currentLabel][statusCode]\n        })\n\n        dataset.data = dataset.data.map(value => value === 0 ? NaN : value)\n      })\n    },\n    populateRequestTimes(stats) {\n      Object.keys(stats).forEach(currentLabel => {\n        const labels = uniq(Object.values(stats).flatMap(Object.keys)).sort()\n        this.requestTimes.labels = labels\n\n        let dataset = this.requestTimes.datasets.find(({ label }) => {\n          return label === currentLabel\n        })\n        if (!dataset) {\n          dataset = {\n            label: currentLabel,\n            borderColor: stringToColor(currentLabel),\n            backgroundColor: stringToColor(currentLabel),\n            fill: false,\n            lineTension: 0,\n            data: []\n          }\n          this.requestTimes.datasets.push(dataset)\n        }\n\n        Object.keys(stats[currentLabel]).map(timestamp => {\n          dataset.data[labels.indexOf(timestamp)] = stats[currentLabel][timestamp]\n        })\n\n        this.requestTimes.datasets.forEach(dataset => {\n          dataset.data = dataset.data.map(value => value === 0 ? NaN : value)\n        })\n      })\n    }\n  },\n  watch: {\n    $route(to, from) {\n      if (to.name !== 'request-logs') return\n\n      this.dateFilter = {\n        startAt: to.query.startAt || moment().utc().startOf('hour').format(\"yyyy-MM-DDTHH:mm\"),\n        endAt: to.query.endAt || moment().utc().endOf('hour').format(\"yyyy-MM-DDTHH:mm\")\n      }\n\n      this.requestsFilter = {\n        application: to.query.application || 'boruta_web',\n        label: to.query.label || ''\n      }\n\n      this.overflow = false\n      this.resetGraphs()\n      this.resetFilters()\n      this.getLogStats()\n      this.render()\n    }\n  },\n  beforeRouteLeave() {\n    this.resetGraphs()\n  }\n}\n\nfunction stringToColor(str) {\n  let hash = 0\n  for (let i = 0; i < str.length; i++) {\n    hash = str.charCodeAt(i) + ((hash << 5) - hash)\n  }\n  let colour = '#'\n  for (let i = 0; i < 3; i++) {\n    const value = (hash >> (i * 8)) & 0xFF\n    colour += ('00' + value.toString(16)).substr(-2)\n  }\n  return colour\n}\n</script>\n\n<style scoped lang=\"scss\">\n.dashboard {\n  position: relative;\n  .dates.form {\n    margin-bottom: 1em;\n  }\n  .logs.segment {\n    overflow: hidden;\n    overflow-x: scroll;\n    overflow-y: scroll;\n    max-height: 30vh;\n    pre {\n      overflow: visible !important;\n    }\n  }\n  .dates.form {\n    button {\n      font-size: 1.08rem!important;\n    }\n  }\n  .status-codes {\n    display: flex!important;\n    align-items: center;\n    justify-content: center;\n  }\n  .log-count {\n    display: flex!important;\n    align-items: center;\n    justify-content: center;\n    flex-direction: column;\n    .counts {\n      padding: 1rem;\n      text-align: center;\n    }\n    label {\n      display: block;\n      font-size: 1.3rem;\n      margin: .5rem;\n      span {\n        font-weight: bold;\n        display: block;\n        font-size: 1.5rem;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Dashboard.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Home.vue",
    "content": "<template>\n  <div class=\"home\">\n    <div class=\"ui container\">\n      <div class=\"ui center aligned segment\">\n        <h2>Welcome to boruta administration</h2>\n        <div class=\"ui three column stackable grid\">\n          <div class=\"column\">\n            <router-link :to=\"{ name: 'dashboard' }\" class=\"ui placeholder segment\">\n              <div class=\"ui icon header\">\n                <i class=\"chart area icon\"></i>\n                Dashboard\n              </div>\n            </router-link>\n          </div>\n          <div class=\"column\">\n            <router-link :to=\"{ name: 'upstreams' }\" class=\"ui placeholder segment\">\n              <div class=\"ui icon header\">\n                <i class=\"server icon\"></i>\n                Upstreams\n              </div>\n            </router-link>\n          </div>\n          <div class=\"column\">\n            <router-link :to=\"{ name: 'clients' }\" class=\"ui placeholder segment\">\n              <div class=\"ui icon header\">\n                <i class=\"certificate icon\"></i>\n                Clients\n              </div>\n            </router-link>\n          </div>\n          <div class=\"column\">\n            <router-link :to=\"{ name: 'identity-providers' }\" class=\"ui placeholder segment\">\n              <div class=\"ui icon header\">\n                <i class=\"users icon\"></i>\n                Identity providers\n              </div>\n            </router-link>\n          </div>\n          <div class=\"column\">\n            <router-link :to=\"{ name: 'scopes' }\" class=\"ui placeholder segment\">\n              <div class=\"ui icon header\">\n                <i class=\"cogs icon\"></i>\n                Scopes\n              </div>\n            </router-link>\n          </div>\n          <div class=\"column\">\n            <router-link :to=\"{ name: 'configuration' }\" class=\"ui placeholder segment\">\n              <div class=\"ui icon header\">\n                <i class=\"columns icon\"></i>\n                Configuration\n              </div>\n            </router-link>\n          </div>\n        </div>\n      </div>\n      <div class=\"ui center aligned segment\">\n        <div class=\"ui segment\">\n          <router-link class=\"ui fluid blue button\" :to=\"{ name: 'configuration-file-upload', params: { type: 'example-configuration-file' } }\">Load example configuration</router-link>\n        </div>\n        <div class=\"ui info message\">\n          <h2>Example decentralized identity flow</h2>\n          <p>Use integrated <a target=\"_blank\" :href=\"walletUrl\">web wallet</a> to request, store, and present verifiable credentials</p>\n          <p>In order to execute example decentralized identity flows, you must generate client did first - <router-link :to=\"{ name: 'edit-client', params: { clientId: '00000000-0000-0000-0000-000000000001' } }\">client configuration</router-link></p>\n          <hr />\n          <div class=\"ui form segment\">\n            <h3>Verifiable credential issuance</h3>\n            <div class=\"field\">\n              <label>Select a wallet</label>\n              <select v-model=\"issuanceRedirectUri\">\n                <option :value=\"walletRedirectUri\">Internal wallet</option>\n                <option value=\"openid-credential-offer://\">Mobile wallet</option>\n              </select>\n            </div>\n            <a class=\"ui fluid blue button\" target=\"_blank\" :href=\"preauthorizeUrl\">Trigger example pre-authorized code flow with associated boruta wallet (load example data first)</a>\n            <hr />\n            <a class=\"ui fluid blue button\" target=\"_blank\" :href=\"presentationPreauthorizeUrl\">Trigger example pre-authorized code with presentation flow with associated boruta wallet (load example data first)</a>\n          </div>\n          <div class=\"ui form segment\">\n            <h3>Verifiable credential presentation</h3>\n            <div class=\"field\">\n              <label>Select a wallet</label>\n              <select v-model=\"presentationRedirectUri\">\n                <option :value=\"walletRedirectUri\">Internal wallet</option>\n                <option value=\"openid4vp://\">Mobile wallet</option>\n              </select>\n            </div>\n            <a class=\"ui fluid blue button\" target=\"_blank\" :href=\"presentationUrl\">Trigger example presentation with associated boruta wallet (issue example credential first)</a>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'home',\n  data () {\n    const walletRedirectUri = new URL('/accounts/wallet', window.env.BORUTA_OAUTH_BASE_URL).toString()\n    return {\n      walletRedirectUri,\n      issuanceRedirectUri: walletRedirectUri,\n      presentationRedirectUri: walletRedirectUri\n    }\n  },\n  computed: {\n    walletUrl () {\n     return window.env.BORUTA_OAUTH_BASE_URL +\n      '/accounts/wallet'\n    },\n    presentationUrl () {\n      if (this.presentationRedirectUri.startsWith('http')) {\n        return window.env.BORUTA_OAUTH_BASE_URL +\n          `/oauth/authorize?client_id=00000000-0000-0000-0000-000000000001&redirect_uri=${this.presentationRedirectUri}&scope=BorutaCredentialJwtVc&response_type=id_token vp_token&client_metadata={}&prompt=login`\n      } else {\n        return window.env.BORUTA_OAUTH_BASE_URL +\n          `/oauth/authorize?client_id=00000000-0000-0000-0000-000000000001&redirect_uri=${this.presentationRedirectUri}&scope=BorutaCredentialJwtVc&response_type=vp_token&client_metadata={}&prompt=login`\n      }\n    },\n    preauthorizeUrl () {\n     return window.env.BORUTA_OAUTH_BASE_URL +\n      `/oauth/authorize?client_id=00000000-0000-0000-0000-000000000001&redirect_uri=${this.presentationRedirectUri}&response_type=id_token urn%3Aietf%3Aparams%3Aoauth%3Aresponse-type%3Apre-authorized_code&client_metadata={}&state=qrm0c4xm&prompt=login`\n    },\n    presentationPreauthorizeUrl () {\n     return window.env.BORUTA_OAUTH_BASE_URL +\n      `/oauth/authorize?client_id=00000000-0000-0000-0000-000000000001&redirect_uri=${this.presentationRedirectUri}&response_type=vp_token urn%3Aietf%3Aparams%3Aoauth%3Aresponse-type%3Apre-authorized_code&client_metadata={}&state=qrm0c4xm&prompt=login&scope=BorutaCredentialJwtVc`\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/BackendList.vue",
    "content": "<template>\n  <div class=\"backend-list\">\n    <Toaster :active=\"deleted\" message=\"backend has been deleted\" type=\"warning\" />\n    <Toaster :active=\"errorMessage\" :message=\"errorMessage\" type=\"error\" />\n    <router-link :to=\"{ name: 'new-backend' }\" class=\"ui violet main create button\">Add a backend</router-link>\n    <div class=\"container\">\n      <div class=\"ui info message\">\n        Backends act as user registries, identity providers are connected to them in order to manage identities.\n      </div>\n      <div class=\"ui three column backends stackable grid\" v-if=\"backends.length\">\n        <div v-for=\"backend in backends\" :key=\"backend.id\" class=\"column\">\n          <FormErrors v-if=\"backend.errors\" :errors=\"backend.errors\" />\n          <div class=\"ui backend highlightable segment\">\n            <div class=\"actions\">\n              <router-link\n                :to=\"{ name: 'edit-backend', params: { backendId: backend.id } }\"\n                class=\"ui tiny blue button\">edit</router-link>\n              <a v-on:click=\"deleteBackend(backend)\" class=\"ui tiny red button\">delete</a>\n            </div>\n            <div class=\"ui attribute list\">\n              <div class=\"item\">\n                <span class=\"header\">Name</span>\n                <span class=\"description\">{{ backend.name }}</span>\n              </div>\n              <div class=\"item\">\n                <span class=\"header\">Backend ID</span>\n                <span class=\"description\">{{ backend.id }}</span>\n              </div>\n              <div class=\"item\">\n                <span class=\"header\">Type</span>\n                <span class=\"description\">{{ backend.type }}</span>\n              </div>\n            </div>\n            <div class=\"ui default label\" v-if=\"backend.is_default\">default</div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Backend from '../../models/backend.model'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'backend-list',\n  components: {\n    Toaster\n  },\n  data () {\n    return {\n      backends: [],\n      errorMessage: false,\n      deleted: false\n    }\n  },\n  mounted () {\n    this.getBackends()\n  },\n  methods: {\n    getBackends () {\n      Backend.all().then((backends) => {\n        this.backends = backends\n      })\n    },\n    deleteBackend (backend) {\n      if (!confirm('Are you sure ?')) return\n      this.errorMessage = false\n      this.deleted = false\n      backend.destroy().then(() => {\n        this.deleted = true\n        this.backends.splice(this.backends.indexOf(backend), 1)\n      }).catch((error) => {\n        this.errorMessage = error.message\n      })\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.backend-list {\n  .backend.segment {\n    padding-bottom: 1.7em;\n    .default.label {\n      position: absolute;\n      bottom: 0;\n      right: 0;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/Backends/Backend.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/Backends/EditConfirmationInstructionsEmailTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-confirmation-instructions-email-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"ui segment\">\n      <h2>Text content</h2>\n      <TextEditor :content=\"txtContent\" @codeUpdate=\"setTxtContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <h2>HTML content</h2>\n      <TextEditor :content=\"htmlContent\" @codeUpdate=\"setHtmlContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-backend', params: { backendId: backend.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport EmailTemplate from '../../../models/email-template.model'\nimport Backend from '../../../models/backend.model'\nimport TextEditor from '../../../components/Forms/TextEditor.vue'\nimport Toaster from '../../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-confirmation-instructions-email-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { backendId } = this.$route.params\n    Backend.get(backendId).then((backend) => {\n      this.backend = backend\n    })\n    EmailTemplate.get(backendId, 'confirmation_instructions').then((template) => {\n      this.template = template\n      this.txtContent = template.txt_content\n      this.htmlContent = template.html_content\n    })\n  },\n  data () {\n    return {\n      txtContent: '',\n      htmlContent: '',\n      backend: new Backend(),\n      template: new EmailTemplate(),\n      success: false\n    }\n  },\n  methods: {\n    setTxtContent(code) {\n      this.template.txt_content = code\n    },\n    setHtmlContent(code) {\n      this.template.html_content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.txtContent = template.txt_content\n          this.htmlContent = template.html_content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/Backends/EditResetPasswordInstructionsEmailTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-reset-password-instructions-email-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"ui segment\">\n      <h2>Text content</h2>\n      <TextEditor :content=\"txtContent\" @codeUpdate=\"setTxtContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <h2>HTML content</h2>\n      <TextEditor :content=\"htmlContent\" @codeUpdate=\"setHtmlContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-backend', params: { backendId: backend.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport EmailTemplate from '../../../models/email-template.model'\nimport Backend from '../../../models/backend.model'\nimport TextEditor from '../../../components/Forms/TextEditor.vue'\nimport Toaster from '../../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-reset-password-instructions-email-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { backendId } = this.$route.params\n    Backend.get(backendId).then((backend) => {\n      this.backend = backend\n    })\n    EmailTemplate.get(backendId, 'reset_password_instructions').then((template) => {\n      this.template = template\n      this.txtContent = template.txt_content\n      this.htmlContent = template.html_content\n    })\n  },\n  data () {\n    return {\n      txtContent: '',\n      htmlContent: '',\n      backend: new Backend(),\n      template: new EmailTemplate(),\n      success: false\n    }\n  },\n  methods: {\n    setTxtContent(code) {\n      this.template.txt_content = code\n    },\n    setHtmlContent(code) {\n      this.template.html_content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.txtContent = template.txt_content\n          this.htmlContent = template.html_content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/Backends/EditTxCodeEmailTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-tx-code-email-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"ui segment\">\n      <h2>Text content</h2>\n      <TextEditor :content=\"txtContent\" @codeUpdate=\"setTxtContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <h2>HTML content</h2>\n      <TextEditor :content=\"htmlContent\" @codeUpdate=\"setHtmlContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-backend', params: { backendId: backend.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport EmailTemplate from '../../../models/email-template.model'\nimport Backend from '../../../models/backend.model'\nimport TextEditor from '../../../components/Forms/TextEditor.vue'\nimport Toaster from '../../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-tx-code-email-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { backendId } = this.$route.params\n    Backend.get(backendId).then((backend) => {\n      this.backend = backend\n    })\n    EmailTemplate.get(backendId, 'tx_code').then((template) => {\n      this.template = template\n      this.txtContent = template.txt_content\n      this.htmlContent = template.html_content\n    })\n  },\n  data () {\n    return {\n      txtContent: '',\n      htmlContent: '',\n      backend: new Backend(),\n      template: new EmailTemplate(),\n      success: false\n    }\n  },\n  methods: {\n    setTxtContent(code) {\n      this.template.txt_content = code\n    },\n    setHtmlContent(code) {\n      this.template.html_content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.txtContent = template.txt_content\n          this.htmlContent = template.html_content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/Backends.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditBackend.vue",
    "content": "<template>\n  <div class=\"edit-backend\">\n    <Toaster :active=\"success\" message=\"backend has been updated\" type=\"success\" />\n    <div class=\"container\">\n      <div class=\"ui stackable grid\">\n        <div class=\"four wide column\">\n          <div class=\"sidebar\">\n            <div class=\"ui segment\">\n              <div class=\"ui attribute list\">\n                <div class=\"item\">\n                  <span class=\"header\">Name</span>\n                  <span class=\"description\">{{ backend.name }}</span>\n                </div>\n                <div class=\"item\">\n                  <span class=\"header\">backend ID</span>\n                  <span class=\"description\">{{ backend.id }}</span>\n                </div>\n                <div class=\"item\">\n                  <span class=\"header\">Type</span>\n                  <span class=\"description\">{{ backend.type }}</span>\n                </div>\n              </div>\n            </div>\n            <router-link :to=\"{ name: 'backend-list' }\" class=\"ui right floated button\">Back</router-link>\n          </div>\n        </div>\n        <div class=\"twelve wide column\">\n          <BackendForm :backend=\"backend\" @submit=\"updateBackend()\" @back=\"back()\" action=\"Update\" />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Backend from '../../models/backend.model'\nimport BackendForm from '../../components/Forms/BackendForm.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-backend',\n  components: {\n    BackendForm,\n    Toaster\n  },\n  mounted () {\n    const { backendId } = this.$route.params\n    Backend.get(backendId).then((backend) => {\n      this.backend = backend\n    })\n  },\n  data () {\n    return {\n      backend: new Backend(),\n      success: false\n    }\n  },\n  methods: {\n    back () {\n      this.$router.push({ name: 'backend-list' })\n    },\n    updateBackend () {\n      this.success = false\n      return this.backend.save().then(() => {\n        this.success = true\n      }).catch()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditCredentialOfferTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-credential-offer-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-credential-offer-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'credential_offer').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-credential-offer-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditCrossDevicePresentationTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-cross-device-presentation-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-cross-device-presentation-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'cross_device_presentation').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-cross-device-presentation-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditEditResetPasswordTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-edit-reset-password-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-edit-reset-password-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'edit_reset_password').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-edit-reset-password-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditEditUserTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-edit-user-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-edit-user-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'edit_user').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-edit-user-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditIdentityProvider.vue",
    "content": "<template>\n  <div class=\"edit-identity-provider\">\n    <Toaster :active=\"success\" message=\"identity provider has been updated\" type=\"success\" />\n    <div class=\"container\">\n      <div class=\"ui stackable grid\">\n        <div class=\"four wide column\">\n          <div class=\"sidebar\">\n            <div class=\"ui segment\">\n              <div class=\"ui attribute list\">\n                <div class=\"item\">\n                  <span class=\"header\">Name</span>\n                  <span class=\"description\">{{ identityProvider.name }}</span>\n                </div>\n                <div class=\"item\">\n                  <span class=\"header\">identity provider ID</span>\n                  <span class=\"description\">{{ identityProvider.id }}</span>\n                </div>\n                <div class=\"item\">\n                  <span class=\"header\">Backend</span>\n                  <span class=\"description\"><router-link :to=\"{ name: 'edit-backend', params: { backendId: identityProvider.backend.id } }\">{{ identityProvider.backend.name }}</router-link></span>\n                </div>\n              </div>\n            </div>\n            <router-link :to=\"{ name: 'identity-provider-list' }\" class=\"ui right floated button\">Back</router-link>\n          </div>\n        </div>\n        <div class=\"twelve wide column\">\n          <IdentityProviderForm :identityProvider=\"identityProvider\" @submit=\"updateIdentityProvider()\" @back=\"back()\" action=\"Update\" />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport IdentityProvider from '../../models/identity-provider.model'\nimport IdentityProviderForm from '../../components/Forms/IdentityProviderForm.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-identity-provider',\n  components: {\n    IdentityProviderForm,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n  },\n  data () {\n    return {\n      identityProvider: new IdentityProvider(),\n      success: false\n    }\n  },\n  methods: {\n    back () {\n      this.$router.push({ name: 'identity-provider-list' })\n    },\n    updateIdentityProvider () {\n      this.success = false\n      return this.identityProvider.save().then(() => {\n        this.success = true\n      }).catch()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditLayoutTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-layout-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-layout-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'layout').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-layout-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditNewChooseSessionTemplate.vue",
    "content": "<template>\n  <div class=\"container choose-session-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'choose-session-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'choose_session').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.choose-session-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditNewConfirmationTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-new-confirmation-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-new-confirmation-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'new_confirmation_instructions').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-new-confirmation-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditNewConsentTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-consent-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-consent-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'new_consent').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-consent-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditNewResetPasswordTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-new-reset-password-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-new-reset-password-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'new_reset_password').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-new-reset-password-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditOrganization.vue",
    "content": "<template>\n  <div class=\"edit-organization\">\n    <Toaster :active=\"success\" message=\"Organization has been updated\" type=\"success\" />\n    <div class=\"container\">\n      <div class=\"ui stackable grid\">\n        <div class=\"four wide column\">\n          <div class=\"sidebar\">\n            <div class=\"ui segment\">\n              <div class=\"ui attribute list\">\n                <div class=\"item\">\n                  <span class=\"header\">OrganizationId</span>\n                  <span class=\"description\">{{ organization.id }}</span>\n                </div>\n              </div>\n            </div>\n            <router-link :to=\"{ name: 'organization-list' }\" class=\"ui right floated button\">Back</router-link>\n          </div>\n        </div>\n        <div class=\"twelve wide column\">\n          <OrganizationForm :organization=\"organization\" @submit=\"updateOrganization()\" @back=\"back()\" action=\"Update\" />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Organization from '../../models/organization.model'\nimport OrganizationForm from '../../components/Forms/OrganizationForm.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-organization',\n  components: {\n    OrganizationForm,\n    Toaster\n  },\n  mounted() {\n    const { organizationId } = this.$route.params\n    Organization.get(organizationId).then((organization) => {\n      this.organization = organization\n    })\n  },\n  data() {\n    return {\n      organization: new Organization(),\n      success: false\n    }\n  },\n  methods: {\n    back() {\n      this.$router.push({ name: 'organization-list' })\n    },\n    async updateOrganization () {\n      this.success = false\n      return this.organization.save().then(() => {\n        this.success = true\n      }).catch()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditRegistrationTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-registration-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-registration-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'new_registration').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-registration-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditSessionTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-session-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-session-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'new_session').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-session-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditTotpAuthenticationTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-totp-authentication-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-totp-authentication-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'new_totp_authentication').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-totp-authentication-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditTotpRegistrationTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-totp-registration-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-totp-registration-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'new_totp_registration').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-totp-registration-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditUser.vue",
    "content": "<template>\n  <div class=\"edit-user\">\n    <Toaster :active=\"success\" message=\"User has been updated\" type=\"success\" />\n    <div class=\"container\">\n      <div class=\"ui stackable grid\">\n        <div class=\"four wide column\">\n          <div class=\"sidebar\">\n            <div class=\"ui segment\">\n              <div class=\"ui attribute list\">\n                <div class=\"item\">\n                  <span class=\"header\">Backend</span>\n                  <span class=\"description\">{{ user.backend.name }}</span>\n                </div>\n                <div class=\"item\">\n                  <span class=\"header\">User ID</span>\n                  <span class=\"description\">{{ user.id }}</span>\n                </div>\n                <div class=\"item\">\n                  <span class=\"header\">User UID</span>\n                  <span class=\"description\">{{ user.uid }}</span>\n                </div>\n                <div class=\"item\">\n                  <span class=\"header\">Email</span>\n                  <span class=\"description\">{{ user.email }}</span>\n                </div>\n              </div>\n            </div>\n            <div class=\"ui segment\" v-for=\"(attributes, federatedServerName) in user.federated_metadata\">\n              <h2>Federated attributes - {{ federatedServerName }}</h2>\n              <div class=\"ui attribute list\">\n                <div class=\"item\" v-for=\"(value, name) in attributes\">\n                  <span class=\"header\">{{ name }}</span>\n                  <span class=\"description\">{{ value }}</span>\n                </div>\n              </div>\n            </div>\n            <router-link :to=\"{ name: 'user-list' }\" class=\"ui right floated button\">Back</router-link>\n          </div>\n        </div>\n        <div class=\"twelve wide column\">\n          <UserForm :user=\"user\" @submit=\"updateUser()\" @back=\"back()\" action=\"Update\" />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport User from '../../models/user.model'\nimport UserForm from '../../components/Forms/UserForm.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-user',\n  components: {\n    UserForm,\n    Toaster\n  },\n  mounted() {\n    const { userId } = this.$route.params\n    User.get(userId).then((user) => {\n      this.user = user\n    })\n  },\n  data() {\n    return {\n      user: new User(),\n      success: false\n    }\n  },\n  methods: {\n    back() {\n      this.$router.push({ name: 'user-list' })\n    },\n    async updateUser () {\n      this.success = false\n      return this.user.save().then(() => {\n        this.success = true\n      }).catch()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditWebauthnAuthenticationTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-webauthn-authentication-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-webauthn-authentication-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'new_webauthn_authentication').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-webauthn-authentication-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/EditWebauthnRegistrationTemplate.vue",
    "content": "<template>\n  <div class=\"container edit-webauthn-registration-template\">\n    <Toaster :active=\"success\" message=\"Template has been updated\" type=\"success\" />\n    <div class=\"field\">\n      <TextEditor :content=\"content\" @codeUpdate=\"setContent\" />\n    </div>\n    <div class=\"ui segment\">\n      <button v-on:click=\"update()\" class=\"ui violet right floated button\">Save</button>\n      <button v-if=\"template.id\" v-on:click=\"destroy()\" class=\"ui red right floated button\">Reset</button>\n      <router-link :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\" class=\"ui blue button\">Back</router-link>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Template from '../../models/template.model'\nimport IdentityProvider from '../../models/identity-provider.model'\nimport TextEditor from '../../components/Forms/TextEditor.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'edit-webauthn-registration-template',\n  components: {\n    TextEditor,\n    Toaster\n  },\n  mounted () {\n    const { identityProviderId } = this.$route.params\n    IdentityProvider.get(identityProviderId).then((identityProvider) => {\n      this.identityProvider = identityProvider\n    })\n    Template.get(identityProviderId, 'new_webauthn_registration').then((template) => {\n      this.template = template\n      this.content = template.content\n    })\n  },\n  data () {\n    return {\n      content: '',\n      identityProvider: new IdentityProvider(),\n      template: new Template(),\n      success: false\n    }\n  },\n  methods: {\n    setContent(code) {\n      this.template.content = code\n    },\n    update () {\n      this.success = false\n      this.template.save().then(() => {\n        this.success = true\n      })\n    },\n    destroy () {\n      if (confirm('Are you sure you want to reset the template?')) {\n        this.template.destroy().then((template) => {\n          this.template = template\n          this.content = template.content\n        })\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.edit-webauthn-registration-template {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  .field {\n    flex: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/IdentityProvider.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/IdentityProviderList.vue",
    "content": "<template>\n  <div class=\"identityProvider-list\">\n    <Toaster :active=\"deleted\" message=\"identity provider has been deleted\" type=\"warning\" />\n    <Toaster :active=\"errorMessage\" :message=\"errorMessage\" type=\"error\" />\n    <router-link :to=\"{ name: 'new-identity-provider' }\" class=\"ui violet main create button\">Add a identity provider</router-link>\n    <div class=\"container\">\n      <div class=\"ui info message\">\n        Identity providers are here the pages the users will navigate to while authenticating, managing of their identity or security.\n      </div>\n      <div class=\"ui three column identityProviders stackable grid\" v-if=\"identityProviders.length\">\n        <div v-for=\"identityProvider in identityProviders\" :key=\"identityProvider.id\" class=\"column\">\n          <FormErrors v-if=\"identityProvider.errors\" :errors=\"identityProvider.errors\" />\n          <div class=\"ui identityProvider highlightable segment\">\n            <div class=\"actions\">\n              <router-link\n                :to=\"{ name: 'edit-identity-provider', params: { identityProviderId: identityProvider.id } }\"\n                class=\"ui tiny blue button\">edit</router-link>\n              <a v-on:click=\"deleteIdentityProvider(identityProvider)\" class=\"ui tiny red button\">delete</a>\n            </div>\n            <div class=\"ui attribute list\">\n              <div class=\"item\">\n                <span class=\"header\">Name</span>\n                <span class=\"description\">{{ identityProvider.name }}</span>\n              </div>\n              <div class=\"item\">\n                <span class=\"header\">IdentityProvider ID</span>\n                <span class=\"description\">{{ identityProvider.id }}</span>\n              </div>\n              <div class=\"item\">\n                <span class=\"header\">Backend</span>\n                <span class=\"description\"><router-link :to=\"{ name: 'edit-backend', params: { backendId: identityProvider.backend.id } }\">{{ identityProvider.backend.name }}</router-link></span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport IdentityProvider from '../../models/identity-provider.model'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'identity-provider-list',\n  components: {\n    Toaster\n  },\n  data () {\n    return {\n      identityProviders: [],\n      errorMessage: false,\n      deleted: false\n    }\n  },\n  mounted () {\n    this.getIdentityProviders()\n  },\n  methods: {\n    getIdentityProviders () {\n      IdentityProvider.all().then((identityProviders) => {\n        this.identityProviders = identityProviders\n      })\n    },\n    deleteIdentityProvider (identityProvider) {\n      if (!confirm('Are you sure ?')) return\n      this.errorMessage = false\n      this.deleted = false\n      identityProvider.destroy().then(() => {\n        this.deleted = true\n        this.identityProviders.splice(this.identityProviders.indexOf(identityProvider), 1)\n      }).catch((error) => {\n        this.errorMessage = error.message\n      })\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/NewBackend.vue",
    "content": "<template>\n  <div class=\"new-backend\">\n    <div class=\"ui container\">\n      <BackendForm :backend=\"backend\" @submit=\"createBackend()\" @back=\"back()\" action=\"Create\" />\n    </div>\n  </div>\n</template>\n\n<script>\nimport Backend from '../../models/backend.model'\nimport BackendForm from '../../components/Forms/BackendForm.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'new-backend',\n  components: {\n    BackendForm,\n    Toaster\n  },\n  data () {\n    return {\n      backend: new Backend(),\n    }\n  },\n  methods: {\n    back () {\n      this.$router.push({ name: 'backend-list' })\n    },\n    createBackend () {\n      return this.backend.save().then(({ id }) => {\n        this.$router.push({ name: 'edit-backend', params: { backendId: id } })\n      }).catch()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/NewIdentityProvider.vue",
    "content": "<template>\n  <div class=\"new-identity-provider\">\n    <div class=\"ui container\">\n      <IdentityProviderForm :identityProvider=\"identityProvider\" @submit=\"createIdentityProvider()\" @back=\"back()\" action=\"Create\" />\n    </div>\n  </div>\n</template>\n\n<script>\nimport IdentityProvider from '../../models/identity-provider.model'\nimport IdentityProviderForm from '../../components/Forms/IdentityProviderForm.vue'\n\nexport default {\n  name: 'new-identity-provider',\n  components: {\n    IdentityProviderForm\n  },\n  data () {\n    return {\n      identityProvider: new IdentityProvider()\n    }\n  },\n  methods: {\n    back () {\n      this.$router.push({ name: 'identity-provider-list' })\n    },\n    createIdentityProvider () {\n      return this.identityProvider.save().then(({ id }) => {\n        this.$router.push({ name: 'edit-identity-provider', params: { identityProviderId: id } })\n      }).catch()\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.new-identity-provider {\n  .field {\n    position: relative;\n    &.identity-providers input {\n      margin-right: 3em;\n    }\n  }\n  .ui.icon.input>i.icon.close {\n    cursor: pointer;\n    pointer-events: all;\n  }\n  .authorized-scopes-select {\n    margin-right: 3em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/NewOrganization.vue",
    "content": "<template>\n  <div class=\"new-organization\">\n    <Toaster :active=\"success\" message=\"Organization has been created\" type=\"success\" />\n    <div class=\"ui container\">\n      <OrganizationForm :organization=\"organization\" @submit=\"createOrganization()\" @back=\"back()\" action=\"Create\" />\n    </div>\n  </div>\n</template>\n\n<script>\nimport Organization from '../../models/organization.model'\nimport OrganizationForm from '../../components/Forms/OrganizationForm.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'new-organization',\n  components: {\n    OrganizationForm,\n    Toaster\n  },\n  data () {\n    return {\n      organization: new Organization(),\n      success: false\n    }\n  },\n  methods: {\n    back () {\n      this.$router.push({ name: 'organization-list' })\n    },\n    createOrganization () {\n      this.success = false\n      return this.organization.save().then(() => {\n        this.success = true\n        this.$router.push({ name: 'organization-list' })\n      }).catch()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/NewUser.vue",
    "content": "<template>\n  <div class=\"new-user\">\n    <div class=\"container\">\n      <div class=\"ui stackable grid\">\n        <div class=\"four wide column\">\n          <div class=\"sidebar\">\n            <router-link :to=\"{ name: 'user-list' }\" class=\"ui right floated button\">Back</router-link>\n          </div>\n        </div>\n        <div class=\"twelve wide column\">\n          <UserForm :user=\"user\" @submit=\"createUser()\" @back=\"back()\" action=\"Create\" />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport User from '../../models/user.model'\nimport UserForm from '../../components/Forms/UserForm.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'new-user',\n  components: {\n    UserForm,\n    Toaster\n  },\n  data () {\n    return {\n      user: new User(),\n      success: false\n    }\n  },\n  methods: {\n    back () {\n      this.$router.push({ name: 'user-list' })\n    },\n    createUser () {\n      this.success = false\n      return this.user.save().then(() => {\n        this.success = true\n        this.$router.push({ name: 'edit-user', params: { userId: this.user.id } })\n      }).catch()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/OrganizationList.vue",
    "content": "<template>\n  <div class=\"organization-list\">\n    <Toaster :active=\"deleted\" message=\"Organization has been deleted\" type=\"warning\" />\n    <Toaster :active=\"errorMessage\" :message=\"errorMessage\" type=\"error\" />\n    <div class=\"main buttons\">\n      <router-link :to=\"{ name: 'new-organization' }\" class=\"ui violet main create button\">Add a organization</router-link>\n    </div>\n    <div class=\"container\">\n      <div class=\"ui info message\">\n        Organizations have many users as members. They help to group them and provide belongship.\n      </div>\n      <div class=\"ui three column stackable organizations grid\" v-if=\"organizations.length\">\n        <div v-for=\"organization in organizations\" class=\"column\" :key=\"organization.id\">\n          <div class=\"ui organization highlightable segment\">\n            <div class=\"actions\">\n              <router-link\n                :to=\"{ name: 'edit-organization', params: { organizationId: organization.id } }\"\n                class=\"ui tiny blue button\">edit</router-link>\n              <a v-on:click=\"deleteOrganization(organization)\" class=\"ui tiny red button\">delete</a>\n            </div>\n            <div class=\"ui attribute list\">\n              <div class=\"item\">\n                <span class=\"header\">OrganizationId</span>\n                <span class=\"description\">{{ organization.id }}</span>\n              </div>\n              <div class=\"item\">\n                <span class=\"header\">Name</span>\n                <span class=\"description\">{{ organization.name }}</span>\n              </div>\n              <div class=\"item\" v-if=\"organization.label\">\n                <span class=\"header\">Label</span>\n                <span class=\"description\">{{ organization.label }}</span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <hr />\n      <div class=\"ui center aligned segment\">\n        <div class=\"total-entries\">{{ totalEntries }} record(s)</div>\n        <div class=\"ui pagination menu\">\n          <button\n            :disabled=\"disableFirstPage\"\n            class=\"item\"\n            @click=\"goToPage(1)\">\n            &lt;\n          </button>\n          <button\n            class=\"item\"\n            :class=\"{ 'active': currentPage == pageNumber }\"\n            v-for=\"pageNumber in meanPages\"\n            :key=\"pageNumber\"\n            @click=\"goToPage(pageNumber)\">\n            {{ pageNumber }}\n          </button>\n          <button\n            :disabled=\"disableLastPage\"\n            class=\"item\"\n            @click=\"goToPage(this.totalPages)\">\n            &gt;\n          </button>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { throttle } from 'lodash'\nimport Organization from '../../models/organization.model'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'organization-list',\n  components: {\n    Toaster\n  },\n  data () {\n    return {\n      organizations: [],\n      deleted: false,\n      errorMessage: false,\n      currentPage: this.$route.query.page,\n      totalPages: 1,\n      totalEntries: 0,\n      total_entries: 0\n    }\n  },\n  computed: {\n    throttledSearch () {\n      return throttle(this.search, 500)\n    },\n    meanPages () {\n      const meanPages = []\n\n      let firstPage = this.currentPage - 1\n      if (firstPage < 1) firstPage = 1\n      let lastPage = firstPage + 2\n      if (lastPage > this.totalPages) {\n        lastPage = this.totalPages\n        firstPage = lastPage - 2 < 1 ? 1 : lastPage - 2\n      }\n\n      for (let i = firstPage; i <= lastPage; i++) { meanPages.push(i) }\n\n      return meanPages\n    },\n    disableFirstPage () {\n      return this.meanPages[0] == 1\n    },\n    disableLastPage () {\n      return this.meanPages.slice(-1) == this.totalPages\n    }\n  },\n  methods: {\n    getOrganizations (pageNumber, query) {\n      Organization.all({ query, pageNumber }).then(({ data, currentPage, totalPages, totalEntries }) => {\n        this.organizations = data\n        this.totalPages = totalPages\n        this.totalEntries = totalEntries\n        this.currentPage = currentPage\n      })\n    },\n    goToPage (pageNumber) {\n      const query = { page: pageNumber }\n      if (this.organizationQuery) query.q = this.organizationQuery\n\n      this.$router.push({ name: 'organization-list', query })\n    },\n    deleteOrganization (organization) {\n      if (!confirm('Are you sure ?')) return\n      this.errorMessage = false\n      this.deleted = false\n      organization.destroy().then(() => {\n        this.getOrganizations(this.currentPage, this.organizationQuery)\n      }).catch((error) => {\n        this.errorMessage = error.response.data.message\n      })\n    }\n  },\n  watch: {\n    '$route.query': {\n      handler ({ page, q }) {\n        this.getOrganizations(page, q)\n      },\n      deep: true,\n      immediate: true\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.organizations.grid {\n  margin-bottom: 1em!important;\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/Organizations.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/UserImport.vue",
    "content": "<template>\n  <div class=\"user-import\">\n    <div class=\"container\">\n      <div class=\"ui segment\">\n        <FormErrors :errors=\"importErrors\" v-if=\"importErrors\" />\n        <form class=\"ui form\" @submit.prevent=\"upload()\">\n          <h3>Base fields</h3>\n          <div class=\"field\">\n            <label>username header</label>\n            <input type=\"text\" v-model=\"options.usernameHeader\" placeholder=\"username\" />\n          </div>\n          <div class=\"field\">\n            <label>password header</label>\n            <input type=\"text\" v-model=\"options.passwordHeader\" placeholder=\"password\" />\n          </div>\n          <div class=\"field\">\n            <div class=\"ui toggle checkbox\">\n              <input type=\"checkbox\" v-model=\"options.hashPassword\">\n              <label>Hash password</label>\n            </div>\n          </div>\n          <h3>Metadata fields</h3>\n          <div class=\"field\">\n            <div v-for=\"header in options.metadataHeaders\" class=\"metadata-header\">\n              <div class=\"field\">\n                <label>metadata header</label>\n                <input type=\"text\" v-model=\"header.origin\" placeholder=\"origin\" />\n              </div>\n              <div class=\"field\">\n                <label>metadata field</label>\n                <input type=\"text\" v-model=\"header.target\" placeholder=\"target\" />\n              </div>\n              <hr />\n            </div>\n            <a class=\"ui blue fluid button\" @click=\"addMetadataHeader()\">Add metadataHeader</a>\n          </div>\n          <div class=\"field\">\n            <label>Backend</label>\n            <select v-model=\"backendId\">\n              <option :value=\"backend.id\" v-for=\"backend in backends\" :key=\"backend.id\">{{ backend.name }}</option>\n            </select>\n          </div>\n          <div class=\"field\">\n            <label>CSV file</label>\n            <input type=\"file\" @change=\"setFile\" accept=\".csv\" :key=\"fileUpdates\" />\n          </div>\n          <hr />\n          <button class=\"ui right floated violet button\" type=\"submit\">upload</button>\n          <router-link :to=\"{ name: 'user-list' }\" class=\"ui button\">Back</router-link>\n        </form>\n      </div>\n      <div class=\"ui center aligned loading segment\" v-if=\"pending\">\n        <h2>Processing request, please wait and do not leave the page...</h2>\n      </div>\n      <div class=\"ui segment\" v-if=\"!pending && importResult\">\n        <h2>Import results</h2>\n        <div class=\"ui stackable grid\">\n          <div class=\"ten wide filter-form column\">\n            <div class=\"import-errors\" v-if=\"importResult.errors.length\">\n              <div class=\"ui error message\" v-for=\"error in importResult.errors\">\n                <strong>Line {{ error.line }}</strong>\n                <ul class=\"list\">\n                  <li v-for=\"key in Object.keys(error.changeset)\" :key=\"key\">\n                    <strong>{{ key }} </strong> {{ error.changeset[key][0] }}\n                  </li>\n                </ul>\n              </div>\n            </div>\n            <div class=\"ui placeholder success message segment\" v-else>\n              <div class=\"ui icon header\">\n                <i class=\"ui large check circle icon\"></i> All clear.\n              </div>\n            </div>\n          </div>\n          <div class=\"six wide import-counts column\">\n            <div class=\"counts\">\n              <label>Success count <span>{{ importResult.success_count }}</span></label>\n              <label>Error count <span>{{ importResult.error_count }}</span></label>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport User from '../../models/user.model'\nimport Backend from '../../models/backend.model'\nimport Toaster from '../../components/Toaster.vue'\nimport FormErrors from '../../components/Forms/FormErrors.vue'\n\nexport default {\n  name: 'user-import',\n  components: {\n    FormErrors\n  },\n  data () {\n    return {\n      fileUpdates: 0,\n      file: null,\n      options: {metadataHeaders: []},\n      backends: [],\n      backendId: null,\n      pending: false,\n      importResult: null,\n      importErrors: null\n    }\n  },\n  mounted () {\n    Backend.all().then((backends) => {\n      this.backends = backends\n    })\n  },\n  methods: {\n    setFile (event) {\n      this.file = event.target.files[0]\n    },\n    addMetadataHeader () {\n      this.options.metadataHeaders.push({})\n    },\n    upload () {\n      const { backendId, file, options } = this\n\n      this.pending = true\n      User.upload({ backendId, file, options }).then(result => {\n        this.fileUpdates++\n        this.pending = false\n        this.importResult = result\n      }).catch((errors) => {\n        this.fileUpdates++\n        this.pending = false\n        this.importErrors = errors\n      })\n    }\n  },\n  watch: {\n    fileUpdates() {\n      this.file = null\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.loading.segment {\n  height: 24.35em;\n  color: inherit!important;\n}\n.import-counts {\n  display: flex!important;\n  align-items: center;\n  justify-content: center;\n  flex-direction: column;\n  .counts {\n    padding: 1rem;\n    text-align: center;\n  }\n  label {\n    display: block;\n    font-size: 1.3rem;\n    margin: .5rem;\n    span {\n      font-weight: bold;\n      display: block;\n      font-size: 1.5rem;\n    }\n  }\n}\n.import-errors {\n  max-height: 31em;\n  overflow: hidden;\n  overflow-y: scroll;\n  padding-right: 1em;\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/UserList.vue",
    "content": "<template>\n  <div class=\"user-list\">\n    <Toaster :active=\"deleted\" message=\"User has been deleted\" type=\"warning\" />\n    <Toaster :active=\"errorMessage\" :message=\"errorMessage\" type=\"error\" />\n    <div class=\"main buttons\">\n      <router-link :to=\"{ name: 'user-import' }\" class=\"ui violet main create button\">Import users</router-link>\n      <router-link :to=\"{ name: 'new-user' }\" class=\"ui violet main create button\">Add a user</router-link>\n    </div>\n    <div class=\"container\">\n      <div class=\"ui info message\">\n        Users are here the ones that can login to Boruta mirroring the backend to give the ability for the server to add security traits (confirmation, consent, or scope access).\n      </div>\n      <form class=\"ui form\" @submit.prevent=\"throttledSearch()\">\n        <div class=\"field\">\n          <input type=\"text\" v-model=\"userQuery\" @keyup=\"throttledSearch()\" placeholder=\"search\" />\n        </div>\n      </form>\n      <hr />\n      <div class=\"ui three column stackable users grid\" v-if=\"users.length\">\n        <div v-for=\"user in users\" class=\"column\" :key=\"user.id\">\n          <div class=\"ui user highlightable segment\">\n            <div class=\"actions\">\n              <router-link\n                :to=\"{ name: 'edit-user', params: { userId: user.id } }\"\n                class=\"ui tiny blue button\">edit</router-link>\n              <a v-on:click=\"deleteUser(user)\" class=\"ui tiny red button\">delete</a>\n            </div>\n            <div class=\"ui attribute list\">\n              <div class=\"item\">\n                <span class=\"header\">Email</span>\n                <span class=\"description\">{{ user.email }}</span>\n                <span class=\"ui mini basic violet label\" v-if=\"user.totp_registered_at\">MFA</span>\n              </div>\n              <div class=\"item\" v-if=\"user.roles.length\">\n                <span class=\"header\">Roles</span>\n                <span class=\"roles description\">\n                  <span v-for=\"role in user.roles\" :key=\"role.id\">{{ role.model.name }}</span>\n                </span>\n              </div>\n              <div class=\"item\" v-if=\"user.organizations.length\">\n                <span class=\"header\">Organizations</span>\n                <span class=\"organizations description\">\n                  <span v-for=\"organization in user.organizations\" :key=\"organization.id\">{{ organization.model.name }}</span>\n                </span>\n              </div>\n              <div class=\"item\">\n                <span class=\"header\">Backend</span>\n                <span class=\"description\"><router-link :to=\"{ name: 'edit-backend', params: { backendId: user.backend.id } }\">{{ user.backend.name }}</router-link></span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <hr />\n      <div class=\"ui center aligned segment\">\n        <div class=\"total-entries\">{{ totalEntries }} record(s)</div>\n        <div class=\"ui pagination menu\">\n          <button\n            :disabled=\"disableFirstPage\"\n            class=\"item\"\n            @click=\"goToPage(1)\">\n            &lt;\n          </button>\n          <button\n            class=\"item\"\n            :class=\"{ 'active': currentPage == pageNumber }\"\n            v-for=\"pageNumber in meanPages\"\n            :key=\"pageNumber\"\n            @click=\"goToPage(pageNumber)\">\n            {{ pageNumber }}\n          </button>\n          <button\n            :disabled=\"disableLastPage\"\n            class=\"item\"\n            @click=\"goToPage(this.totalPages)\">\n            &gt;\n          </button>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { throttle } from 'lodash'\nimport User from '../../models/user.model'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'user-list',\n  components: {\n    Toaster\n  },\n  data () {\n    return {\n      users: [],\n      deleted: false,\n      errorMessage: false,\n      currentPage: this.$route.query.page,\n      userQuery: this.$route.query.q,\n      totalPages: 1,\n      totalEntries: 0,\n      total_entries: 0\n    }\n  },\n  computed: {\n    throttledSearch () {\n      return throttle(this.search, 500)\n    },\n    meanPages () {\n      const meanPages = []\n\n      let firstPage = this.currentPage - 1\n      if (firstPage < 1) firstPage = 1\n      let lastPage = firstPage + 2\n      if (lastPage > this.totalPages) {\n        lastPage = this.totalPages\n        firstPage = lastPage - 2 < 1 ? 1 : lastPage - 2\n      }\n\n      for (let i = firstPage; i <= lastPage; i++) { meanPages.push(i) }\n\n      return meanPages\n    },\n    disableFirstPage () {\n      return this.meanPages[0] == 1\n    },\n    disableLastPage () {\n      return this.meanPages.slice(-1) == this.totalPages\n    }\n  },\n  methods: {\n    getUsers (pageNumber, query) {\n      User.all({ query, pageNumber }).then(({ data, currentPage, totalPages, totalEntries }) => {\n        this.users = data\n        this.totalPages = totalPages\n        this.totalEntries = totalEntries\n        this.currentPage = currentPage\n      })\n    },\n    goToPage (pageNumber) {\n      const query = { page: pageNumber }\n      if (this.userQuery) query.q = this.userQuery\n\n      this.$router.push({ name: 'user-list', query })\n    },\n    search () {\n      const { userQuery } = this\n\n      this.$router.push({ name: 'user-list', query: { q: userQuery } })\n    },\n    deleteUser (user) {\n      if (!confirm('Are you sure ?')) return\n      this.errorMessage = false\n      this.deleted = false\n      user.destroy().then(() => {\n        this.getUsers(this.currentPage, this.userQuery)\n      }).catch((error) => {\n        this.errorMessage = error.response.data.message\n      })\n    }\n  },\n  watch: {\n    '$route.query': {\n      handler ({ page, q }) {\n        this.getUsers(page, q)\n      },\n      deep: true,\n      immediate: true\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.users.grid {\n  margin-bottom: 1em!important;\n}\n.roles.description {\n  span {\n    display: block;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders/Users.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/IdentityProviders.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Layouts/Main.vue",
    "content": "<template>\n  <div id=\"app\" :class=\"{ 'dark': currentMode }\">\n    <Header ref=\"header\" :darkMode=\"currentMode\" />\n    <div id=\"main\" ref=\"main\">\n      <div class=\"sidebar-menu\" ref=\"menu\">\n        <i class=\"ui large burger bars icon\" @click=\"toggleMenu()\"></i>\n        <i class=\"ui large burger close icon\" @click=\"toggleMenu()\"></i>\n        <div class=\"ui vertical fluid tabular menu\" :class=\"{ 'inverted': currentMode }\">\n          <router-link\n            v-slot=\"{ href, route, navigate, isActive, isExactActive }\"\n            :to=\"{ name: 'dashboard' }\">\n            <div class=\"dashboard item\" :class=\"{'active': isActive }\">\n              <a :href=\"href\" @click=\"navigate\">\n                <i class=\"chart area icon\"></i>\n                <span>Dashboard</span>\n              </a>\n              <div class=\"dropdown\">\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'request-logs' }\">\n                    <span>Requests</span>\n                  </router-link>\n                </div>\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'business-event-logs' }\">\n                    <span>Business events</span>\n                  </router-link>\n                </div>\n              </div>\n            </div>\n          </router-link>\n          <router-link\n            v-slot=\"{ href, route, navigate, isActive, isExactActive }\"\n            :to=\"{ name: 'upstreams' }\">\n            <div class=\"upstreams item\" :class=\"{'active': isActive }\">\n              <a :href=\"href\" @click=\"navigate\">\n                <i class=\"server icon\"></i>\n                <span>Upstreams</span>\n              </a>\n            </div>\n          </router-link>\n          <router-link\n            v-slot=\"{ href, route, navigate, isActive, isExactActive }\"\n            :to=\"{ name: 'clients' }\">\n            <div class=\"clients item\" :class=\"{'active': isActive }\">\n              <a :href=\"href\" @click=\"navigate\">\n                <i class=\"certificate icon\"></i>\n                <span>Clients</span>\n              </a>\n              <div class=\"dropdown\">\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'client-list' }\">\n                    <span>client list</span>\n                  </router-link>\n                </div>\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'key-pair-list' }\">\n                    <span>key pair list</span>\n                  </router-link>\n                </div>\n              </div>\n            </div>\n          </router-link>\n          <router-link\n            v-slot=\"{ href, route, navigate, isActive, isExactActive }\"\n            :to=\"{ name: 'identity-providers' }\">\n            <div class=\"identity-providers item\" :class=\"{'active': isActive }\">\n              <a :href=\"href\" @click=\"navigate\">\n                <i class=\"users icon\"></i>\n                <span>Identity providers</span>\n              </a>\n              <div class=\"dropdown\">\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'identity-provider-list' }\">\n                    <span>identity provider list</span>\n                  </router-link>\n                </div>\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'backend-list' }\">\n                    <span>backend list</span>\n                  </router-link>\n                </div>\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'user-list' }\">\n                    <span>user list</span>\n                  </router-link>\n                </div>\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'organization-list' }\">\n                    <span>organization list</span>\n                  </router-link>\n                </div>\n              </div>\n            </div>\n          </router-link>\n          <router-link\n            v-slot=\"{ href, route, navigate, isActive, isExactActive }\"\n            :to=\"{ name: 'scopes' }\">\n            <div class=\"scopes item\" :class=\"{'active': isActive }\">\n              <a :href=\"href\" @click=\"navigate\">\n                <i class=\"cogs icon\"></i>\n                <span>Scopes</span>\n              </a>\n              <div class=\"dropdown\">\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'scope-list' }\">\n                    <span>scope list</span>\n                  </router-link>\n                </div>\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'role-list' }\">\n                    <span>role list</span>\n                  </router-link>\n                </div>\n              </div>\n            </div>\n          </router-link>\n          <router-link\n            v-slot=\"{ href, route, navigate, isActive, isExactActive }\"\n            :to=\"{ name: 'configuration' }\">\n            <div class=\"configuration item\" :class=\"{'active': isActive }\">\n              <a :href=\"href\" @click=\"navigate\">\n                <i class=\"columns icon\"></i>\n                <span>Configuration</span>\n              </a>\n              <div class=\"dropdown\">\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'error-template-list' }\">\n                    <span>error templates</span>\n                  </router-link>\n                </div>\n                <div class=\"subitem\">\n                  <router-link :to=\"{ name: 'configuration-file-upload' }\">\n                    <span>upload a configuration file</span>\n                  </router-link>\n                </div>\n              </div>\n            </div>\n          </router-link>\n        </div>\n      </div>\n      <div class=\"content-wrapper\">\n        <Breadcrumb class=\"main-breadcrumb\" />\n        <router-view class=\"content\" />\n      </div>\n    </div>\n    <footer>\n      <a @click=\"toggleDarkMode()\" class=\"dark-mode\">\n        <i class=\"sun icon\" :class=\"{ 'outline': currentMode }\"></i>\n      </a>\n      <Feedback />\n      &copy; 2025 malachit\n    </footer>\n  </div>\n</template>\n\n<script>\nimport Header from '../../components/Header.vue'\nimport Feedback from '../../components/Feedback.vue'\nimport Breadcrumb from '../../components/Breadcrumb.vue'\n\nexport default {\n  name: 'Main',\n  components: {\n    Header,\n    Feedback,\n    Breadcrumb\n  },\n  data () {\n    return {\n      currentMode: JSON.parse(localStorage.getItem('dark_mode'))\n    }\n  },\n  mounted () {\n    const sidebarOffset = this.$refs.header.$el.offsetHeight\n\n    document.addEventListener('scroll', () => {\n      const main = this.$refs.main\n\n      if (window.scrollY < sidebarOffset) {\n        main.classList.remove('fixed-sidebar')\n      } else {\n        main.classList.add('fixed-sidebar')\n      }\n    })\n  },\n  methods: {\n    toggleMenu () {\n      this.$refs.menu.classList.toggle('opened')\n    },\n    toggleDarkMode () {\n      this.currentMode = !this.currentMode\n      localStorage.setItem('dark_mode', this.currentMode)\n    }\n  },\n  beforeRouteUpdate () {\n    this.$refs.menu.classList.remove('opened')\n  }\n}\n</script>\n\n<style lang=\"scss\">\n#app {\n  min-height: 100vh;\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  .ui.label {\n    margin: 5px;\n  }\n  pre {\n    margin: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n  hr {\n    border: none;\n    border-bottom: 1px solid #d4d4d5;\n    margin: 1rem 0;\n  }\n  .attribute.list {\n    margin: 0;\n    .item {\n      list-style-type: none;\n      .header {\n        color: #999;\n      }\n      .description {\n        padding-left: .5rem;\n        color: inherit;\n        text-overflow: ellipsis;\n        overflow: hidden;\n      }\n    }\n  }\n  .ui.menu>.item {\n    border-radius: 0!important;\n  }\n  .label {\n    cursor: default;\n  }\n  footer {\n    position: relative;\n    z-index: 200;\n    text-align: right;\n    padding: 1rem;\n    border-top: 1px solid rgba(34,36,38,.15);\n    .dark-mode {\n      cursor: pointer;\n      float: left;\n      color: rgba(0,0,0,.87);\n    }\n  }\n}\n#main {\n  position: relative;\n  flex: 1;\n  display: flex;\n  min-height: calc(100% - 41px);\n  .main.create.button {\n    position: absolute;\n    top: .53em;\n    right: .5em;\n    font-size: .825rem;\n    @media screen and (max-width: 1127px) {\n      position: relative;\n      display: block;\n      top: 0;\n      right: 0;\n      margin: 0 1rem;\n      margin-bottom: 1rem;\n    }\n  }\n  .main.buttons {\n    position: absolute;\n    top: .53em;\n    right: .5em;\n    .create.button {\n      position: relative;\n      top: 0;\n    }\n    @media screen and (max-width: 1127px) {\n      position: relative;\n      display: block;\n      top: 0;\n      right: 0;\n    }\n  }\n  a {\n    cursor: pointer;\n  }\n  h3, h4, h5 {\n    margin: .5em 0;\n  }\n  .actions {\n    float: right;\n    margin-bottom: 1rem;\n    &.main {\n      margin: 1rem 0;\n    }\n    .button {\n      margin: 5px;\n    }\n  }\n  .sidebar-menu {\n    position: relative;\n    min-width: 200px;\n    border-right: 1px solid #d4d4d5;\n    .menu {\n      margin-top: 0!important;\n      background: white;\n      height: 100%;\n      border: none;\n      .item {\n        background: white;\n        position: relative;\n        padding: 0;\n        min-width: 3em;\n        min-height: 2.5em;\n        border: none;\n        cursor: pointer;\n        span {\n          margin-left: 2.5em;\n          margin-right: 1em;\n          line-height: 2.5rem;\n        }\n        i {\n          position: absolute;\n          top: .625em;\n          left: .75em;\n        }\n        &.active {\n          background: inherit;\n          .dropdown {\n            display: block;\n            border-right: 1px solid #d4d4d5;\n            .subitem {\n              &:last-child {\n                border-bottom: none;\n              }\n            }\n          }\n        }\n        &:not(.active):hover {\n          .dropdown {\n            display: block;\n            position: absolute;\n            left: 100%;\n            top: 0px;\n            width: 200px;\n            z-index: 1000;\n            .subitem {\n              text-align: left;\n              border: 1px solid #d4d4d5;\n              border-top: none;\n              span {\n                margin-left: 1em;\n              }\n            }\n            @media screen and (max-width: 1127px) {\n              display: none;\n            }\n          }\n        }\n        &:hover {\n          background: #e7e7e8;\n        }\n      }\n      a {\n        color: rgba(0,0,0,.87);\n        display: block;\n      }\n      .dropdown {\n        display: none;\n        background: white;\n        border-top: 1px solid #d4d4d5;\n        .subitem {\n          position: relative;\n          border-top: none;\n          text-align: right;\n          font-size: .9em;\n          height: 2rem;\n          span {\n            line-height: 2rem;\n            padding-left: .5rem;\n          }\n          a {\n            font-weight: normal!important;\n            &.router-link-exact-active {\n              font-weight: bold!important;\n            }\n            &:hover {\n              background: #e7e7e8;\n            }\n          }\n        }\n      }\n    }\n    .burger {\n      display: none;\n      cursor: pointer;\n      position: absolute;\n      top: -1.5em;\n      left: .5em;\n    }\n    @media screen and (max-width: 1127px) {\n      position: fixed;\n      z-index: 100;\n      width: 100%;\n      .menu {\n        display: none;\n        z-index: 100;\n        .item {\n          border-bottom: 1px solid #d4d4d5;\n        }\n      }\n      .burger.bars {\n        display: block;\n      }\n      &.opened {\n        .burger.close {\n          display: block;\n        }\n        .burger.bars {\n          display: none;\n        }\n        .menu {\n          display: block;\n        }\n      }\n      height: auto;\n    }\n  }\n  &.fixed-sidebar {\n    @media screen and (min-width: 1127px) {\n      padding-left: 200px;\n      .sidebar-menu {\n        position: fixed;\n        left: 0;\n        top: 0;\n        z-index: 100;\n        height: 100%;\n      }\n    }\n  }\n  .ui.message {\n    word-break: break-word;\n  }\n  .ui.button {\n    font-size: .9em;\n  }\n  .ui.list {\n    & .item:last-child {\n      margin: 0;\n    }\n  }\n  .ui.tabular.menu {\n    .item.error {\n      color: #9f3a38;\n      background: #fff6f6;\n    }\n    @media (max-width: 1200px) {\n      flex-direction: column;\n      .item {\n        width: 100% !important\n      }\n    }\n  }\n  .ui.tab {\n    border-top: none;\n  }\n  .ui.grid {\n    margin: -1rem -1rem 0 0;\n    .column {\n      padding: 0;\n      padding-top: 1rem;\n      padding-right: 1rem;\n      padding-bottom: 1rem;\n    }\n    .column>.segment {\n      height: 100%;\n    }\n  }\n  .ui.tab {\n    &:target {\n      display: block;\n    }\n  }\n  .ui.form {\n    position: relative;\n    label i {\n      font-weight: normal;\n    }\n    .error-message {\n      position: absolute;\n      bottom: -.2em;\n      left: 1em;\n    }\n    .ui.checkbox input[type=radio] {\n      opacity: 1!important;\n    }\n    .inline.fields>label {\n      display: block;\n      width: 100%;\n    }\n    .inline.fields>.field {\n      padding-bottom: 0;\n    }\n    .ui.icon.input>i.icon {\n      cursor: pointer;\n      pointer-events: all;\n      position: absolute;\n    }\n    .field {\n      padding-bottom: 1em;\n      &.error label {\n        color: #9f3a38!important;\n      }\n    }\n  }\n  .ui.segments {\n    margin: 0;\n    .segment{\n      border: 1px solid rgba(34,36,38,.15)!important;\n      &:last-child {\n        margin: 0;\n      }\n    }\n  }\n  .ui.segment {\n    margin-top: 0;\n    margin-bottom: 1rem;\n    &>.field, &>.fields {\n      margin: 0;\n    }\n  }\n  .ui.pagination {\n    .item {\n      border: none;\n      cursor: pointer;\n      &:hover {\n        font-weight: bold;\n      }\n      &:disabled {\n        opacity: .5;\n        cursor: inherit;\n        &:hover {\n          font-weight: normal;\n        }\n      }\n    }\n  }\n  .content-wrapper {\n    flex: 1;\n    width: calc(100vw - 250px);\n    display: flex;\n    flex-direction: column;\n    .content {\n      flex: 1;\n    }\n    .container {\n      padding: 0 1rem;\n      @media screen and (max-width: 768px) {\n        padding: 0;\n      }\n    }\n  }\n  @media screen and (max-width: 1127px) {\n    padding-top: 3em;\n    flex-direction: column;\n    .content-wrapper {\n      width: auto;\n    }\n    .menu {\n      border-right: none;\n      .item {\n        border-right: 1px solid rgba(255,255,255,.05);\n        border-left: 1px solid rgba(255,255,255,.05);\n        border-bottom: 1px solid rgba(255,255,255,.05);\n        &.active {\n          background: rgba(255,255,255,.05);\n        }\n      }\n    }\n  }\n}\n#app.dark {\n  background: #1b1c1d;\n  color: white;\n  hr {\n    border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n  }\n  .sidebar-menu {\n    background: #1b1c1d;\n    border-right: 1px solid rgba(255,255,255,.05);\n    color: white;\n    .menu {\n      background: #1b1c1d;\n      border: none;\n      .item {\n        background: #1b1c1d;\n        border: none;\n        &.active {\n          border: none;\n          background: rgba(255,255,255,.05);\n          &:hover {\n            background: rgba(255,255,255,.08);\n            &:not(.active) {\n              border: 1px solid rgba(255,255,255,.03);\n            }\n          }\n        }\n        &:hover {\n          background: rgba(255,255,255,.08);\n        }\n        a {\n          color: white;\n        }\n      }\n      .dropdown {\n        border: none;\n        background: #1b1c1d;\n        border-top: 1px solid rgba(255,255,255,.05);\n        .subitem {\n          border: none!important;\n          a {\n            color: white;\n            font-weight: normal!important;\n            &.active {\n              background: inherit;\n            }\n            &.router-link-exact-active {\n              background: rgba(255,255,255,.05);\n              border: none;\n              &:hover {\n                background: rgba(255,255,255,.08);\n              }\n            }\n            &:hover {\n              background: rgba(255,255,255,.08)!important;\n            }\n          }\n        }\n      }\n    }\n    @media screen and (max-width: 1127px) {\n      .menu .item {\n        border-bottom: 1px solid rgba(255,255,255,.05);\n      }\n    }\n  }\n  .ui.tabular.menu {\n    .item {\n      color: white;\n      &.active {\n        background: rgba(255, 255, 255, .05);\n      }\n      &.error {\n        color: #e09494;\n        background: #493939;\n      }\n    }\n  }\n  .ui.form {\n    position: relative;\n    select, input, textarea {\n      border: 1px solid rgba(255, 255, 255, 0.15);\n      background: #393939;\n      color: white;\n      &:focus {\n        border: 1px solid #85b7d9!important;\n      }\n    }\n    label {\n      color: white!important;\n    }\n    .ui.input {\n      color: white;\n    }\n    .error-message {\n      color: #e09494!important;\n    }\n    .error.field {\n      label {\n        color: #e09494!important;\n      }\n      select, input {\n        border: 1px solid #e09494;\n        background: #493939;\n      }\n    }\n  }\n  input[disabled]~label {\n    color: #ddd!important;\n  }\n  .ui.segment {\n    background: rgba(255,255,255,.05);\n    color: white;\n    border: 1px solid rgba(255, 255, 255, 0.1)!important;\n    &.highlightable:hover {\n      background: rgba(255,255,255,.08);\n    }\n  }\n  .ui.breadcrumb {\n    .divider {\n      color: white!important;\n    }\n    a.section {\n      font-weight: bold;\n      color: rgba(153, 153, 153, 1.0)!important;\n      &:hover {\n        color: rgba(153, 153, 153, 0.7)!important;\n      }\n    }\n  }\n  .ui.header {\n    color: white;\n  }\n  .ui.list {\n    &>.item .header {\n      color: white;\n    }\n    &.celled {\n      &>.item {\n        border-top: 1px solid rgba(255, 255, 255, 0.1);\n      }\n    }\n  }\n  .ui.pagination.menu {\n    background: #1b1c1d;\n    border: 1px solid rgba(255,255,255,.08);\n    .item {\n      border: none!important;\n      color: white;\n      &.active {\n        background: rgba(255,255,255,.05);\n      }\n    }\n  }\n  .text-editor {\n    .codejar-wrap {\n      height: 100%;\n      .codejar-linenumbers {\n        color: white;\n      }\n    }\n    .editor {\n      background: rgba(255,255,255,.05);\n    }\n  }\n  .olive {\n    background: rgba(61, 61, 61, 1.0)!important;\n    &.button:hover, &.label:hover {\n      background: rgba(61, 61, 61, 0.7)!important;\n    }\n  }\n  .violet {\n    background: rgba(131, 52, 113, 1.0)!important;\n    &.button:hover, &.label:hover {\n      background: rgba(131, 52, 113, 0.7)!important;\n    }\n  }\n  .blue {\n    background: rgba(34, 112, 147,1.0)!important;\n    &.button:hover, &.label:hover {\n      background: rgba(34, 112, 147,0.7)!important;\n    }\n  }\n  .red {\n    background: rgba(179, 57, 57,1.0)!important;\n    &.button:hover, &.label:hover {\n      background: rgba(179, 57, 57,0.7)!important;\n    }\n  }\n  .teal {\n    background: rgba(33, 140, 116,1.0)!important;\n    &.button:hover, &.label:hover {\n      background: rgba(33, 140, 116,0.7)!important;\n    }\n  }\n  footer {\n    background: #1b1c1d;\n    border-top: 1px solid rgba(255,255,255,.08);\n    .dark-mode {\n      color: white;\n      &:hover {\n        background: inherit!important;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/NotFound.vue",
    "content": "<template>\n  <div class=\"not-found\">\n    <div class=\"ui container\">\n      <div class=\"ui placeholder segment\">\n        <div class=\"ui icon header\">\n          <i class=\"sign question circle icon\"></i>\n          The resource you are looking for does not exist\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'not-found'\n}\n</script>\n\n<style scoped lang=\"scss\">\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/OauthCallback.vue",
    "content": "<template>\n  <div class=\"oauth-callback\">\n    <div class=\"ui container\">\n      <div class=\"ui loading placeholder segment\"></div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport oauth from '../services/oauth.service'\n\nexport default {\n  name: 'oauth-callback',\n  beforeRouteEnter (from, to, next) {\n    oauth.callback().then(() => {\n      if (window.frameElement) return\n\n      next(oauth.storedLocation)\n    }).catch(error => next(false))\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Roles/EditRole.vue",
    "content": "<template>\n  <div class=\"edit-role\">\n    <Toaster :active=\"success\" message=\"Role has been updated\" type=\"success\" />\n    <div class=\"ui container\">\n      <div class=\"ui segment\">\n        <div class=\"ui attribute list\">\n          <div class=\"item\">\n            <span class=\"header\">Role ID</span>\n            <span class=\"description\">{{ role.id }}</span>\n          </div>\n        </div>\n      </div>\n      <RoleForm :role=\"role\" @submit=\"updateRole()\" @back=\"back()\" action=\"Update\" />\n    </div>\n  </div>\n</template>\n\n<script>\nimport Role from '../../models/role.model'\nimport RoleForm from '../../components/Forms/RoleForm.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'roles',\n  components: {\n    RoleForm,\n    Toaster\n  },\n  mounted () {\n    const { roleId } = this.$route.params\n    Role.get(roleId).then((role) => {\n      this.role = role\n    })\n  },\n  data () {\n    return {\n      role: new Role(),\n      success: false\n    }\n  },\n  methods: {\n    back () {\n      this.$router.push({ name: 'role-list' })\n    },\n    updateRole () {\n      this.success = false\n      return this.role.save().then(() => {\n        this.success = true\n      }).catch()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Roles/NewRole.vue",
    "content": "<template>\n  <div class=\"new-role\">\n    <div class=\"ui container\">\n      <RoleForm :role=\"role\" @submit=\"createRole()\" @back=\"back()\" action=\"Create\" />\n    </div>\n  </div>\n</template>\n\n<script>\nimport Role from '../../models/role.model'\nimport RoleForm from '../../components/Forms/RoleForm.vue'\n\nexport default {\n  name: 'new-role',\n  components: {\n    RoleForm\n  },\n  data () {\n    return {\n      role: new Role()\n    }\n  },\n  methods: {\n    back () {\n      this.$router.push({ name: 'role-list' })\n    },\n    createRole () {\n      return this.role.save().then(() => {\n        this.$router.push({ name: 'role-list' })\n      }).catch()\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.new-role {\n  .field {\n    position: relative;\n    &.roles input {\n      margin-right: 3em;\n    }\n  }\n  .ui.icon.input>i.icon.close {\n    cursor: pointer;\n    pointer-events: all;\n  }\n  .authorized-scopes-select {\n    margin-right: 3em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Roles/Role.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Roles/RoleList.vue",
    "content": "<template>\n  <div class=\"role-list\">\n    <Toaster :active=\"deleted\" message=\"Role has been deleted\" type=\"warning\" />\n    <router-link :to=\"{ name: 'new-role' }\" class=\"ui violet main create button\">Add a role</router-link>\n    <div class=\"container\">\n      <div class=\"ui info message\">\n        Roles have here a label and are associated with scopes that will be available to the users having them.\n      </div>\n      <div class=\"ui three column roles stackable grid\">\n        <div v-for=\"role in roles\" :key=\"role.id\" class=\"column\">\n          <div class=\"ui upstream highlightable segment\">\n            <div class=\"actions\">\n              <router-link\n                :to=\"{ name: 'edit-role', params: { roleId: role.id } }\"\n                class=\"ui tiny blue button\">edit</router-link>\n              <a v-on:click=\"deleteRole(role)\" class=\"ui tiny red button\">delete</a>\n            </div>\n            <div class=\"ui attribute list\">\n              <div class=\"item\">\n                <span class=\"header\">Role ID</span>\n                <span class=\"description\">{{ role.id }}</span>\n              </div>\n              <div class=\"item\">\n                <span class=\"header\">Name</span>\n                <span class=\"description\">{{ role.name }}</span>\n              </div>\n              <div class=\"scopes item\">\n                <span class=\"header\">Scopes</span>\n                <div class=\"description\">\n                  <span v-for=\"scope in role.scopes\">{{ scope.model.name }}</span>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Role from '../../models/role.model'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'role-list',\n  components: {\n    Toaster\n  },\n  data () {\n    return {\n      roles: [],\n      deleted: false\n    }\n  },\n  mounted () {\n    this.getRoles()\n  },\n  methods: {\n    getRoles () {\n      Role.all().then((roles) => {\n        this.roles = roles\n      })\n    },\n    deleteRole (role) {\n      if (!confirm('Are you sure ?')) return\n      this.deleted = false\n      role.destroy().then(() => {\n        this.deleted = true\n        this.roles.splice(this.roles.indexOf(role), 1)\n      })\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.scopes.item .description {\n  height: 7em;\n  overflow-y: scroll!important;\n  span {\n    display: block;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Roles.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Scopes/ScopeList.vue",
    "content": "<template>\n  <div id=\"scope-list\">\n    <Toaster :active=\"saved\" message=\"Scope has been saved\" type=\"success\" />\n    <Toaster :active=\"deleted\" message=\"Scope has been deleted\" type=\"warning\" />\n    <Toaster :active=\"errorMessage\" :message=\"errorMessage\" type=\"error\" />\n    <div class=\"container\">\n      <div class=\"ui info message\">\n        Scopes are here the ones that are to be requested by the client. They are to be public, accessible to everyone, or private, granted only having a priviledge user or client.\n      </div>\n      <div class=\"ui segments\" v-if=\"scopes.length\">\n        <div v-for=\"(scope, index) in scopes\" class=\"ui mini highlightable segment\" :key=\"index\">\n          <div v-if=\"scope.edit\">\n            <form @submit.prevent=\"saveScope(scope)\" class=\"ui form\">\n              <div class=\"ui stackable grid\">\n                <div class=\"four wide column\">\n                  <div class=\"ui field\" :class=\"{ 'error': scope.errors && scope.errors.name }\">\n                    <input type=\"text\" v-model=\"scope.name\" placeholder=\"iam:a:scope\">\n                    <em v-if=\"scope.errors && scope.errors.name\" class=\"error-message\">{{ scope.errors.name[0] }}</em>\n                  </div>\n                </div>\n                <div class=\"four wide column\">\n                  <div class=\"ui field\" :class=\"{ 'error': scope.errors && scope.errors.label }\">\n                    <input type=\"text\" v-model=\"scope.label\" placeholder=\"I am a scope label\">\n                    <em v-if=\"scope.errors && scope.errors.label\" class=\"error-message\">{{ scope.errors.label[0] }}</em>\n                  </div>\n                </div>\n                <div class=\"five wide column\">\n                  <div class=\"ui checkbox\">\n                    <input type=\"checkbox\" v-model=\"scope.public\" placeholder=\"http://redirect.uri\">\n                    <label>Public</label>\n                  </div>\n                </div>\n                <div class=\"three wide actions column\">\n                  <button v-on:click.prevent=\"viewScope(scope)\" class=\"ui tiny blue button\">cancel</button>\n                  <button type=\"submit\" class=\"ui tiny violet button\">save</button>\n                </div>\n              </div>\n            </form>\n          </div>\n          <div v-else>\n            <div class=\"ui stackable grid\">\n              <div class=\"four wide column\">\n                <span class=\"ui teal label\">{{ scope.name }}</span>\n              </div>\n              <div class=\"four wide column\">\n                <strong>{{ scope.label }}</strong>\n              </div>\n              <div class=\"five wide column\">\n                <div class=\"ui checkbox\">\n                  <input disabled type=\"checkbox\" v-model=\"scope.public\" placeholder=\"http://redirect.uri\">\n                  <label>Public</label>\n                </div>\n              </div>\n              <div class=\"three wide actions column\">\n                <button v-on:click=\"editScope(scope)\" class=\"ui tiny blue button\">edit</button>\n                <button v-on:click=\"deleteScope(scope)\" class=\"ui tiny red button\">delete</button>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <hr />\n      <button @click=\"addScope()\" class=\"ui violet add button\">Add a scope</button>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Scope from '../../models/scope.model'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'client-list',\n  components: {\n    Toaster\n  },\n  data () {\n    return {\n      scopes: [],\n      saved: false,\n      deleted: false,\n      errorMessage: false\n    }\n  },\n  mounted () {\n    this.getScopes()\n  },\n  methods: {\n    getScopes () {\n      Scope.all().then((scopes) => {\n        this.scopes = scopes\n      })\n    },\n    addScope () {\n      this.scopes.push(new Scope({ edit: true }))\n    },\n    editScope (scope) {\n      scope.edit = true\n    },\n    viewScope (scope) {\n      if (scope.persisted) {\n        scope.reset().then(() => {\n          scope.edit = false\n        })\n      } else {\n        this.deleteScope(scope)\n      }\n    },\n    saveScope (scope) {\n      this.saved = false\n      this.errorMessage = false\n      scope.save().then((scope) => {\n        this.saved = true\n        scope.edit = false\n      }).catch((error) => {\n        this.errorMessage = error.message\n      })\n    },\n    deleteScope (scope) {\n      if (!confirm('Are you sure ?')) return\n      this.errorMessage = false\n      this.deleted = false\n      if (scope.persisted) {\n        scope.destroy().then(() => {\n          this.deleted = true\n          this.scopes.splice(\n            this.scopes.indexOf(scope),\n            1\n          )\n        }).catch((error) => {\n          this.errorMessage = error.message\n        })\n      } else {\n        this.scopes.splice(\n          this.scopes.indexOf(scope),\n          1\n        )\n      }\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n#scope-list {\n  .segments {\n    border: none;\n    @media screen and (max-width: 768px) {\n      padding: 0 1rem;\n    }\n  }\n  .ui.grid {\n    margin-bottom: -1rem;\n  }\n  .column {\n    padding: 1rem!important;\n    display: flex;\n    align-items: center;\n    &.actions {\n      margin: 0;\n      justify-content: flex-end;\n      .button {\n        margin-left: 1em;\n      }\n    }\n    .field {\n      flex-direction: column;\n      width: 100%;\n      margin-bottom: 0;\n      padding-bottom: 0;\n      .error-message {\n        color: #9f3a38;\n      }\n    }\n  }\n  .add.button {\n    margin-bottom: 1rem;\n    @media (max-width: 768px) {\n      margin-left: 1rem;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Scopes.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Upstreams/EditUpstream.vue",
    "content": "<template>\n  <div class=\"edit-upstream\">\n    <Toaster :active=\"success\" message=\"Upstream has been updated\" type=\"success\" />\n    <div class=\"container\">\n      <div class=\"ui stackable grid\">\n        <div class=\"four wide column\">\n          <div class=\"sidebar\">\n            <div class=\"ui segment\">\n              <div class=\"ui attribute list\">\n                <div class=\"item\">\n                  <span class=\"header\">Upstream ID</span>\n                  <span class=\"description\">{{ upstream.id }}</span>\n                </div>\n              </div>\n            </div>\n            <router-link :to=\"{ name: 'upstream-list' }\" class=\"ui right floated button\">Back</router-link>\n          </div>\n        </div>\n        <div class=\"twelve wide column\">\n          <UpstreamForm :upstream=\"upstream\" @submit=\"updateUpstream()\" @back=\"back()\" action=\"Update\" />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Upstream from '../../models/upstream.model'\nimport UpstreamForm from '../../components/Forms/UpstreamForm.vue'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'upstreams',\n  components: {\n    UpstreamForm,\n    Toaster\n  },\n  mounted () {\n    const { upstreamId } = this.$route.params\n    Upstream.get(upstreamId).then((upstream) => {\n      this.upstream = upstream\n    })\n  },\n  data () {\n    return {\n      upstream: new Upstream(),\n      success: false\n    }\n  },\n  methods: {\n    back () {\n      this.$router.push({ name: 'upstream-list' })\n    },\n    updateUpstream () {\n      this.success = false\n      return this.upstream.save().then(() => {\n        this.success = true\n      }).catch()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Upstreams/NewUpstream.vue",
    "content": "<template>\n  <div class=\"new-upstream\">\n    <div class=\"container\">\n      <div class=\"ui stackable grid\">\n        <div class=\"four wide column\">\n          <div class=\"sidebar\">\n            <router-link :to=\"{ name: 'upstream-list' }\" class=\"ui right floated button\">Back</router-link>\n          </div>\n        </div>\n        <div class=\"twelve wide column\">\n          <UpstreamForm :upstream=\"upstream\" @submit=\"createUpstream()\" @back=\"back()\" action=\"Create\" />\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Upstream from '../../models/upstream.model'\nimport UpstreamForm from '../../components/Forms/UpstreamForm.vue'\n\nexport default {\n  name: 'new-upstream',\n  components: {\n    UpstreamForm\n  },\n  data () {\n    return {\n      upstream: new Upstream()\n    }\n  },\n  methods: {\n    back () {\n      this.$router.push({ name: 'upstream-list' })\n    },\n    createUpstream () {\n      return this.upstream.save().then(() => {\n        this.$router.push({ name: 'upstream-list' })\n      }).catch()\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.new-upstream {\n  .field {\n    position: relative;\n    &.upstreams input {\n      margin-right: 3em;\n    }\n  }\n  .ui.icon.input>i.icon.close {\n    cursor: pointer;\n    pointer-events: all;\n  }\n  .authorized-scopes-select {\n    margin-right: 3em;\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Upstreams/Upstream.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Upstreams/UpstreamList.vue",
    "content": "<template>\n  <div class=\"upstream-list\">\n    <Toaster :active=\"deleted\" message=\"Upstream has been deleted\" type=\"warning\" />\n    <router-link :to=\"{ name: 'new-upstream' }\" class=\"ui violet main create button\">Add an upstream</router-link>\n    <div class=\"container\">\n      <div class=\"ui info message\">\n        Upstreams are here the configuration of the flow the gateway will apply for requests to be forwarded to backends. Corresponding to given paths, the gateway will route the request to the configured server.\n      </div>\n      <div class=\"ui segment\" v-for=\"nodeName in Object.keys(upstreams)\" :key=\"nodeName\">\n        <h2>{{ nodeName }}</h2>\n        <div class=\"ui three column upstreams stackable grid\">\n          <div v-for=\"upstream in upstreams[nodeName]\" :key=\"upstream.id\" class=\"column\">\n            <div class=\"ui upstream highlightable segment\">\n              <div class=\"actions\">\n                <router-link\n                  :to=\"{ name: 'edit-upstream', params: { upstreamId: upstream.id } }\"\n                  class=\"ui tiny blue button\">edit</router-link>\n                <a v-on:click=\"deleteUpstream(upstream)\" class=\"ui tiny red button\">delete</a>\n              </div>\n              <div class=\"ui attribute list\">\n                <div class=\"item\">\n                  <span class=\"header\">Upstream ID</span>\n                  <span class=\"description\">{{ upstream.id }}</span>\n                </div>\n                <div class=\"item\">\n                  <span class=\"header\">Base URL</span>\n                  <span class=\"description\">{{ upstream.baseUrl }}</span>\n                </div>\n                <div class=\"item\">\n                  <span class=\"header\">Paths</span>\n                  <div class=\"description\">\n                    <span v-for=\"path in upstream.uris\" class=\"ui teal label\" :key=\"path.uri\">\n                      {{ path.uri }}\n                    </span>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Upstream from '../../models/upstream.model'\nimport Toaster from '../../components/Toaster.vue'\n\nexport default {\n  name: 'upstream-list',\n  components: {\n    Toaster\n  },\n  data () {\n    return {\n      upstreams: [],\n      deleted: false\n    }\n  },\n  mounted () {\n    this.getUpstreams()\n  },\n  methods: {\n    getUpstreams () {\n      Upstream.all().then((upstreams) => {\n        this.upstreams = upstreams\n      })\n    },\n    deleteUpstream (upstream) {\n      if (!confirm('Are you sure ?')) return\n      this.deleted = false\n      upstream.destroy().then(() => {\n        this.deleted = true\n        this.upstreams[upstream.node_name].splice(this.upstreams[upstream.node_name].indexOf(upstream), 1)\n        if (!this.upstreams[upstream.node_name].length)\n          delete this.upstreams[upstream.node_name]\n      })\n    }\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n</style>\n"
  },
  {
    "path": "apps/boruta_admin/assets/src/views/Upstreams.vue",
    "content": "<template>\n  <router-view />\n</template>\n"
  },
  {
    "path": "apps/boruta_admin/assets/tests/unit/.eslintrc.js",
    "content": "module.exports = {\n  env: {\n    mocha: true\n  }\n}\n"
  },
  {
    "path": "apps/boruta_admin/assets/vite.config.js",
    "content": "import { defineConfig } from 'vite'\nimport { viteSingleFile } from 'vite-plugin-singlefile'\nimport vue from '@vitejs/plugin-vue'\nimport path from 'path'\nimport { nodePolyfills } from 'vite-plugin-node-polyfills'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  define: {\n    'process.env.NODE_ENV': '\"production\"',\n  },\n  plugins: [\n    vue(),\n    viteSingleFile(),\n    nodePolyfills({\n      // To exclude specific polyfills, add them to this list.\n      exclude: [\n        'fs', // Excludes the polyfill for `fs` and `node:fs`.\n      ],\n      // Whether to polyfill specific globals.\n      globals: {\n        Buffer: true, // can also be 'build', 'dev', or false\n        global: true,\n        process: true,\n      },\n      // Whether to polyfill `node:` protocol imports.\n      protocolImports: true,\n    }),\n  ],\n  build: {\n    outDir: path.resolve(__dirname, '../priv/static/assets'),\n    lib: {\n      entry: path.resolve(__dirname, './src/main.js'),\n      name: 'Boruta',\n      fileName: (format) => `app.${format}.js`\n    },\n    target: 'esnext',\n    assetsInlineLimit: 100000000,\n    chunkSizeWarningLimit: 100000000,\n    cssCodeSplit: false,\n    brotliSize: false\n  }\n})\n"
  },
  {
    "path": "apps/boruta_admin/config/config.exs",
    "content": "import Config\n\nconfig :boruta_admin,\n  ecto_repos: [\n    BorutaAdmin.Repo,\n    BorutaAuth.Repo,\n    BorutaIdentity.Repo,\n    BorutaGateway.Repo,\n    BorutaWeb.Repo\n  ]\n\nconfig :boruta_admin, BorutaAdminWeb.Endpoint,\n  url: [\n    host: \"localhost\",\n    protocol_options: [idle_timeout: 3_600_000, inactivity_timeout: 3_600_000]\n  ],\n  secret_key_base: \"Caq0kwgjLGwxoEVPOxUhEiZ3AG2nADaNYi+ceWh2RuAgKF6vv/FfwqM/P7cDcNrR\",\n  render_errors: [view: BorutaAdminWeb.ErrorView, accepts: ~w(html json), layout: false],\n  pubsub_server: BorutaAdmin.PubSub,\n  live_view: [signing_salt: \"mtlt3we/\"]\n\nconfig :boruta, Boruta.Oauth,\n  repo: BorutaAuth.Repo,\n  contexts: [\n    resource_owners: BorutaIdentity.ResourceOwners\n  ],\n  issuer: System.get_env(\"BORUTA_OAUTH_BASE_URL\", \"http://localhost:4000\")\n\nconfig :phoenix, :json_library, Jason\n\nimport_config \"#{Mix.env()}.exs\"\n"
  },
  {
    "path": "apps/boruta_admin/config/dev.exs",
    "content": "import Config\n\nconfig :boruta_admin, BorutaAdmin.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 1\n\nconfig :boruta_admin, BorutaAdminWeb.Endpoint,\n  http: [\n    port: System.get_env(\"BORUTA_ADMIN_PORT\", \"4001\") |> String.to_integer(),\n    protocol_options: [idle_timeout: 3_600_000, inactivity_timeout: 3_600_000]\n  ],\n  debug_errors: true,\n  code_reloader: true,\n  check_origin: false,\n  watchers: [\n    npm: [\n      \"run\",\n      \"build:watch\",\n      cd: Path.expand(\"../assets\", __DIR__)\n    ]\n  ],\n  live_reload: [\n    patterns: [\n      ~r\"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$\",\n      ~r\"priv/gettext/.*(po)$\",\n      ~r\"lib/boruta_admin_web/{live,views}/.*(ex)$\",\n      ~r\"lib/boruta_admin_web/templates/.*(eex)$\"\n    ]\n  ]\n\nconfig :boruta_web, BorutaAdminWeb.Authorization,\n  oauth2: [\n    site: System.get_env(\"BORUTA_ADMIN_OAUTH_BASE_URL\", \"http://localhost:4000\")\n  ],\n  sub_restricted: System.get_env(\"BORUTA_SUB_RESTRICTED\", nil),\n  organization_restricted: System.get_env(\"BORUTA_ORGANIZATION_RESTRICTED\", nil)\n\nconfig :logger, :console, level: :debug\n\nconfig :phoenix, :stacktrace_depth, 20\n\nconfig :phoenix, :plug_init_mode, :runtime\n"
  },
  {
    "path": "apps/boruta_admin/config/prod.exs",
    "content": "import Config\n"
  },
  {
    "path": "apps/boruta_admin/config/test.exs",
    "content": "import Config\n\n# Configure your database\n#\n# The MIX_TEST_PARTITION environment variable can be used\n# to provide built-in test partitioning in CI environment.\n# Run `mix help test` for more information.\nconfig :boruta_admin, BorutaAdmin.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_identity_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool: Ecto.Adapters.SQL.Sandbox\n\nconfig :boruta_identity, BorutaIdentity.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_identity_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool: Ecto.Adapters.SQL.Sandbox\n\nconfig :boruta_web, BorutaWeb.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_web_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool: Ecto.Adapters.SQL.Sandbox\n\nconfig :boruta_gateway, BorutaGateway.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_gateway_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool: Ecto.Adapters.SQL.Sandbox\n\nconfig :boruta_auth, BorutaAuth.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_gateway_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool: Ecto.Adapters.SQL.Sandbox\n\nconfig :boruta_gateway,\n  port: 7777,\n  server: true\n\n# We don't run a server during test. If one is required,\n# you can enable the server option below.\nconfig :boruta_admin, BorutaAdminWeb.Endpoint,\n  http: [port: 4002],\n  server: false\n\nconfig :boruta_web, BorutaAdminWeb.Authorization,\n  oauth2: [\n    site: System.get_env(\"BORUTA_ADMIN_OAUTH_BASE_URL\", \"http://localhost:7000\")\n  ],\n  sub_restricted: System.get_env(\"BORUTA_SUB_RESTRICTED\", nil),\n  organization_restricted: System.get_env(\"BORUTA_ORGANIZATION_RESTRICTED\", nil)\n\nconfig :boruta_identity, BorutaIdentity.SMTP,\n  adapter: Swoosh.Adapters.Test\n\nconfig :boruta_identity, BorutaIdentity.LdapRepo,\n  adapter: BorutaIdentity.LdapRepoMock\n\n# Print only warnings and errors during test\nconfig :logger, level: :warn\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin/application.ex",
    "content": "defmodule BorutaAdmin.Application do\n  @moduledoc false\n\n  require Logger\n\n  use Application\n\n  def start(_type, _args) do\n    children = [\n      BorutaAdmin.Repo,\n      BorutaAdminWeb.Telemetry,\n      {Phoenix.PubSub, name: BorutaAdmin.PubSub},\n      BorutaAdminWeb.Endpoint\n    ]\n\n    :telemetry.attach(\n      :boruta_admin_requests,\n      [:boruta_admin, :endpoint, :stop],\n      &__MODULE__.boruta_admin_request_handler/4,\n      :ok\n    )\n\n    setup_database()\n    opts = [strategy: :one_for_one, name: BorutaAdmin.Supervisor]\n    Supervisor.start_link(children, opts)\n  end\n\n  def setup_database do\n    Enum.each([BorutaAdmin.Repo], fn repo ->\n      repo.__adapter__.storage_up(repo.config)\n    end)\n\n    Enum.each([BorutaAdmin.Repo], fn repo ->\n      Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))\n    end)\n\n    :ok\n  end\n\n  def config_change(changed, _new, removed) do\n    BorutaAdminWeb.Endpoint.config_change(changed, removed)\n    :ok\n  end\n\n  def boruta_admin_request_handler(_, %{duration: duration}, %{conn: conn} = metadata, _) do\n    case log_level(metadata[:options][:log], conn) do\n      false ->\n        :ok\n\n      level ->\n        Logger.log(\n          level,\n          fn ->\n            %{method: method, request_path: path, status: status, state: state} = conn\n            status = Integer.to_string(status)\n\n            [\n              \"boruta_admin\",\n              ?\\s,\n              method,\n              ?\\s,\n              path,\n              \" - \",\n              connection_type(state),\n              ?\\s,\n              status,\n              \" in \",\n              duration(duration)\n            ]\n          end,\n          type: :request\n        )\n    end\n  end\n\n  # From Phoenix.Logger\n  defp log_level(nil, _conn), do: :info\n  defp log_level(level, _conn) when is_atom(level), do: level\n\n  defp log_level({mod, fun, args}, conn) when is_atom(mod) and is_atom(fun) and is_list(args) do\n    apply(mod, fun, [conn | args])\n  end\n\n  defp connection_type(:set_chunked), do: \"chunked\"\n  defp connection_type(_), do: \"sent\"\n\n  defp duration(duration) do\n    duration = System.convert_time_unit(duration, :native, :microsecond)\n\n    if duration > 1000 do\n      [duration |> div(1000) |> Integer.to_string(), \"ms\"]\n    else\n      [Integer.to_string(duration), \"µs\"]\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin/configuration_loader/schema.ex",
    "content": "defmodule BorutaAdmin.ConfigurationLoader.Schema do\n  @moduledoc false\n\n  alias ExJsonSchema.Schema\n\n  def gateway do\n    %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"authorize\" => %{\"type\" => \"boolean\"},\n        \"error_content_type\" => %{\"type\" => \"string\"},\n        \"forbidden_response\" => %{\"type\" => \"string\"},\n        \"unauthorized_response\" => %{\"type\" => \"string\"},\n        \"forwarded_token_private_key\" => %{\"type\" => \"string\"},\n        \"forwarded_token_public_key\" => %{\"type\" => \"string\"},\n        \"forwarded_token_secret\" => %{\"type\" => \"string\"},\n        \"forwarded_token_signature_alg\" => %{\"type\" => \"string\"},\n        \"scheme\" => %{\"type\" => \"string\", \"pattern\" => \"^(http|https)$\"},\n        \"host\" => %{\"type\" => \"string\"},\n        \"port\" => %{\"type\" => \"number\"},\n        \"uris\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"string\"\n          }\n        },\n        \"strip_uri\" => %{\"type\" => \"boolean\"},\n        \"pool_count\" => %{\"type\" => \"number\"},\n        \"pool_size\" => %{\"type\" => \"number\"},\n        \"max_idle_time\" => %{\"type\" => \"number\"},\n        \"required_scopes\" => %{\n          \"type\" => \"object\",\n          \"patternProperties\" => %{\n            \"(GET|POST|PUT|HEAD|OPTIONS|PATCH|DELETE|\\\\*)\" => %{\n              \"type\" => \"array\",\n              \"items\" => %{\n                \"type\" => \"string\",\n                \"pattern\" => \".+\"\n              },\n              \"minItems\" => 1\n            }\n          },\n          \"additionalProperties\" => false\n        }\n      },\n      \"required\" => [\"scheme\", \"host\", \"port\", \"uris\"],\n      \"additionalProperties\" => false\n    }\n    |> Schema.resolve()\n  end\n\n  def microgateway do\n    %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"node_name\" => %{\"type\" => \"string\"},\n        \"authorize\" => %{\"type\" => \"boolean\"},\n        \"error_content_type\" => %{\"type\" => \"string\"},\n        \"forbidden_response\" => %{\"type\" => \"string\"},\n        \"unauthorized_response\" => %{\"type\" => \"string\"},\n        \"forwarded_token_private_key\" => %{\"type\" => \"string\"},\n        \"forwarded_token_public_key\" => %{\"type\" => \"string\"},\n        \"forwarded_token_secret\" => %{\"type\" => \"string\"},\n        \"forwarded_token_signature_alg\" => %{\"type\" => \"string\"},\n        \"scheme\" => %{\"type\" => \"string\", \"pattern\" => \"^(http|https)$\"},\n        \"host\" => %{\"type\" => \"string\"},\n        \"port\" => %{\"type\" => \"number\"},\n        \"uris\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"string\"\n          }\n        },\n        \"strip_uri\" => %{\"type\" => \"boolean\"},\n        \"pool_count\" => %{\"type\" => \"number\"},\n        \"pool_size\" => %{\"type\" => \"number\"},\n        \"max_idle_time\" => %{\"type\" => \"number\"},\n        \"required_scopes\" => %{\n          \"type\" => \"object\",\n          \"patternProperties\" => %{\n            \"(GET|POST|PUT|HEAD|OPTIONS|PATCH|DELETE|\\\\*)\" => %{\n              \"type\" => \"array\",\n              \"items\" => %{\n                \"type\" => \"string\",\n                \"pattern\" => \".+\"\n              },\n              \"minItems\" => 1\n            }\n          },\n          \"additionalProperties\" => false\n        }\n      },\n      \"required\" => [\"node_name\", \"scheme\", \"host\", \"port\", \"uris\"],\n      \"additionalProperties\" => false\n    }\n    |> Schema.resolve()\n  end\n\n  def organization do\n    %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"id\" => %{\"type\" => \"string\"},\n        \"name\" => %{\"type\" => \"string\"},\n        \"label\" => %{\"type\" => \"string\"}\n      },\n      \"required\" => [\"name\"],\n      \"additionalProperties\" => false\n    }\n  end\n\n  def backend do\n    %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"create_default_organization\" => %{\"type\" => \"boolean\"},\n        \"federated_servers\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"object\",\n            \"properties\" => %{\n              \"name\" => %{\"type\" => \"string\", \"pattern\" => \"^[^\\s]+$\"},\n              \"client_id\" => %{\"type\" => \"string\"},\n              \"client_secret\" => %{\"type\" => \"string\"},\n              \"base_url\" => %{\"type\" => \"string\"},\n              \"discovery_path\" => %{\"type\" => \"string\"},\n              \"userinfo_path\" => %{\"type\" => \"string\"},\n              \"authorize_path\" => %{\"type\" => \"string\"},\n              \"token_path\" => %{\"type\" => \"string\"},\n              \"scope\" => %{\"type\" => \"string\"},\n              \"federated_attributes\" => %{\"type\" => \"string\"},\n              \"metadata_endpoints\" => %{\n                \"type\" => \"array\",\n                \"items\" => %{\n                  \"type\" => \"object\",\n                  \"properties\" => %{\n                    \"endpoint\" => %{\"type\" => \"string\"},\n                    \"claims\" => %{\"type\" => \"string\"}\n                  },\n                  \"required\" => [\"endpoint\", \"claims\"]\n                }\n              }\n            },\n            \"required\" => [\n              \"name\",\n              \"client_id\",\n              \"client_secret\",\n              \"base_url\"\n            ],\n            \"additionalProperties\" => false\n          }\n        },\n        \"id\" => %{\"type\" => \"string\"},\n        \"is_default\" => %{\"type\" => \"boolean\"},\n        \"ldap_base_dn\" => %{\"type\" => \"string\"},\n        \"ldap_host\" => %{\"type\" => \"string\"},\n        \"ldap_master_dn\" => %{\"type\" => \"string\"},\n        \"ldap_master_password\" => %{\"type\" => \"string\"},\n        \"ldap_ou\" => %{\"type\" => \"string\"},\n        \"ldap_pool_size\" => %{\"type\" => \"string\"},\n        \"ldap_user_rdn_attribute\" => %{\"type\" => \"string\"},\n        \"metadata_fields\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"object\",\n            \"properties\" => %{\n              \"attribute_name\" => %{\"type\" => \"string\"},\n              \"user_editable\" => %{\"type\" => \"boolean\"},\n              \"scopes\" => %{\"type\" => \"array\", \"items\" => %{\"type\" => \"string\"}}\n            },\n            \"required\" => [\"attribute_name\"],\n            \"additionalProperties\" => false\n          }\n        },\n        \"name\" => %{\"type\" => \"string\"},\n        \"password_hashing_alg\" => %{\"type\" => \"string\"},\n        \"password_hashing_opts\" => %{\"type\" => \"object\"},\n        \"roles\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"object\",\n            \"properties\" => %{\n              \"id\" => %{\"type\" => \"string\"}\n            }\n          }\n        },\n        \"smtp_from\" => %{\"type\" => \"string\"},\n        \"smtp_password\" => %{\"type\" => \"string\"},\n        \"smtp_port\" => %{\"type\" => \"number\"},\n        \"smtp_relay\" => %{\"type\" => \"string\"},\n        \"smtp_ssl\" => %{\"type\" => \"boolean\"},\n        \"smtp_tls\" => %{\"type\" => \"string\"},\n        \"smtp_username\" => %{\"type\" => \"string\"},\n        \"type\" => %{\"type\" => \"string\"},\n        \"verifiable_credentials\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"object\",\n            \"properties\" => %{\n              \"version\" => %{\"type\" => \"string\"},\n              \"credential_identifier\" => %{\"type\" => \"string\"},\n              \"time_to_live\" => %{\"type\" => \"number\"},\n              \"types\" => %{\"type\" => \"string\"},\n              \"format\" => %{\"type\" => \"string\", \"pattern\" => \"jwt_vc|jwt_vc_json|vc\\\\+sd\\\\-jwt\"},\n              \"claims\" => %{\n                \"type\" => \"array\",\n                \"items\" => %{\n                  \"type\" => \"object\",\n                  \"properties\" => %{\n                    \"name\" => %{\"type\" => \"string\"},\n                    \"label\" => %{\"type\" => \"string\"},\n                    \"pointer\" => %{\"type\" => \"string\"}\n                  },\n                  \"required\" => [\"name\", \"label\", \"pointer\"]\n                }\n              },\n              \"display\" => %{\n                \"type\" => \"object\",\n                \"properties\" => %{\n                  \"name\" => %{\"type\" => \"string\"},\n                  \"locale\" => %{\"type\" => \"string\"},\n                  \"background_color\" => %{\"type\" => \"string\"},\n                  \"text_color\" => %{\"type\" => \"string\"},\n                  \"logo\" => %{\n                    \"type\" => \"object\",\n                    \"properties\" => %{\n                      \"url\" => %{\"type\" => \"string\"},\n                      \"alt_text\" => %{\"type\" => \"string\"}\n                    }\n                  }\n                },\n                \"required\" => [\"name\"],\n                \"additionalProperties\" => false\n              }\n            },\n            \"required\" => [\n              \"version\",\n              \"credential_identifier\",\n              \"format\",\n              \"types\",\n              \"claims\",\n              \"display\"\n            ],\n            \"additionalProperties\" => false\n          }\n        },\n        \"verifiable_presentations\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"object\",\n            \"properties\" => %{\n              \"presentation_identifier\" => %{\"type\" => \"string\"},\n              \"presentation_definition\" => %{\"type\" => \"string\"}\n            },\n            \"required\" => [\n              \"presentation_identifier\",\n              \"presentation_definition\"\n            ],\n            \"additionalProperties\" => false\n          }\n        }\n      },\n      \"required\" => [],\n      \"additionalProperties\" => false\n    }\n    |> Schema.resolve()\n  end\n\n  def identity_provider do\n    %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"backend_id\" => %{\"type\" => \"string\"},\n        \"choose_session\" => %{\"type\" => \"boolean\"},\n        \"confirmable\" => %{\"type\" => \"boolean\"},\n        \"consentable\" => %{\"type\" => \"boolean\"},\n        \"enforce_totp\" => %{\"type\" => \"boolean\"},\n        \"id\" => %{\"type\" => \"string\"},\n        \"name\" => %{\"type\" => \"string\"},\n        \"registrable\" => %{\"type\" => \"boolean\"},\n        \"totpable\" => %{\"type\" => \"boolean\"},\n        \"user_editable\" => %{\"type\" => \"boolean\"},\n        \"templates\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"object\",\n            \"properties\" => %{\n              \"type\" => %{\"type\" => \"string\"},\n              \"content\" => %{\"type\" => \"string\"}\n            }\n          }\n        }\n      },\n      \"additionalProperties\" => false\n    }\n    |> Schema.resolve()\n  end\n\n  def client do\n    %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"access_token_ttl\" => %{\"type\" => \"number\"},\n        \"authorization_code_ttl\" => %{\"type\" => \"number\"},\n        \"authorization_request_ttl\" => %{\"type\" => \"number\"},\n        \"authorize_scope\" => %{\"type\" => \"boolean\"},\n        \"authorized_scopes\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"object\",\n            \"properties\" => %{\n              \"id\" => %{\"type\" => \"string\"},\n              \"name\" => %{\"type\" => \"string\"}\n            }\n          }\n        },\n        \"confidential\" => %{\"type\" => \"boolean\"},\n        \"enforce_dpop\" => %{\"type\" => \"boolean\"},\n        \"id\" => %{\"type\" => \"string\"},\n        \"id_token_signature_alg\" => %{\"type\" => \"string\"},\n        \"id_token_ttl\" => %{\"type\" => \"number\"},\n        \"identity_provider\" => %{\n          \"type\" => \"object\",\n          \"properties\" => %{\n            \"id\" => %{\"type\" => \"string\"}\n          }\n        },\n        \"jwt_public_key\" => %{\"type\" => \"string\"},\n        \"name\" => %{\"type\" => \"string\"},\n        \"pkce\" => %{\"type\" => \"boolean\"},\n        \"public_refresh_token\" => %{\"type\" => \"boolean\"},\n        \"public_revoke\" => %{\"type\" => \"boolean\"},\n        \"redirect_uris\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\"type\" => \"string\"}\n        },\n        \"refresh_token_ttl\" => %{\"type\" => \"number\"},\n        \"secret\" => %{\"type\" => \"string\"},\n        \"supported_grant_types\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\"type\" => \"string\"}\n        },\n        \"token_endpoint_auth_methods\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\"type\" => \"string\"}\n        },\n        \"token_endpoint_jwt_auth_alg\" => %{\"type\" => \"string\"},\n        \"userinfo_signed_response_alg\" => %{\"type\" => \"string\"}\n      },\n      \"required\" => [],\n      \"additionalProperties\" => false\n    }\n    |> Schema.resolve()\n  end\n\n  def scope do\n    %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"id\" => %{\"type\" => \"string\"},\n        \"name\" => %{\"type\" => \"string\"},\n        \"label\" => %{\"type\" => \"string\"},\n        \"public\" => %{\"type\" => \"boolean\"}\n      },\n      \"required\" => [],\n      \"additionalProperties\" => false\n    }\n  end\n\n  def role do\n    %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"id\" => %{\"type\" => \"string\"},\n        \"name\" => %{\"type\" => \"string\"},\n        \"scopes\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"object\",\n            \"properties\" => %{\n              \"id\" => %{\"type\" => \"string\"}\n            }\n          }\n        }\n      },\n      \"required\" => [],\n      \"additionalProperties\" => false\n    }\n  end\n\n  def error_template do\n    %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"type\" => %{\"type\" => \"string\"},\n        \"content\" => %{\"type\" => \"string\"}\n      },\n      \"additionalProperties\" => false\n    }\n    |> Schema.resolve()\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin/configuration_loader.ex",
    "content": "defmodule BorutaAdmin.ConfigurationLoader do\n  @moduledoc false\n\n  alias Boruta.Ecto.Admin\n  alias BorutaAdmin.ConfigurationLoader.Schema\n  alias BorutaGateway.Upstreams\n  alias BorutaIdentity.Clients\n  alias BorutaIdentity.Configuration\n  alias BorutaIdentity.Configuration.ErrorTemplate\n  alias BorutaIdentity.IdentityProviders\n  alias ExJsonSchema.Validator.Error.BorutaFormatter\n\n  @spec node_name() :: node_name :: String.t()\n  def node_name do\n    case Application.get_env(__MODULE__, :node_name) do\n      nil ->\n        path = Application.get_env(:boruta_admin, :configuration_path)\n\n        %{\n          \"configuration\" => %{\n            \"node_name\" => node_name\n          }\n        } = YamlElixir.read_from_file!(path)\n\n        Application.put_env(__MODULE__, :node_name, node_name)\n        node_name\n\n      node_name ->\n        node_name\n    end\n  rescue\n    _ ->\n      node_name = Atom.to_string(node())\n      Application.put_env(__MODULE__, :node_name, node_name)\n      node_name\n  end\n\n  @spec from_file!(configuration_file_path :: String.t()) ::\n          {:ok, result :: map()} | {:error, reason :: String.t()}\n  def from_file!(path) do\n    case YamlElixir.read_from_file!(path) do\n      %{\"configuration\" => configuration, \"version\" => \"1.0\"} ->\n        {:ok, load_configuration(configuration)}\n\n      _ ->\n        {:error, \"Bad configuration file.\"}\n    end\n  end\n\n  def load_configuration(configuration) do\n    load_configuration(configuration, %{})\n  end\n\n  def load_configuration(%{\"gateway\" => gateway_configurations} = configuration, result) when is_list(gateway_configurations) do\n    result =\n      Map.put(\n        result,\n        :gateway,\n        Enum.map(gateway_configurations, fn gateway_configuration ->\n          with :ok <-\n                 ExJsonSchema.Validator.validate(Schema.gateway(), gateway_configuration,\n                   error_formatter: BorutaFormatter\n                 ),\n               {:ok, upstream} <- Upstreams.create_upstream(gateway_configuration) do\n            {:ok, upstream}\n          else\n            {:error, %Ecto.Changeset{} = changeset} ->\n              {:error, [changeset]}\n\n            {:error, errors} ->\n              {:error, errors}\n          end\n        end)\n        |> Enum.flat_map(fn\n          {:ok, _upstream} -> []\n          {:error, errors} -> errors\n        end)\n      )\n\n    load_configuration(Map.delete(configuration, \"gateway\"), result)\n  end\n\n  def load_configuration(%{\"microgateway\" => gateway_configurations} = configuration, result) when is_list(gateway_configurations) do\n    result =\n      Map.put(\n        result,\n        :microgateway,\n        Enum.map(gateway_configurations, fn gateway_configuration ->\n          gateway_configuration =\n            Map.put(\n              gateway_configuration,\n              \"node_name\",\n              node_name()\n            )\n\n          with :ok <-\n                 ExJsonSchema.Validator.validate(\n                   Schema.microgateway(),\n                   gateway_configuration,\n                   error_formatter: BorutaFormatter\n                 ),\n               {:ok, upstream} <-\n                 Upstreams.create_upstream(gateway_configuration) do\n            {:ok, upstream}\n          else\n            {:error, %Ecto.Changeset{} = changeset} ->\n              {:error, [changeset]}\n\n            {:error, errors} ->\n              {:error, errors}\n          end\n        end)\n        |> Enum.flat_map(fn\n          {:ok, _upstream} -> []\n          {:error, errors} -> errors\n        end)\n      )\n\n    load_configuration(Map.delete(configuration, \"microgateway\"), result)\n  end\n\n  def load_configuration(%{\"organization\" => organization_configurations} = configuration, result) when is_list(organization_configurations) do\n    result =\n      Map.put(\n        result,\n        :organization,\n        Enum.map(organization_configurations, fn organization_configuration ->\n          with :ok <-\n                 ExJsonSchema.Validator.validate(\n                   Schema.organization(),\n                   organization_configuration,\n                   error_formatter: BorutaFormatter\n                 ),\n               {:ok, organization} <-\n                 BorutaIdentity.Admin.create_organization(organization_configuration) do\n            {:ok, organization}\n          else\n            {:error, %Ecto.Changeset{} = changeset} ->\n              {:error, [changeset]}\n\n            {:error, errors} ->\n              {:error, errors}\n          end\n        end)\n        |> Enum.flat_map(fn\n          {:ok, _organization} -> []\n          {:error, errors} -> errors\n        end)\n      )\n\n    load_configuration(Map.delete(configuration, \"organization\"), result)\n  end\n\n  def load_configuration(%{\"backend\" => backend_configurations} = configuration, result) when is_list(backend_configurations) do\n    result =\n      Map.put(\n        result,\n        :backend,\n        Enum.map(backend_configurations, fn backend_configuration ->\n          with :ok <-\n                 ExJsonSchema.Validator.validate(\n                   Schema.backend(),\n                   backend_configuration,\n                   error_formatter: BorutaFormatter\n                 ),\n               {:ok, backend} <-\n                 IdentityProviders.create_backend(backend_configuration) do\n            {:ok, backend}\n          else\n            {:error, %Ecto.Changeset{} = changeset} ->\n              {:error, [changeset]}\n\n            {:error, errors} ->\n              {:error, errors}\n          end\n        end)\n        |> Enum.flat_map(fn\n          {:ok, _backend} -> []\n          {:error, errors} -> errors\n        end)\n      )\n\n    load_configuration(Map.delete(configuration, \"backend\"), result)\n  end\n\n  def load_configuration(\n        %{\"identity_provider\" => identity_provider_configurations} = configuration,\n        result\n      ) when is_list(identity_provider_configurations) do\n    result =\n      Map.put(\n        result,\n        :identity_provider,\n        Enum.map(identity_provider_configurations, fn identity_provider_configuration ->\n          with :ok <-\n                 ExJsonSchema.Validator.validate(\n                   Schema.identity_provider(),\n                   identity_provider_configuration,\n                   error_formatter: BorutaFormatter\n                 ),\n               {:ok, identity_provider} <-\n                 IdentityProviders.create_identity_provider(identity_provider_configuration) do\n            {:ok, identity_provider}\n          else\n            {:error, %Ecto.Changeset{} = changeset} ->\n              {:error, [changeset]}\n\n            {:error, errors} ->\n              {:error, errors}\n          end\n        end)\n        |> Enum.flat_map(fn\n          {:ok, _identity_provider} -> []\n          {:error, errors} -> errors\n        end)\n      )\n\n    load_configuration(Map.delete(configuration, \"identity_provider\"), result)\n  end\n\n  def load_configuration(%{\"client\" => client_configurations} = configuration, result) when is_list(client_configurations) do\n    result =\n      Map.put(\n        result,\n        :client,\n        Enum.map(client_configurations, fn client_configuration ->\n          with :ok <-\n                 ExJsonSchema.Validator.validate(\n                   Schema.client(),\n                   client_configuration,\n                   error_formatter: BorutaFormatter\n                 ),\n               {:ok, client} <-\n                 Clients.create_client(client_configuration) do\n            {:ok, client}\n          else\n            {:error, %Ecto.Changeset{} = changeset} ->\n              {:error, [changeset]}\n\n            {:error, errors} ->\n              {:error, errors}\n          end\n        end)\n        |> Enum.flat_map(fn\n          {:ok, _client} -> []\n          {:error, errors} -> errors\n        end)\n      )\n\n    load_configuration(Map.delete(configuration, \"client\"), result)\n  end\n\n  def load_configuration(%{\"scope\" => scope_configurations} = configuration, result) when is_list(scope_configurations) do\n    result =\n      Map.put(\n        result,\n        :scope,\n        Enum.map(scope_configurations, fn scope_configuration ->\n          with :ok <-\n                 ExJsonSchema.Validator.validate(\n                   Schema.scope(),\n                   scope_configuration,\n                   error_formatter: BorutaFormatter\n                 ),\n               {:ok, scope} <-\n                 Admin.create_scope(scope_configuration) do\n            {:ok, scope}\n          else\n            {:error, %Ecto.Changeset{} = changeset} ->\n              {:error, [changeset]}\n\n            {:error, errors} ->\n              {:error, errors}\n          end\n        end)\n        |> Enum.flat_map(fn\n          {:ok, _scope} -> []\n          {:error, errors} -> errors\n        end)\n      )\n\n    load_configuration(Map.delete(configuration, \"scope\"), result)\n  end\n\n  def load_configuration(%{\"role\" => role_configurations} = configuration, result) when is_list(role_configurations) do\n    result =\n      Map.put(\n        result,\n        :role,\n        Enum.map(role_configurations, fn role_configuration ->\n          with :ok <-\n                 ExJsonSchema.Validator.validate(\n                   Schema.role(),\n                   role_configuration,\n                   error_formatter: BorutaFormatter\n                 ),\n               {:ok, role} <-\n                 BorutaIdentity.Admin.create_role(role_configuration) do\n            {:ok, role}\n          else\n            {:error, %Ecto.Changeset{} = changeset} ->\n              {:error, [changeset]}\n\n            {:error, errors} ->\n              {:error, errors}\n          end\n        end)\n        |> Enum.flat_map(fn\n          {:ok, _role} -> []\n          {:error, errors} -> errors\n        end)\n      )\n\n    load_configuration(Map.delete(configuration, \"role\"), result)\n  end\n\n  def load_configuration(\n        %{\"error_template\" => error_template_configurations} = configuration,\n        result\n      ) when is_list(error_template_configurations) do\n    result =\n      Map.put(\n        result,\n        :error_template,\n        Enum.map(error_template_configurations, fn error_template_configuration ->\n          with :ok <-\n                 ExJsonSchema.Validator.validate(\n                   Schema.error_template(),\n                   error_template_configuration,\n                   error_formatter: BorutaFormatter\n                 ),\n               template <-\n                 Configuration.get_error_template!(\n                   String.to_integer(error_template_configuration[\"type\"])\n                 ),\n               {:ok, %ErrorTemplate{} = template} <-\n                 Configuration.upsert_error_template(template, error_template_configuration) do\n            {:ok, template}\n          else\n            {:error, %Ecto.Changeset{} = changeset} ->\n              {:error, [changeset]}\n\n            {:error, errors} ->\n              {:error, errors}\n          end\n        end)\n        |> Enum.flat_map(fn\n          {:ok, _error_template} -> []\n          {:error, errors} -> errors\n        end)\n      )\n\n    load_configuration(Map.delete(configuration, \"error_template\"), result)\n  rescue\n    _e in Ecto.NoResultsError ->\n      result =\n        Map.put(\n          result,\n          :error_template,\n          [\"Error template does not exist.\"]\n        )\n\n      load_configuration(Map.delete(configuration, \"error_template\"), result)\n  end\n\n  def load_configuration(%{}, result), do: result\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin/configurations/configuration.ex",
    "content": "defmodule BorutaAdmin.Configurations.Configuration do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  @type t :: %__MODULE__{\n          id: String.t(),\n          name: String.t(),\n          value: String.t(),\n          inserted_at: DateTime.t(),\n          updated_at: DateTime.t()\n        }\n\n  @primary_key {:id, :binary_id, autogenerate: true}\n  @foreign_key_type :binary_id\n  schema \"configurations\" do\n    field(:name, :string)\n    field(:value, :string)\n\n    timestamps()\n  end\n\n  @doc false\n  def changeset(upstream, attrs) do\n    upstream\n    |> cast(attrs, [\n      :name,\n      :value\n    ])\n    |> validate_required([:name, :value])\n    |> unique_constraint(:name)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin/configurations.ex",
    "content": "defmodule BorutaAdmin.Configurations do\n  @moduledoc false\n\n  alias BorutaAdmin.Configurations.Configuration\n  alias BorutaAdmin.Repo\n\n  @spec upsert_configuration(name :: String.t(), value :: String.t()) ::\n          {:ok, Configuration.t()} | {:error, Ecto.Changeset.t()}\n  def upsert_configuration(name, value) do\n    %Configuration{}\n    |> Configuration.changeset(%{\n      name: name,\n      value: value\n    })\n    |> Repo.insert(on_conflict: :replace_all, conflict_target: [:name])\n  end\n\n  def list_configurations do\n    Repo.all(Configuration)\n  end\n\n  def get_configuration(name) do\n    Repo.get_by(Configuration, name: name)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin/logs.ex",
    "content": "defmodule BorutaAdmin.Logs.FileTooLargeError do\n  @enforce_keys [:message]\n  defexception [:message, plug_status: 422]\n\n  @type t :: %__MODULE__{\n          message: String.t()\n        }\n\n  def exception(message) when is_binary(message) do\n    %__MODULE__{message: message}\n  end\n\n  def message(exception) do\n    exception.message\n  end\nend\n\ndefmodule BorutaAdmin.Logs do\n  @moduledoc false\n\n  alias BorutaAuth.LogRotate\n\n  @max_file_size 100_000_000\n  @max_log_lines 10_000\n  @request_log_regex ~r/(\\d{4}-\\d{2}-\\d{2}T[^Z]+Z) request_id=([^\\s]+) \\[info\\] ([^\\s]+) (\\w+) ([^\\s]+) - (\\w+) (\\d{3}) from ([^\\s]+) in (\\d+)(\\w+)/\n  @business_event_log_regex ~r/(\\d{4}-\\d{2}-\\d{2}T[^Z]+Z) request_id=([^\\s]+) \\[info\\] ([^\\s]+) (\\w+) (\\w+) - (\\w+)(( ([^\\=]+)\\=((\\\".+\\\")|([^\\s]+)))+)/\n\n  @spec read(\n          start_at :: DateTime.t(),\n          end_at :: DateTime.t(),\n          application :: atom(),\n          type :: atom(),\n          query :: map()\n        ) :: Enumerable.t()\n  # credo:disable-for-next-line\n  def read(start_at, end_at, application, :request = type, query) do\n    time_scale_unit = time_scale_unit(start_at, end_at)\n\n    log_stream(start_at, end_at, application, type)\n    |> Stream.map(&parse_request_log/1)\n    |> Stream.reject(&is_nil/1)\n    |> apply_request_filters(query)\n    |> Enum.reduce(\n      %{\n        time_scale_unit: time_scale_unit,\n        overflow: false,\n        log_lines: [],\n        log_count: 0,\n        status_codes: %{},\n        request_counts: %{},\n        request_times: %{},\n        labels: []\n      },\n      fn %{\n           label: label,\n           log_line: log_line,\n           time: time,\n           status_code: status_code,\n           duration: duration,\n           duration_unit: duration_unit\n         },\n         %{\n           time_scale_unit: time_scale_unit,\n           overflow: overflow,\n           log_lines: log_lines,\n           log_count: log_count,\n           status_codes: status_codes,\n           request_counts: request_counts,\n           request_times: request_times,\n           labels: labels\n         } ->\n        overflow = overflow || log_count >= @max_log_lines\n        truncated_time = DateTime.truncate(time, :second)\n\n        truncated_time =\n          case time_scale_unit do\n            :second -> truncated_time\n            :minute -> %{truncated_time | second: 0}\n            :hour -> %{truncated_time | second: 0, minute: 0}\n          end\n\n        normalized_duration =\n          case duration_unit do\n            \"ms\" -> duration\n            \"µs\" -> duration / 1000\n          end\n\n        %{\n          time_scale_unit: time_scale_unit,\n          overflow: overflow,\n          log_lines:\n            case overflow do\n              true -> log_lines\n              false -> log_lines ++ [log_line]\n            end,\n          log_count: log_count + 1,\n          status_codes:\n            Map.merge(status_codes, %{label => %{status_code => 1}}, fn _, a, b ->\n              Map.merge(a, b, fn _, i, j -> i + j end)\n            end),\n          request_counts:\n            Map.merge(request_counts, %{label => %{truncated_time => 1}}, fn _, a, b ->\n              Map.merge(a, b, fn _, i, j -> i + j end)\n            end),\n          request_times:\n            Map.merge(\n              request_times,\n              %{label => %{truncated_time => normalized_duration}},\n              fn _, a, b ->\n                Map.merge(a, b, fn _, i, j -> (i + j) / 2 end)\n              end\n            ),\n          labels:\n            case Enum.member?(labels, label) do\n              false -> [label | labels] |> Enum.sort()\n              true -> labels\n            end\n        }\n      end\n    )\n  end\n\n  # credo:disable-for-next-line\n  def read(start_at, end_at, application, :business = type, query) do\n    time_scale_unit = time_scale_unit(start_at, end_at)\n\n    log_stream(start_at, end_at, application, type)\n    |> Stream.map(&parse_business_log/1)\n    |> Stream.reject(&is_nil/1)\n    |> apply_business_filters(query)\n    |> Enum.reduce(\n      %{\n        time_scale_unit: time_scale_unit,\n        overflow: false,\n        log_lines: [],\n        log_count: 0,\n        counts: %{},\n        business_event_counts: %{},\n        domains: [],\n        actions: []\n      },\n      fn %{\n           log_line: log_line,\n           time: time,\n           label: label,\n           status: status,\n           domain: domain,\n           action: action\n         },\n         %{\n           time_scale_unit: time_scale_unit,\n           overflow: overflow,\n           log_lines: log_lines,\n           log_count: log_count,\n           counts: counts,\n           business_event_counts: business_event_counts,\n           domains: domains,\n           actions: actions\n         } ->\n        overflow = overflow || log_count >= @max_log_lines\n        truncated_time = DateTime.truncate(time, :second)\n\n        truncated_time =\n          case time_scale_unit do\n            :second -> truncated_time\n            :minute -> %{truncated_time | second: 0}\n            :hour -> %{truncated_time | second: 0, minute: 0}\n          end\n\n        %{\n          time_scale_unit: time_scale_unit,\n          overflow: overflow,\n          log_lines:\n            case overflow do\n              true -> log_lines\n              false -> log_lines ++ [log_line]\n            end,\n          log_count: log_count + 1,\n          business_event_counts:\n            Map.merge(business_event_counts, %{label => %{truncated_time => 1}}, fn _, a, b ->\n              Map.merge(a, b, fn _, i, j -> i + j end)\n            end),\n          counts:\n            Map.merge(counts, %{label => %{status => 1}}, fn _, a, b ->\n              Map.merge(a, b, fn _, i, j -> i + j end)\n            end),\n          domains:\n            case Enum.member?(domains, domain) do\n              false -> [domain | domains] |> Enum.sort()\n              true -> domains\n            end,\n          actions:\n            case Enum.member?(actions, action) do\n              false -> [action | actions] |> Enum.sort()\n              true -> actions\n            end\n        }\n      end\n    )\n  end\n\n  def read(_start_at, _end_at, _application, _type), do: %{}\n\n  defp log_stream(start_at, end_at, application, type) do\n    paths =\n      log_dates(DateTime.to_date(start_at), DateTime.to_date(end_at))\n      |> Enum.map(&LogRotate.path(application, type, &1))\n      |> Enum.filter(&File.exists?/1)\n\n    if Enum.reduce(paths, 0, fn path, _acc -> File.stat!(path).size end) > @max_file_size do\n      raise BorutaAdmin.Logs.FileTooLargeError,\n            \"Requested for more than #{@max_file_size} bytes of logs, could not perform the request.\"\n    end\n\n    paths\n    |> Enum.map(&File.stream!/1)\n    |> Stream.concat()\n    |> Stream.drop_while(fn log ->\n      case DateTime.from_iso8601(String.split(log, \" \") |> List.first()) do\n        {:ok, log_time, _offset} ->\n          DateTime.compare(log_time, start_at) == :lt\n\n        _ ->\n          true\n      end\n    end)\n    |> Stream.take_while(fn log ->\n      case DateTime.from_iso8601(String.split(log, \" \") |> List.first()) do\n        {:ok, log_time, _offset} ->\n          DateTime.compare(log_time, end_at) == :lt\n\n        _ ->\n          true\n      end\n    end)\n  end\n\n  defp parse_request_log(log_line) do\n    case Regex.run(@request_log_regex, log_line) do\n      nil ->\n        nil\n\n      [\n        log_line,\n        raw_time,\n        request_id,\n        application,\n        method,\n        path,\n        _state,\n        status_code,\n        ip_address,\n        duration,\n        duration_unit\n      ] ->\n        with {:ok, time, _offset} <- DateTime.from_iso8601(raw_time) do\n          label_path = normalize_request_label_path(path)\n\n          %{\n            log_line: log_line,\n            time: time,\n            label: String.slice(\"#{application} - #{method} #{label_path}\", 0..70),\n            request_id: request_id,\n            application: application,\n            method: method,\n            path: path,\n            status_code: status_code,\n            ip_address: ip_address,\n            duration: String.to_integer(duration),\n            duration_unit: duration_unit\n          }\n        end\n    end\n  end\n\n  defp normalize_request_label_path(\"/openid/direct_post/\" <> code_id) when code_id != \"\",\n    do: \"/openid/direct_post/:code_id\"\n\n  defp normalize_request_label_path(path), do: path\n\n  def apply_request_filters(request_stream, query) do\n    Enum.reduce(query, request_stream, fn\n      {_key, nil}, stream ->\n        stream\n\n      {_key, \"\"}, stream ->\n        stream\n\n      {key, value}, stream when key in [:label] ->\n        Stream.filter(stream, fn\n          %{^key => ^value} -> true\n          _ -> false\n        end)\n\n      _, stream ->\n        stream\n    end)\n  end\n\n  def apply_business_filters(request_stream, query) do\n    Enum.reduce(query, request_stream, fn\n      {_key, nil}, stream ->\n        stream\n\n      {_key, \"\"}, stream ->\n        stream\n\n      {key, value}, stream when key in [:domain, :action] ->\n        Stream.filter(stream, fn\n          %{^key => ^value} -> true\n          _ -> false\n        end)\n\n      _, stream ->\n        stream\n    end)\n  end\n\n  defp parse_business_log(log_line) do\n    case Regex.run(@business_event_log_regex, log_line) do\n      nil ->\n        nil\n\n      [\n        log_line,\n        raw_time,\n        request_id,\n        application,\n        domain,\n        action,\n        status | _raw_attributes\n      ] ->\n        with {:ok, time, _offset} <- DateTime.from_iso8601(raw_time) do\n          %{\n            log_line: log_line,\n            time: time,\n            request_id: request_id,\n            label: String.slice(\"#{application} - #{domain} #{action}\", 0..70),\n            application: application,\n            domain: String.slice(\"#{application} - #{domain}\", 0..70),\n            action: String.slice(\"#{application} - #{domain} #{action}\", 0..70),\n            status: status\n          }\n        end\n    end\n  end\n\n  defp log_dates(start_date, end_date) do\n    if Date.compare(start_date, end_date) == :gt do\n      []\n    else\n      [start_date | log_dates(Date.add(start_date, 1), end_date)]\n    end\n  end\n\n  defp time_scale_unit(start_at, end_at) do\n    case DateTime.diff(end_at, start_at, :second) do\n      duration when duration < 60 * 60 -> :second\n      duration when duration < 60 * 60 * 24 -> :minute\n      _duration -> :hour\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin/release.ex",
    "content": "defmodule BorutaAdmin.Release do\n  @moduledoc false\n\n  def load_configuration do\n    Application.ensure_all_started(:boruta_admin)\n\n    configuration_path = Application.get_env(:boruta_admin, :configuration_path)\n\n    BorutaAdmin.ConfigurationLoader.from_file!(configuration_path)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin/repo.ex",
    "content": "defmodule BorutaAdmin.Repo do\n  use Ecto.Repo,\n    otp_app: :boruta_admin,\n    adapter: Ecto.Adapters.Postgres\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin.ex",
    "content": "defmodule BorutaAdmin do\n  @moduledoc \"\"\"\n  BorutaAdmin keeps the contexts that define your domain\n  and business logic.\n\n  Contexts are also responsible for managing your data, regardless\n  if it comes from the database, an external API or others.\n  \"\"\"\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/backend_controller.ex",
    "content": "defmodule BorutaAdminWeb.BackendController do\n  use BorutaAdminWeb, :controller\n\n  import BorutaAdminWeb.Authorization,\n    only: [\n      authorize: 2\n    ]\n\n  alias BorutaIdentity.Accounts.EmailTemplate\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  action_fallback(BorutaAdminWeb.FallbackController)\n\n  plug(:authorize, [\"identity-providers:manage:all\"])\n\n  def index(conn, _params) do\n    backends = IdentityProviders.list_backends()\n    render(conn, \"index.json\", backends: backends)\n  end\n\n  def create(conn, %{\"backend\" => backend_params}) do\n    with {:ok, %Backend{} = backend} <-\n           IdentityProviders.create_backend(backend_params) do\n      conn\n      |> put_status(:created)\n      |> put_resp_header(\"location\", Routes.admin_backend_path(conn, :show, backend))\n      |> render(\"show.json\", backend: backend)\n    end\n  end\n\n  def create(_conn, _params), do: {:error, :bad_request}\n\n  def show(conn, %{\"id\" => id}) do\n    backend = IdentityProviders.get_backend!(id)\n    render(conn, \"show.json\", backend: backend)\n  end\n\n  def update(conn, %{\"id\" => id, \"backend\" => backend_params}) do\n    backend = IdentityProviders.get_backend!(id)\n\n    with {:ok, %Backend{} = backend} <-\n           IdentityProviders.update_backend(backend, backend_params) do\n      render(conn, \"show.json\", backend: backend)\n    end\n  end\n\n  def update(_conn, _params), do: {:error, :bad_request}\n\n  def delete(conn, %{\"id\" => id}) do\n    backend = IdentityProviders.get_backend!(id)\n\n    # with :ok <- ensure_open_for_edition(id),\n    with {:ok, %Backend{}} <- IdentityProviders.delete_backend(backend) do\n      send_resp(conn, :no_content, \"\")\n    end\n  end\n\n  def email_template(conn, %{\"backend_id\" => id, \"template_type\" => template_type}) do\n    template = IdentityProviders.get_backend_email_template!(id, String.to_atom(template_type))\n    render(conn, \"show_email_template.json\", email_template: template)\n  end\n\n  def update_email_template(conn, %{\n        \"backend_id\" => id,\n        \"template_type\" => template_type,\n        \"template\" => template_params\n      }) do\n    template = IdentityProviders.get_backend_email_template!(id, String.to_atom(template_type))\n\n    with {:ok, %EmailTemplate{} = template} <-\n           IdentityProviders.upsert_email_template(template, template_params) do\n      render(conn, \"show_email_template.json\", email_template: template)\n    end\n  end\n\n  def delete_email_template(conn, %{\"backend_id\" => id, \"template_type\" => template_type}) do\n    template = IdentityProviders.delete_email_template!(id, String.to_atom(template_type))\n    render(conn, \"show_email_template.json\", email_template: template)\n  end\n\n  # TODO client backend association\n  # defp ensure_open_for_edition(backend_id) do\n  #   admin_ui_client_id =\n  #     System.get_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", \"6a2f41a3-c54c-fce8-32d2-0324e1c32e20\")\n\n  #   case IdentityProviders.get_backend_by_client_id(admin_ui_client_id) do\n  #     %Backend{id: ^backend_id} ->\n  #       {:error, :protected_resource}\n  #     _ ->\n  #       :ok\n  #   end\n  # end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/boruta/client_controller.ex",
    "content": "defmodule BorutaAdminWeb.ClientController do\n  use BorutaAdminWeb, :controller\n\n  import BorutaAdminWeb.Authorization,\n    only: [\n      authorize: 2\n    ]\n\n  alias Boruta.Ecto.Admin\n  alias Boruta.Ecto.Client\n  alias BorutaIdentity.Clients\n  alias BorutaIdentity.IdentityProviders\n\n  plug(:authorize, [\"clients:manage:all\"])\n\n  action_fallback(BorutaAdminWeb.FallbackController)\n\n  def index(conn, _params) do\n    clients = Admin.list_clients()\n\n    render(conn, \"index.json\", clients: clients)\n  end\n\n  def create(conn, %{\"client\" => client_params}) do\n    with {:ok, %Client{} = client} <- Clients.create_client(client_params) do\n      conn\n      |> put_status(:created)\n      |> put_resp_header(\"location\", Routes.admin_client_path(conn, :show, client))\n      |> render(\"show.json\", client: client)\n    end\n  end\n\n  def show(conn, %{\"id\" => client_id}) do\n    client = get_client(client_id)\n\n    render(conn, \"show.json\", client: client)\n  end\n\n  def update(conn, %{\"id\" => client_id, \"client\" => client_params}) do\n    client = get_client(client_id)\n\n    with :ok <- ensure_open_for_edition(client_id),\n         {:ok, %Client{} = client} <- update_client(client, client_params),\n         {:ok, client} <- Clients.insert_global_key_pair(client, client_params[\"key_pair_id\"]) do\n      render(conn, \"show.json\", client: client)\n    end\n  end\n\n  defp update_client(\n         client,\n         %{\"identity_provider\" => %{\"id\" => identity_provider_id}} = client_params\n       ) do\n    BorutaWeb.Repo.transaction(fn ->\n      with {:ok, client} <- Admin.update_client(client, client_params),\n           {:ok, _client_identity_provider} <-\n             IdentityProviders.upsert_client_identity_provider(\n               client.id,\n               identity_provider_id\n             ) do\n        client\n      else\n        {:error, error} ->\n          BorutaWeb.Repo.rollback(error)\n      end\n    end)\n  end\n\n  defp update_client(client, client_params) do\n    Admin.update_client(client, client_params)\n  end\n\n  def regenerate_did(conn, %{\"id\" => client_id}) do\n    client = get_client(client_id)\n\n    with {:ok, client} <- Admin.regenerate_client_did(client) do\n      render(conn, \"show.json\", client: client)\n    end\n  end\n\n  def regenerate_key_pair(conn, %{\"id\" => client_id}) do\n    client = get_client(client_id)\n\n    with :ok <- ensure_open_for_edition(client_id),\n         {:ok, client} <- Admin.regenerate_client_key_pair(client) do\n      render(conn, \"show.json\", client: client)\n    end\n  end\n\n  def delete(conn, %{\"id\" => client_id}) do\n    with :ok <- ensure_open_for_edition(client_id),\n         {:ok, _result} <- delete_client_multi(client_id) do\n      send_resp(conn, :no_content, \"\")\n    else\n      {:error, :protected_resource} ->\n        {:error, :protected_resource}\n\n      {:error, _failed_operation, changeset, _changes} ->\n        {:error, changeset}\n    end\n  end\n\n  defp get_client(client_id) do\n    Admin.get_client!(client_id)\n  end\n\n  # TODO protect public client\n  defp ensure_open_for_edition(client_id) do\n    admin_ui_client_id =\n      System.get_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", \"6a2f41a3-c54c-fce8-32d2-0324e1c32e20\")\n\n    case client_id do\n      ^admin_ui_client_id -> {:error, :protected_resource}\n      _ -> :ok\n    end\n  end\n\n  defp delete_client_multi(client_id) do\n    Ecto.Multi.new()\n    |> Ecto.Multi.run(:delete_client, fn _repo, _changes ->\n      client = get_client(client_id)\n      Admin.delete_client(client)\n    end)\n    |> Ecto.Multi.run(:delete_client_identity_provider_association, fn _repo, _changes ->\n      IdentityProviders.remove_client_identity_provider(client_id)\n    end)\n    |> BorutaAuth.Repo.transaction()\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/boruta/scope_controller.ex",
    "content": "defmodule BorutaAdminWeb.ScopeController do\n  use BorutaAdminWeb, :controller\n\n  import BorutaAdminWeb.Authorization,\n    only: [\n      authorize: 2\n    ]\n\n  alias Boruta.Ecto.Admin\n  alias Boruta.Ecto.Scope\n\n  @protected_scopes [\n    \"users:manage:all\",\n    \"clients:manage:all\",\n    \"identity-providers:manage:all\",\n    \"scopes:manage:all\",\n    \"roles:manage:all\",\n    \"upstreams:manage:all\"\n  ]\n\n  plug(:authorize, [\"scopes:manage:all\"])\n\n  action_fallback(BorutaAdminWeb.FallbackController)\n\n  def index(conn, _params) do\n    scopes = Admin.list_scopes()\n    render(conn, \"index.json\", scopes: scopes)\n  end\n\n  def create(conn, %{\"scope\" => scope_params}) do\n    with {:ok, %Scope{} = scope} <- Admin.create_scope(scope_params) do\n      conn\n      |> put_status(:created)\n      |> put_resp_header(\"location\", Routes.admin_scope_path(conn, :show, scope))\n      |> render(\"show.json\", scope: scope)\n    end\n  end\n\n  def show(conn, %{\"id\" => id}) do\n    scope = Admin.get_scope!(id)\n    render(conn, \"show.json\", scope: scope)\n  end\n\n  def update(conn, %{\"id\" => id, \"scope\" => scope_params}) do\n    scope = Admin.get_scope!(id)\n\n    with :ok <- ensure_open_for_edition(scope),\n         {:ok, %Scope{} = scope} <- Admin.update_scope(scope, scope_params) do\n      render(conn, \"show.json\", scope: scope)\n    end\n  end\n\n  def delete(conn, %{\"id\" => id}) do\n    scope = Admin.get_scope!(id)\n\n    with :ok <- ensure_open_for_edition(scope),\n         {:ok, _changes} <- BorutaAuth.Repo.transaction(delete_scope_multi(scope)) do\n      send_resp(conn, :no_content, \"\")\n    end\n  end\n\n  def ensure_open_for_edition(scope) do\n    case Enum.member?(@protected_scopes, scope.name) do\n      true -> {:error, :protected_resource}\n      false -> :ok\n    end\n  end\n\n  defp delete_scope_multi(scope) do\n    Ecto.Multi.new()\n    |> Ecto.Multi.run(:delete_user_scopes, fn _repo, _changes ->\n      with {_deleted, nil} <- BorutaIdentity.Admin.delete_user_authorized_scopes_by_id(scope.id) do\n        {:ok, nil}\n      end\n    end)\n    |> Ecto.Multi.run(:delete_scope, fn _repo, _changes ->\n      Admin.delete_scope(scope)\n    end)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/configuration_controller.ex",
    "content": "defmodule BorutaAdminWeb.ConfigurationController do\n  use BorutaAdminWeb, :controller\n\n  import Boruta.Config, only: [issuer: 0]\n\n  import BorutaAdminWeb.Authorization,\n    only: [\n      authorize: 2\n    ]\n\n  alias BorutaAdmin.ConfigurationLoader\n  alias BorutaAdmin.Configurations\n  alias BorutaIdentity.Configuration\n  alias BorutaIdentity.Configuration.ErrorTemplate\n\n  action_fallback(BorutaAdminWeb.FallbackController)\n\n  plug(:authorize, [\"configuration:manage:all\"])\n\n  @resource %{\n    \"client\" => \"clients\",\n    \"identity_provider\" => \"identity-providers\",\n    \"backend\" => \"identity-providers\",\n    \"role\" => \"scopes\",\n    \"scope\" => \"scopes\",\n    \"gateway\" => \"upstreams\",\n    \"microgateway\" => \"upstreams\",\n    \"error_template\" => \"configuration\"\n  }\n\n  def error_template(conn, %{\"template_type\" => template_type}) do\n    template = Configuration.get_error_template!(String.to_integer(template_type))\n    render(conn, \"show_error_template.json\", template: template)\n  end\n\n  def update_error_template(conn, %{\n        \"template_type\" => template_type,\n        \"template\" => template_params\n      }) do\n    template = Configuration.get_error_template!(String.to_integer(template_type))\n\n    with {:ok, %ErrorTemplate{} = template} <-\n           Configuration.upsert_error_template(template, template_params) do\n      render(conn, \"show_error_template.json\", template: template)\n    end\n  end\n\n  def delete_error_template(conn, %{\"template_type\" => template_type}) do\n    template = Configuration.delete_error_template!(String.to_integer(template_type))\n    render(conn, \"show_error_template.json\", template: template)\n  end\n\n  def example_configuration_file(conn, _params) do\n    content =\n      :code.priv_dir(:boruta_admin)\n      |> Path.join(\"/examples/configuration.yml\")\n      |> File.read!()\n\n    content =\n      String.replace(\n        content,\n        \"{{PREAUTHORIZED_CODE_REDIRECT_URI}}\",\n        issuer() <>\n          # credo:disable-for-next-line\n          BorutaIdentityWeb.Router.Helpers.wallet_path(BorutaIdentityWeb.Endpoint, :index)\n      )\n\n    content =\n      String.replace(\n        content,\n        \"{{PRESENTATION_REDIRECT_URI}}\",\n        issuer() <>\n          # credo:disable-for-next-line\n          BorutaIdentityWeb.Router.Helpers.wallet_path(BorutaIdentityWeb.Endpoint, :index)\n      )\n\n    configurations = [\n      %{\n        name: \"configuration_file\",\n        value: content\n      }\n    ]\n\n    render(conn, \"configuration.json\", configurations: configurations)\n  end\n\n  def configuration(conn, _params) do\n    configurations = Configurations.list_configurations()\n\n    render(conn, \"configuration.json\", configurations: configurations)\n  end\n\n  def upload_configuration_file(conn, %{\"file\" => %Plug.Upload{path: path}}) do\n    file_content = File.read!(path)\n\n    with %{\"configuration\" => %{} = configuration, \"version\" => \"1.0\"} <-\n           YamlElixir.read_from_file!(path),\n         configuration <- filter_configuration(configuration, conn.assigns[:authorization]),\n         result <- ConfigurationLoader.load_configuration(configuration) do\n      # TODO perform a transaction\n      Configurations.upsert_configuration(\"configuration_file\", file_content)\n      render(conn, \"file_upload.json\", result: result, file_content: file_content)\n    else\n      _ ->\n        {:error, :bad_request}\n    end\n  end\n\n  defp filter_configuration(configuration, %{\"scope\" => scope}) do\n    Enum.filter(configuration, fn {key, _value} ->\n      Regex.match?(~r{#{@resource[key]}:manage:all}, scope)\n    end)\n    |> Enum.into(%{})\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/fallback_controller.ex",
    "content": "defmodule BorutaAdminWeb.FallbackController do\n  @moduledoc \"\"\"\n  Translates controller action results into valid `Plug.Conn` responses.\n\n  See `Phoenix.Controller.action_fallback/1` for more details.\n  \"\"\"\n  use BorutaAdminWeb, :controller\n\n  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do\n    conn\n    |> put_status(:unprocessable_entity)\n    |> put_view(BorutaAdminWeb.ChangesetView)\n    |> render(\"error.json\", changeset: changeset)\n  end\n\n  def call(conn, {:error, :bad_request}) do\n    conn\n    |> put_status(:bad_request)\n    |> put_view(BorutaAdminWeb.ErrorView)\n    |> render(\"400.\" <> get_format(conn))\n  end\n\n  def call(conn, {:error, :not_found}) do\n    conn\n    |> put_status(:not_found)\n    |> put_view(BorutaAdminWeb.ErrorView)\n    |> render(\"404.\" <> get_format(conn))\n  end\n\n  def call(conn, {:error, :unauthorized}) do\n    conn\n    |> put_status(:unauthorized)\n    |> put_view(BorutaAdminWeb.ErrorView)\n    |> render(\"401.\" <> get_format(conn))\n  end\n\n  def call(conn, {:error, :forbidden}) do\n    conn\n    |> put_status(:forbidden)\n    |> put_view(BorutaAdminWeb.ErrorView)\n    |> render(\"403.\" <> get_format(conn))\n  end\n\n  def call(conn, {:error, :protected_resource}) do\n    conn\n    |> put_status(:forbidden)\n    |> put_view(BorutaAdminWeb.ErrorView)\n    |> render(\"protected_resource.\" <> get_format(conn))\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/identity_provider_controller.ex",
    "content": "defmodule BorutaAdminWeb.IdentityProviderController do\n  use BorutaAdminWeb, :controller\n\n  import BorutaAdminWeb.Authorization,\n    only: [\n      authorize: 2\n    ]\n\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.IdentityProviders.Template\n\n  action_fallback(BorutaAdminWeb.FallbackController)\n\n  plug(:authorize, [\"identity-providers:manage:all\"])\n\n  def index(conn, _params) do\n    identity_providers = IdentityProviders.list_identity_providers()\n    render(conn, \"index.json\", identity_providers: identity_providers)\n  end\n\n  def create(conn, %{\"identity_provider\" => identity_provider_params}) do\n    with {:ok, %IdentityProvider{} = identity_provider} <-\n           IdentityProviders.create_identity_provider(identity_provider_params) do\n      conn\n      |> put_status(:created)\n      |> put_resp_header(\"location\", Routes.admin_identity_provider_path(conn, :show, identity_provider))\n      |> render(\"show.json\", identity_provider: identity_provider)\n    end\n  end\n\n  def show(conn, %{\"id\" => id}) do\n    identity_provider = IdentityProviders.get_identity_provider!(id)\n    render(conn, \"show.json\", identity_provider: identity_provider)\n  end\n\n  def update(conn, %{\"id\" => id, \"identity_provider\" => identity_provider_params}) do\n    identity_provider = IdentityProviders.get_identity_provider!(id)\n\n    with {:ok, %IdentityProvider{} = identity_provider} <-\n           IdentityProviders.update_identity_provider(identity_provider, identity_provider_params) do\n      render(conn, \"show.json\", identity_provider: identity_provider)\n    end\n  end\n\n  def delete(conn, %{\"id\" => id}) do\n    identity_provider = IdentityProviders.get_identity_provider!(id)\n\n    with :ok <- ensure_open_for_edition(id),\n         {:ok, %IdentityProvider{}} <- IdentityProviders.delete_identity_provider(identity_provider) do\n      send_resp(conn, :no_content, \"\")\n    end\n  end\n\n  def template(conn, %{\"identity_provider_id\" => id, \"template_type\" => template_type}) do\n    template = IdentityProviders.get_identity_provider_template!(id, String.to_atom(template_type))\n    render(conn, \"show_template.json\", template: template)\n  end\n\n  def update_template(conn, %{\n        \"identity_provider_id\" => id,\n        \"template_type\" => template_type,\n        \"template\" => template_params\n      }) do\n    template = IdentityProviders.get_identity_provider_template!(id, String.to_atom(template_type))\n\n    with {:ok, %Template{} = template} <-\n           IdentityProviders.upsert_template(template, template_params) do\n      render(conn, \"show_template.json\", template: template)\n    end\n  end\n\n  def delete_template(conn, %{\"identity_provider_id\" => id, \"template_type\" => template_type}) do\n    template = IdentityProviders.delete_identity_provider_template!(id, String.to_atom(template_type))\n    render(conn, \"show_template.json\", template: template)\n  end\n\n  defp ensure_open_for_edition(identity_provider_id) do\n    admin_ui_client_id =\n      System.get_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", \"6a2f41a3-c54c-fce8-32d2-0324e1c32e20\")\n\n    case IdentityProviders.get_identity_provider_by_client_id(admin_ui_client_id) do\n      %IdentityProvider{id: ^identity_provider_id} ->\n        {:error, :protected_resource}\n      _ ->\n        :ok\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/key_pair_controller.ex",
    "content": "defmodule BorutaAdminWeb.KeyPairController do\n  use BorutaAdminWeb, :controller\n\n  import BorutaAdminWeb.Authorization,\n    only: [\n      authorize: 2\n    ]\n\n  alias BorutaAuth.KeyPairs\n  alias BorutaAuth.KeyPairs.KeyPair\n\n  plug(:authorize, [\"clients:manage:all\"])\n\n  action_fallback(BorutaAdminWeb.FallbackController)\n\n  def index(conn, _params) do\n    key_pairs = KeyPairs.list_key_pairs()\n    render(conn, \"index.json\", key_pairs: key_pairs)\n  end\n\n  def create(conn, %{\"key_pair\" => key_pair_params}) do\n    with {:ok, %KeyPair{} = key_pair} <- KeyPairs.create_key_pair(key_pair_params) do\n      conn\n      |> put_status(:created)\n      |> put_resp_header(\"location\", Routes.admin_key_pair_path(conn, :show, key_pair))\n      |> render(\"show.json\", key_pair: key_pair)\n    end\n  end\n\n  def show(conn, %{\"id\" => id}) do\n    key_pair = KeyPairs.get_key_pair!(id)\n    render(conn, \"show.json\", key_pair: key_pair)\n  end\n\n  def rotate(conn, %{\"id\" => id}) do\n    key_pair = KeyPairs.get_key_pair!(id)\n\n    with {:ok, key_pair} <- KeyPairs.rotate(key_pair) do\n      render(conn, \"show.json\", key_pair: key_pair)\n    end\n  end\n\n  def update(conn, %{\"id\" => id, \"key_pair\" => key_pair_params}) do\n    key_pair = KeyPairs.get_key_pair!(id)\n\n    with {:ok, %KeyPair{} = key_pair} <- KeyPairs.update_key_pair(key_pair, key_pair_params) do\n      render(conn, \"show.json\", key_pair: key_pair)\n    end\n  end\n\n  def delete(conn, %{\"id\" => id}) do\n    key_pair = KeyPairs.get_key_pair!(id)\n\n    with {:ok, _key_pair} <- KeyPairs.delete_key_pair(key_pair) do\n      send_resp(conn, :no_content, \"\")\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/logs_controller.ex",
    "content": "defmodule BorutaAdminWeb.LogsController do\n  use BorutaAdminWeb, :controller\n\n  import BorutaAdminWeb.Authorization,\n    only: [\n      authorize: 2\n    ]\n\n  alias BorutaAdmin.Logs\n\n  action_fallback(BorutaAdminWeb.FallbackController)\n\n  plug(:authorize, [\"logs:read:all\"])\n\n  def index(\n        conn,\n        %{\n          \"start_at\" => start_at,\n          \"end_at\" => end_at,\n          \"application\" => application,\n          \"type\" => type\n        } = params\n      ) do\n    with {:ok, start_at, _offset} <- DateTime.from_iso8601(start_at),\n         {:ok, end_at, _offset} <- DateTime.from_iso8601(end_at) do\n      query =\n        (params[\"query\"] || %{})\n        |> Enum.map(fn {key, value} -> {String.to_atom(key), value} end)\n        |> Enum.into(%{})\n\n      log_stream = Logs.read(start_at, end_at, String.to_atom(application), String.to_atom(type), query)\n\n      conn\n      |> render(\"index.json\", stats: Enum.into(log_stream, %{}))\n    else\n      _ ->\n        {:error, :bad_request}\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/organization_controller.ex",
    "content": "defmodule BorutaAdminWeb.OrganizationController do\n  use BorutaAdminWeb, :controller\n\n  import BorutaAdminWeb.Authorization,\n    only: [\n      authorize: 2\n    ]\n\n  alias BorutaIdentity.Admin\n  alias BorutaIdentity.Organizations.Organization\n\n  plug(:authorize, [\"users:manage:all\"])\n\n  action_fallback(BorutaAdminWeb.FallbackController)\n\n  def index(conn, params) do\n    organizations =\n      case params[\"q\"] do\n        nil -> Admin.list_organizations(params)\n        # query -> Admin.search_organizations(query, params)\n      end\n\n    render(conn, \"index.json\",\n      organizations: organizations.entries,\n      page_number: organizations.page_number,\n      page_size: organizations.page_size,\n      total_pages: organizations.total_pages,\n      total_entries: organizations.total_entries\n    )\n  end\n\n  def show(conn, %{\"id\" => id}) do\n    case Admin.get_organization(id) do\n      %Organization{} = organization ->\n        render(conn, \"show.json\", organization: organization)\n\n      nil ->\n        {:error, :not_found}\n    end\n  end\n\n  def create(conn, %{\"organization\" => organization_params}) do\n    create_params = %{\n      name: organization_params[\"name\"],\n      label: organization_params[\"label\"],\n    }\n\n    with {:ok, organization} <- Admin.create_organization(create_params) do\n      render(conn, \"show.json\", organization: organization)\n    end\n  end\n\n  def create(_conn, _params), do: {:error, :bad_request}\n\n  def update(conn, %{\"id\" => id, \"organization\" => organization_params}) do\n    update_params = %{\n      name: organization_params[\"name\"],\n      label: organization_params[\"label\"],\n    }\n\n    with :ok <- ensure_open_for_edition(id, conn),\n         %Organization{} = organization <- Admin.get_organization(id),\n         {:ok, %Organization{} = organization} <-\n           Admin.update_organization(organization, update_params) do\n      render(conn, \"show.json\", organization: organization)\n    else\n      nil -> {:error, :not_found}\n      error -> error\n    end\n  end\n\n  def update(_conn, _params), do: {:error, :bad_request}\n\n  def delete(conn, %{\"id\" => organization}) do\n    with :ok <- ensure_open_for_edition(organization, conn),\n         {:ok, _organization} <- Admin.delete_organization(organization) do\n      send_resp(conn, 204, \"\")\n    end\n  end\n\n  defp ensure_open_for_edition(_user_id, _conn) do\n    :ok\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/page_controller.ex",
    "content": "defmodule BorutaAdminWeb.PageController do\n  use BorutaAdminWeb, :controller\n\n  def index(conn, _params) do\n    conn\n    |> put_layout(false)\n    |> render(\"admin.html\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/role_controller.ex",
    "content": "defmodule BorutaAdminWeb.RoleController do\n  use BorutaAdminWeb, :controller\n\n  import BorutaAdminWeb.Authorization,\n    only: [\n      authorize: 2\n    ]\n\n  alias BorutaIdentity.Accounts.Role\n  alias BorutaIdentity.Admin\n\n  plug(:authorize, [\"scopes:manage:all\"])\n\n  action_fallback(BorutaAdminWeb.FallbackController)\n\n  def index(conn, _params) do\n    roles = Admin.list_roles()\n    render(conn, \"index.json\", roles: roles)\n  end\n\n  def create(conn, %{\"role\" => role_params}) do\n    with {:ok, %Role{} = role} <- Admin.create_role(role_params) do\n      conn\n      |> put_status(:created)\n      |> put_resp_header(\"location\", Routes.admin_role_path(conn, :show, role))\n      |> render(\"show.json\", role: role)\n    end\n  end\n\n  def show(conn, %{\"id\" => id}) do\n    role = Admin.get_role!(id)\n    render(conn, \"show.json\", role: role)\n  end\n\n  def update(conn, %{\"id\" => id, \"role\" => role_params}) do\n    role = Admin.get_role!(id)\n\n    with :ok <- ensure_open_for_edition(role),\n         {:ok, %Role{} = role} <- Admin.update_role(role, role_params) do\n      render(conn, \"show.json\", role: role)\n    end\n  end\n\n  def delete(conn, %{\"id\" => id}) do\n    role = Admin.get_role!(id)\n\n    with :ok <- ensure_open_for_edition(role),\n         {:ok, _changes} <- Admin.delete_role(role) do\n      send_resp(conn, :no_content, \"\")\n    end\n  end\n\n  def ensure_open_for_edition(_role) do\n    :ok\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/upstream_controller.ex",
    "content": "defmodule BorutaAdminWeb.UpstreamController do\n  use BorutaAdminWeb, :controller\n\n  import BorutaAdminWeb.Authorization, only: [\n    authorize: 2\n  ]\n\n  alias BorutaGateway.ConfigurationLoader\n  alias BorutaGateway.Upstreams\n  alias BorutaGateway.Upstreams.Upstream\n\n  plug :authorize, [\"upstreams:manage:all\"]\n\n  action_fallback BorutaAdminWeb.FallbackController\n\n  def index(conn, _params) do\n    upstreams = Upstreams.list_upstreams()\n    render(conn, \"index.json\", upstreams: upstreams)\n  end\n\n  def node_list(conn, _params) do\n    nodes = [node() | Node.list()]\n            |> Enum.map(fn node ->\n              :rpc.call(node, ConfigurationLoader, :node_name, [])\n            end)\n            |> Enum.uniq()\n    render(conn, \"node_list.json\", nodes: nodes)\n  end\n\n  def show(conn, %{\"id\" => id}) do\n    upstream = Upstreams.get_upstream!(id)\n    render(conn, \"show.json\", upstream: upstream)\n  end\n\n  def create(conn, %{\"upstream\" => upstream_params}) do\n    with {:ok, %Upstream{} = upstream} <- Upstreams.create_upstream(upstream_params) do\n      conn\n      |> put_status(:created)\n      |> put_resp_header(\"location\", Routes.admin_upstream_path(conn, :show, upstream))\n      |> render(\"show.json\", upstream: upstream)\n    end\n  end\n\n  def update(conn, %{\"id\" => id, \"upstream\" => upstream_params}) do\n    upstream = Upstreams.get_upstream!(id)\n\n    with {:ok, %Upstream{} = upstream} <- Upstreams.update_upstream(upstream, upstream_params) do\n      render(conn, \"show.json\", upstream: upstream)\n    end\n  end\n\n  def delete(conn, %{\"id\" => id}) do\n    upstream = Upstreams.get_upstream!(id)\n\n    with {:ok, %Upstream{}} <- Upstreams.delete_upstream(upstream) do\n      send_resp(conn, :no_content, \"\")\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/controllers/user_controller.ex",
    "content": "defmodule BorutaAdminWeb.UserController do\n  use BorutaAdminWeb, :controller\n\n  import BorutaAdminWeb.Authorization,\n    only: [\n      authorize: 2\n    ]\n\n  alias BorutaIdentity.Accounts.LdapError\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Admin\n  alias BorutaIdentity.IdentityProviders\n\n  plug(:authorize, [\"users:manage:all\"])\n\n  action_fallback(BorutaAdminWeb.FallbackController)\n\n  def index(conn, params) do\n    users =\n      case params[\"q\"] do\n        nil -> Admin.list_users(params)\n        query -> Admin.search_users(query, params)\n      end\n\n    render(conn, \"index.json\",\n      users: users.entries,\n      page_number: users.page_number,\n      page_size: users.page_size,\n      total_pages: users.total_pages,\n      total_entries: users.total_entries\n    )\n  end\n\n  def show(conn, %{\"id\" => id}) do\n    case Admin.get_user(id) do\n      %User{} = user ->\n        render(conn, \"show.json\", user: user)\n\n      nil ->\n        {:error, :not_found}\n    end\n  end\n\n  def create(conn, %{\"backend_id\" => backend_id, \"user\" => user_params}) do\n    create_params = %{\n      username: user_params[\"email\"],\n      group: user_params[\"group\"],\n      password: user_params[\"password\"],\n      metadata: user_params[\"metadata\"] || %{},\n      authorized_scopes: user_params[\"authorized_scopes\"],\n      organizations: user_params[\"organizations\"],\n      roles: user_params[\"roles\"]\n    }\n\n    backend = IdentityProviders.get_backend!(backend_id)\n\n    with {:ok, user} <- Admin.create_user(backend, create_params) do\n      render(conn, \"show.json\", user: user)\n    end\n  rescue\n    _e in Ecto.NoResultsError ->\n      {:error,\n       Ecto.Changeset.change(%User{}) |> Ecto.Changeset.add_error(:backend, \"does not exist\")}\n\n    error in LdapError ->\n      {:error,\n       Ecto.Changeset.change(%User{}) |> Ecto.Changeset.add_error(:backend, error.message)}\n  end\n\n  def create(conn, %{\"backend_id\" => backend_id, \"file\" => file_params} = import_params) do\n    backend = IdentityProviders.get_backend!(backend_id)\n\n    import_users_opts =\n      (import_params[\"options\"] || %{})\n      |> Enum.map(fn\n        {\"metadata_headers\" = k, v} -> {String.to_atom(k), v}\n        {\"username_header\" = k, v} -> {String.to_atom(k), v}\n        {\"password_header\" = k, v} -> {String.to_atom(k), v}\n        {\"hash_password\" = k, \"true\"} -> {String.to_atom(k), true}\n        {\"hash_password\" = k, \"false\"} -> {String.to_atom(k), false}\n        {\"hash_password\" = k, v} when is_boolean(v) -> {String.to_atom(k), v}\n        {_k, _v} -> nil\n      end)\n      |> Enum.reject(&is_nil/1)\n      |> Enum.into(%{})\n\n    case file_params do\n      %Plug.Upload{} ->\n        result = Admin.import_users(backend, file_params.path, import_users_opts)\n\n        render(conn, \"import_result.json\", import_result: result)\n\n      _ ->\n        {:error, Ecto.Changeset.change(%User{}) |> Ecto.Changeset.add_error(:file, \"is invalid\")}\n    end\n  rescue\n    _e in Ecto.NoResultsError ->\n      {:error,\n       Ecto.Changeset.change(%User{}) |> Ecto.Changeset.add_error(:backend, \"does not exist\")}\n\n    error in LdapError ->\n      {:error,\n       Ecto.Changeset.change(%User{}) |> Ecto.Changeset.add_error(:backend, error.message)}\n  end\n\n  def create(_conn, _params), do: {:error, :bad_request}\n\n  def update(conn, %{\"id\" => id, \"user\" => user_params}) do\n    update_params = %{\n      username: user_params[\"email\"],\n      group: user_params[\"group\"],\n      metadata: user_params[\"metadata\"] || %{},\n      authorized_scopes: user_params[\"authorized_scopes\"],\n      organizations: user_params[\"organizations\"],\n      roles: user_params[\"roles\"]\n    }\n\n    with :ok <- ensure_open_for_edition(id, conn),\n         %User{} = user <- Admin.get_user(id),\n         # TODO update user email and password\n         {:ok, %User{} = user} <- Admin.update_user(user, update_params) do\n      render(conn, \"show.json\", user: user)\n    else\n      nil -> {:error, :not_found}\n      error -> error\n    end\n  end\n\n  def update(_conn, _params), do: {:error, :bad_request}\n\n  def delete(conn, %{\"id\" => user_id}) do\n    with :ok <- ensure_open_for_edition(user_id, conn),\n         {:ok, _user} <- Admin.delete_user(user_id) do\n      send_resp(conn, 204, \"\")\n    else\n      {:error, \"\" <> reason} ->\n        {:error, Ecto.Changeset.change(%User{}) |> Ecto.Changeset.add_error(:backend, reason)}\n\n      error ->\n        error\n    end\n  end\n\n  defp ensure_open_for_edition(user_id, conn) do\n    %{\"sub\" => sub} = conn.assigns[:authorization]\n\n    case user_id == sub do\n      true -> {:error, :protected_resource}\n      false -> :ok\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/endpoint.ex",
    "content": "defmodule BorutaAdminWeb.Endpoint do\n  use Phoenix.Endpoint, otp_app: :boruta_admin\n\n  # sets the same session as :boruta_web\n  @session_options [\n    store: :cookie,\n    key: \"_boruta_web_key\",\n    signing_salt: \"OCKBuS86\"\n  ]\n\n  plug RemoteIp\n  plug Plug.Static,\n    at: \"/\",\n    from: :boruta_admin,\n    gzip: false,\n    only: ~w(assets favicon.ico semantic-ui.min.css prism-dark.min.css themes)\n\n  if code_reloading? do\n    socket \"/phoenix/live_reload/socket\", Phoenix.LiveReloader.Socket\n    plug Phoenix.LiveReloader\n    plug Phoenix.CodeReloader\n    plug Phoenix.Ecto.CheckRepoStatus, otp_app: :boruta_admin\n  end\n\n  plug Plug.RequestId\n  plug BorutaAdminWeb.Logger\n\n  plug Plug.Parsers,\n    parsers: [:urlencoded, :multipart, :json],\n    pass: [\"*/*\"],\n    json_decoder: Phoenix.json_library()\n\n  plug Plug.MethodOverride\n  plug Plug.Head\n  plug Plug.Session, @session_options\n  plug BorutaAdminWeb.Router\n\n  def log_level(%{path_info: [\"healthcheck\" | _]}), do: false\n  def log_level(%{path_info: [\"favicon.ico\" | _]}), do: false\n  def log_level(_), do: :info\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/gettext.ex",
    "content": "defmodule BorutaAdminWeb.Gettext do\n  @moduledoc \"\"\"\n  A module providing Internationalization with a gettext-based API.\n\n  By using [Gettext](https://hexdocs.pm/gettext),\n  your module gains a set of macros for translations, for example:\n\n      import BorutaAdminWeb.Gettext\n\n      # Simple translation\n      gettext(\"Here is the string to translate\")\n\n      # Plural translation\n      ngettext(\"Here is the string to translate\",\n               \"Here are the strings to translate\",\n               3)\n\n      # Domain-based translation\n      dgettext(\"errors\", \"Here is the error message to translate\")\n\n  See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.\n  \"\"\"\n  use Gettext.Backend, otp_app: :boruta_admin\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/plugs/authorization.ex",
    "content": "defmodule BorutaAdminWeb.Authorization do\n  @moduledoc false\n  @dialyzer {:no_unused, {:maybe_validate_user, 1}}\n\n  @behaviour Boruta.Openid.UserinfoApplication\n\n  require Logger\n\n  alias Boruta.Oauth.Authorization.ResourceOwner\n  use BorutaAdminWeb, :controller\n\n  alias Boruta.Oauth.ResourceOwner\n  alias Boruta.Openid.UserinfoResponse\n  alias BorutaAdminWeb.ErrorView\n  alias BorutaIdentity.ResourceOwners\n\n  def require_authenticated(conn, _opts \\\\ []) do\n    with [authorization_header] <- get_req_header(conn, \"authorization\"),\n         [_authorization_header, access_token] <-\n           Regex.run(~r/Bearer (.+)/, authorization_header),\n         {:ok, token} <- Boruta.Oauth.Authorization.AccessToken.authorize(value: access_token) do\n      userinfo = ResourceOwners.claims(%ResourceOwner{sub: token.sub}, \"profile\")\n\n      case maybe_validate_user(userinfo) do\n        :ok ->\n          conn\n          |> assign(:authorization, %{\n            \"scope\" => token.scope,\n            \"sub\" => token.sub\n          })\n\n        error ->\n          respond_unauthorized(conn, error)\n      end\n    else\n      {:error, _error} ->\n        with [authorization_header] <- get_req_header(conn, \"authorization\"),\n             [_authorization_header, access_token] <-\n               Regex.run(~r/Bearer (.+)/, authorization_header),\n             {:ok, userinfo} <- userinfo(access_token),\n             :ok <- maybe_validate_user(userinfo) do\n          assign(conn, :authorization, userinfo)\n        else\n          e ->\n            respond_unauthorized(conn, e)\n        end\n\n      e ->\n        respond_unauthorized(conn, e)\n    end\n  end\n\n  @impl Boruta.Openid.UserinfoApplication\n  def unauthorized(_conn, error) do\n    {:error, error}\n  end\n\n  @impl Boruta.Openid.UserinfoApplication\n  def userinfo_fetched(_conn, userinfo_response) do\n    userinfo =\n      UserinfoResponse.payload(%{userinfo_response | format: :json})\n      |> Enum.map(fn {k, v} -> {to_string(k), v} end)\n      |> Enum.into(%{})\n\n    {:ok, userinfo}\n  end\n\n  def authorize(conn, [_h | _t] = scopes) do\n    current_scopes = String.split(conn.assigns[:authorization][\"scope\"], \" \")\n\n    case Enum.empty?(scopes -- current_scopes) do\n      true ->\n        conn\n\n      false ->\n        conn\n        |> put_status(:forbidden)\n        |> put_view(ErrorView)\n        |> render(\"403.json\")\n        |> halt()\n    end\n  end\n\n  def authorize(conn, _opts) do\n    conn\n    |> put_status(:forbidden)\n    |> put_view(ErrorView)\n    |> render(\"403.json\")\n    |> halt()\n  end\n\n  # TODO cache token introspection\n  def userinfo(access_token) do\n    site = Application.get_env(:boruta_web, BorutaAdminWeb.Authorization)[:oauth2][:site]\n\n    with {:ok, %Finch.Response{body: body}} <-\n           Finch.build(\n             :get,\n             \"#{site}/oauth/userinfo\",\n             [\n               {\"accept\", \"application/json\"},\n               {\"authorization\", \"Bearer \" <> access_token}\n             ]\n           )\n           |> Finch.request(FinchHttp) do\n      Jason.decode(body)\n    end\n  end\n\n  defp respond_unauthorized(conn, e) do\n    Logger.debug(\"User unauthorized : #{inspect(e)}\")\n\n    conn\n    |> put_status(:unauthorized)\n    |> put_view(ErrorView)\n    |> render(\"401.json\")\n    |> halt()\n  end\n\n  defp maybe_validate_user(userinfo) do\n    case {\n      Application.get_env(:boruta_web, BorutaAdminWeb.Authorization)[:sub_restricted],\n      Application.get_env(:boruta_web, BorutaAdminWeb.Authorization)[:organization_restricted]\n    } do\n      {_, \"\" <> restricted_organization} ->\n        case Map.get(userinfo, \"organizations\", [])\n             |> Enum.map(fn %{\"id\" => id} -> id end)\n             |> Enum.member?(restricted_organization) do\n          true ->\n            :ok\n\n          false ->\n            {:error, \"Instance management is restricted to #{restricted_organization}\"}\n        end\n\n      {\"\" <> restricted_sub, _} ->\n        case userinfo[\"sub\"] do\n          ^restricted_sub ->\n            :ok\n\n          _ ->\n            {:error, \"Instance management is restricted to #{restricted_sub}\"}\n        end\n\n      {_, _} ->\n        :ok\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/plugs/logger.ex",
    "content": "defmodule BorutaAdminWeb.Logger do\n  @moduledoc false\n\n  require Logger\n  alias Plug.Conn\n  @behaviour Plug\n\n  @impl true\n  def init(opts) do\n    Keyword.get(opts, :log, :info)\n  end\n\n  @impl true\n  def call(conn, level) do\n    start = System.monotonic_time()\n\n    Conn.register_before_send(\n      conn,\n      fn conn ->\n        Logger.log(\n          level,\n          fn ->\n            remote_ip = :inet.ntoa(conn.remote_ip)\n            stop = System.monotonic_time()\n            duration = System.convert_time_unit(stop - start, :native, :microsecond)\n            status = Integer.to_string(conn.status)\n\n            [\n              \"boruta_admin\",\n              ?\\s,\n              conn.method,\n              ?\\s,\n              conn.request_path,\n              \" - \",\n              connection_type(conn.state),\n              ?\\s,\n              status,\n              \" from \",\n              remote_ip,\n              \" in \",\n              duration(duration)\n            ]\n          end,\n          type: :request\n        )\n\n        conn\n      end\n    )\n  end\n\n  defp duration(duration) do\n    duration = System.convert_time_unit(duration, :native, :microsecond)\n\n    if duration > 1000 do\n      [duration |> div(1000) |> Integer.to_string(), \"ms\"]\n    else\n      [Integer.to_string(duration), \"µs\"]\n    end\n  end\n\n  defp connection_type(%{state: :set_chunked}), do: \"Chunked\"\n  defp connection_type(_), do: \"Sent\"\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/router.ex",
    "content": "defmodule BorutaAdminWeb.Router do\n  use BorutaAdminWeb, :router\n  use Plug.ErrorHandler\n\n  alias Plug.Conn.Status\n\n  import BorutaAdminWeb.Authorization,\n    only: [\n      require_authenticated: 2\n    ]\n\n  pipeline :authenticated_api do\n    plug(:accepts, [\"json\"])\n    plug(:require_authenticated)\n  end\n\n  pipeline :browser do\n    plug(:accepts, [\"html\"])\n    plug(:fetch_session)\n    plug(:fetch_flash)\n    plug(:protect_from_forgery)\n    plug(:put_secure_browser_headers)\n  end\n\n  pipeline :api do\n    plug(:accepts, [\"json\"])\n  end\n\n  scope \"/\", BorutaAdminWeb do\n    pipe_through(:browser)\n\n    get(\"/\", PageController, :index)\n  end\n\n  scope \"/api\", BorutaAdminWeb, as: :admin do\n    pipe_through(:authenticated_api)\n\n    resources(\"/logs\", LogsController, only: [:index])\n    resources(\"/scopes\", ScopeController, except: [:new, :edit])\n    resources(\"/roles\", RoleController, except: [:new, :edit])\n    resources(\"/key-pairs\", KeyPairController, except: [:new, :edit])\n    post(\"/key-pairs/:id/rotate\", KeyPairController, :rotate)\n    resources(\"/clients\", ClientController, except: [:new, :edit])\n    post(\"/clients/:id/regenerate_did\", ClientController, :regenerate_did)\n    post(\"/clients/:id/regenerate_key_pair\", ClientController, :regenerate_key_pair)\n    resources(\"/users\", UserController, except: [:new, :edit])\n    resources(\"/organizations\", OrganizationController, except: [:new, :edit])\n    get(\"/upstreams/nodes\", UpstreamController, :node_list)\n    resources(\"/upstreams\", UpstreamController, except: [:new, :edit])\n\n    scope \"/configuration\", as: :configuration do\n      get(\"/\", ConfigurationController, :configuration, as: :configuration_list)\n\n      get(\"/example-configuration-file\", ConfigurationController, :example_configuration_file)\n\n      post(\"/upload-configuration-file\", ConfigurationController, :upload_configuration_file,\n        as: :upload_configuration_file\n      )\n\n      get(\"/error-templates/:template_type\", ConfigurationController, :error_template,\n        as: :error_template\n      )\n\n      patch(\"/error-templates/:template_type\", ConfigurationController, :update_error_template,\n        as: :error_template\n      )\n\n      delete(\"/error-templates/:template_type\", ConfigurationController, :delete_error_template,\n        as: :error_template\n      )\n    end\n\n    resources \"/identity-providers\", IdentityProviderController, except: [:new, :edit] do\n      get(\"/templates/:template_type\", IdentityProviderController, :template, as: :template)\n\n      patch(\"/templates/:template_type\", IdentityProviderController, :update_template,\n        as: :template\n      )\n\n      delete(\"/templates/:template_type\", IdentityProviderController, :delete_template,\n        as: :template\n      )\n    end\n    resources \"/backends\", BackendController, except: [:new, :edit] do\n      get(\"/email-templates/:template_type\", BackendController, :email_template, as: :email_template)\n\n      patch(\"/email-templates/:template_type\", BackendController, :update_email_template,\n        as: :email_template\n      )\n\n      delete(\"/email-templates/:template_type\", BackendController, :delete_email_template,\n        as: :email_template\n      )\n    end\n  end\n\n  scope \"/\", BorutaAdminWeb do\n    pipe_through(:browser)\n\n    match(:get, \"/*path\", PageController, :index)\n  end\n\n  @impl Plug.ErrorHandler\n  def handle_errors(conn, %{kind: _kind, reason: reason, stack: _stack}) do\n    message = %{\n      code: Status.reason_atom(conn.status) |> Atom.to_string() |> String.upcase(),\n      message: reason.__struct__.message(reason)\n    }\n\n    conn\n    |> put_resp_content_type(\"application/json\")\n    |> send_resp(conn.status, Jason.encode!(message))\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/telemetry.ex",
    "content": "defmodule BorutaAdminWeb.Telemetry do\n  @moduledoc false\n\n  use Supervisor\n  import Telemetry.Metrics\n\n  def start_link(arg) do\n    Supervisor.start_link(__MODULE__, arg, name: __MODULE__)\n  end\n\n  @impl true\n  def init(_arg) do\n    children = [\n      # Telemetry poller will execute the given period measurements\n      # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics\n      {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}\n      # Add reporters as children of your supervision tree.\n      # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}\n    ]\n\n    Supervisor.init(children, strategy: :one_for_one)\n  end\n\n  def metrics do\n    [\n      # Phoenix Metrics\n      summary(\"phoenix.endpoint.stop.duration\",\n        unit: {:native, :millisecond}\n      ),\n      summary(\"phoenix.router_dispatch.stop.duration\",\n        tags: [:route],\n        unit: {:native, :millisecond}\n      ),\n\n      # Database Metrics\n      summary(\"boruta_admin.repo.query.total_time\", unit: {:native, :millisecond}),\n      summary(\"boruta_admin.repo.query.decode_time\", unit: {:native, :millisecond}),\n      summary(\"boruta_admin.repo.query.query_time\", unit: {:native, :millisecond}),\n      summary(\"boruta_admin.repo.query.queue_time\", unit: {:native, :millisecond}),\n      summary(\"boruta_admin.repo.query.idle_time\", unit: {:native, :millisecond}),\n\n      # VM Metrics\n      summary(\"vm.memory.total\", unit: {:byte, :kilobyte}),\n      summary(\"vm.total_run_queue_lengths.total\"),\n      summary(\"vm.total_run_queue_lengths.cpu\"),\n      summary(\"vm.total_run_queue_lengths.io\")\n    ]\n  end\n\n  defp periodic_measurements do\n    [\n      # A module, function and arguments to be invoked periodically.\n      # This function must call :telemetry.execute/3 and a metric must be added above.\n      # {BorutaAdminWeb, :count_users, []}\n    ]\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/templates/error/404.html.eex",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Phoenix Framework</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <h1 class=\"ui icon header\">\n        <i class=\"question icon\"></i>\n        <div class=\"content\">\n          Page not found\n          <div class=\"sub header\">The page you requested was not found. Please contact your administrator.</div>\n        </div>\n      </h1>\n    </div>\n    </main>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/templates/error/500.html.eex",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Phoenix Framework</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <h1 class=\"ui icon header\">\n        <i class=\"exclamation icon\"></i>\n        <div class=\"content\">\n          Internal server error\n          <div class=\"sub header\">An unexpected error occured. Please contact your administrator.</div>\n        </div>\n      </h1>\n    </div>\n    </main>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/templates/page/admin.html.eex",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Administration panel</title>\n    <link rel=\"stylesheet\" href=\"/semantic-ui.min.css\" />\n    <link rel=\"stylesheet\" href=\"/prism-dark.min.css\" />\n    <script>\n      window.env = {\n        BORUTA_ADMIN_OAUTH_CLIENT_ID: '<%= System.get_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", \"6a2f41a3-c54c-fce8-32d2-0324e1c32e20\") %>',\n        BORUTA_ADMIN_OAUTH_BASE_URL: '<%= System.get_env(\"BORUTA_ADMIN_OAUTH_BASE_URL\", \"http://localhost:4000\") %>',\n        BORUTA_ADMIN_BASE_URL: '<%= System.get_env(\"BORUTA_ADMIN_BASE_URL\", \"http://localhost:4001\") %>',\n        BORUTA_OAUTH_BASE_URL: '<%= System.get_env(\"BORUTA_OAUTH_BASE_URL\", \"http://localhost:4000\") %>',\n      }\n    </script>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"<%= Routes.static_path(@conn, \"/assets/boruta-admin.css\") %>\" media=\"all\"/>\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but boruta-admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n\n    <script type=\"module\" src=\"<%= Routes.static_path(@conn, \"/assets/app.umd.js\") %>\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/backend_view.ex",
    "content": "defmodule BorutaAdminWeb.BackendView do\n  use BorutaAdminWeb, :view\n\n  alias BorutaAdminWeb.BackendView\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  def render(\"index.json\", %{backends: backends}) do\n    %{data: render_many(backends, BackendView, \"backend.json\")}\n  end\n\n  def render(\"show.json\", %{backend: backend}) do\n    %{data: render_one(backend, BackendView, \"backend.json\")}\n  end\n\n  def render(\"show_email_template.json\", %{email_template: template}) do\n    %{data: render_one(template, __MODULE__, \"email_template.json\", template: template)}\n  end\n\n  def render(\"backend.json\", %{backend: backend}) do\n    %{\n      id: backend.id,\n      name: backend.name,\n      type: backend.type,\n      is_default: backend.is_default,\n      create_default_organization: backend.create_default_organization,\n      roles: IdentityProviders.get_backend_roles(backend.id),\n      metadata_fields: backend.metadata_fields,\n      federated_servers: backend.federated_servers,\n      verifiable_credentials: backend.verifiable_credentials,\n      verifiable_presentations: backend.verifiable_presentations,\n      password_hashing_alg: backend.password_hashing_alg,\n      password_hashing_opts: backend.password_hashing_opts,\n      ldap_pool_size: backend.ldap_pool_size,\n      ldap_host: backend.ldap_host,\n      ldap_user_rdn_attribute: backend.ldap_user_rdn_attribute,\n      ldap_base_dn: backend.ldap_base_dn,\n      ldap_ou: backend.ldap_ou,\n      ldap_master_dn: backend.ldap_master_dn,\n      ldap_master_password: backend.ldap_master_password,\n      smtp_from: backend.smtp_from,\n      smtp_relay: backend.smtp_relay,\n      smtp_username: backend.smtp_username,\n      smtp_password: backend.smtp_password,\n      smtp_ssl: backend.smtp_ssl,\n      smtp_tls: backend.smtp_tls,\n      smtp_port: backend.smtp_port,\n      features: Backend.features(backend)\n    }\n  end\n\n  def render(\"email_template.json\", %{template: template}) do\n    %{\n      id: template.id,\n      txt_content: template.txt_content,\n      html_content: template.html_content,\n      type: template.type,\n      backend_id: template.backend_id\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/changeset_view.ex",
    "content": "defmodule BorutaAdminWeb.ChangesetView do\n  use BorutaWeb, :view\n\n  @doc \"\"\"\n  Traverses and translates changeset errors.\n\n  See `Ecto.Changeset.traverse_errors/2` and\n  `BorutaWeb.ErrorHelpers.translate_error/1` for more details.\n  \"\"\"\n  def translate_errors(changeset) do\n    Ecto.Changeset.traverse_errors(changeset, &translate_error/1)\n  end\n\n  def render(\"error.json\", %{changeset: changeset}) do\n    %{\n      code: \"UNPROCESSABLE_ENTITY\",\n      message: \"Your request could not be processed.\",\n      errors: translate_errors(changeset)\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/client_view.ex",
    "content": "defmodule BorutaAdminWeb.ClientView do\n  use BorutaAdminWeb, :view\n\n  alias BorutaAdminWeb.ClientView\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n\n  def render(\"index.json\", %{clients: clients}) do\n    %{data: render_many(clients, ClientView, \"client.json\")}\n  end\n\n  def render(\"show.json\", %{client: client}) do\n    %{data: render_one(client, ClientView, \"client.json\")}\n  end\n\n  def render(\"client.json\", %{client: client}) do\n    identity_provider =\n      IdentityProviders.get_identity_provider_by_client_id(client.id) || %IdentityProvider{}\n\n    %{\n      id: client.id,\n      public_client_id: client.public_client_id,\n      check_public_client_id: client.check_public_client_id,\n      name: client.name,\n      secret: client.secret,\n      confidential: client.confidential,\n      redirect_uris: client.redirect_uris,\n      public_refresh_token: client.public_refresh_token,\n      public_revoke: client.public_revoke,\n      authorize_scope: client.authorize_scope,\n      enforce_dpop: client.enforce_dpop,\n      enforce_tx_code: client.enforce_tx_code,\n      access_token_ttl: client.access_token_ttl,\n      authorization_code_ttl: client.authorization_code_ttl,\n      authorization_request_ttl: client.authorization_request_ttl,\n      refresh_token_ttl: client.refresh_token_ttl,\n      id_token_ttl: client.id_token_ttl,\n      pkce: client.pkce,\n      public_key: client.public_key,\n      key_pair_type: client.key_pair_type,\n      signatures_adapter: client.signatures_adapter,\n      did: client.did,\n      identity_provider: %{\n        id: identity_provider.id,\n        name: identity_provider.name\n      },\n      authorized_scopes:\n        Enum.map(client.authorized_scopes, fn scope ->\n          %{\n            id: scope.id,\n            name: scope.name,\n            public: scope.public\n          }\n        end),\n      supported_grant_types: client.supported_grant_types,\n      id_token_signature_alg: client.id_token_signature_alg,\n      userinfo_signed_response_alg: client.userinfo_signed_response_alg,\n      token_endpoint_jwt_auth_alg: client.token_endpoint_jwt_auth_alg,\n      token_endpoint_auth_methods: client.token_endpoint_auth_methods,\n      jwt_public_key: client.jwt_public_key,\n      response_mode: client.response_mode\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/configuration_view.ex",
    "content": "defmodule BorutaAdminWeb.ConfigurationView do\n  use BorutaAdminWeb, :view\n\n  alias BorutaAdminWeb.ChangesetView\n\n  def render(\"show_error_template.json\", %{template: template}) do\n    %{data: render_one(template, __MODULE__, \"error_template.json\", template: template)}\n  end\n\n  def render(\"error_template.json\", %{template: template}) do\n    %{\n      id: template.id,\n      content: template.content,\n      type: template.type\n    }\n  end\n\n  def render(\"configuration.json\", %{configurations: configurations}) do\n    %{\n      data: Enum.map(configurations, &Map.take(&1, [:name, :value]))\n    }\n  end\n\n  def render(\"file_upload.json\", %{result: result, file_content: file_content}) do\n    errors =\n      Enum.map(result, fn {key, errors} ->\n        errors =\n          Enum.map(errors, fn\n            %Ecto.Changeset{} = changeset ->\n              ChangesetView.translate_errors(changeset)\n\n            \"\" <> error ->\n              %{validation: [error]}\n          end)\n\n        {key, errors}\n      end)\n      |> Enum.into(%{})\n\n    %{\n      errors: errors,\n      file_content: file_content\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/error_helpers.ex",
    "content": "defmodule BorutaAdminWeb.ErrorHelpers do\n  @moduledoc \"\"\"\n  Conveniences for translating and building error messages.\n  \"\"\"\n\n  use Phoenix.HTML\n\n  @doc \"\"\"\n  Generates tag for inlined form input errors.\n  \"\"\"\n  def error_tag(form, field) do\n    Enum.map(Keyword.get_values(form.errors, field), fn error ->\n      content_tag(:span, translate_error(error),\n        class: \"invalid-feedback\",\n        phx_feedback_for: input_name(form, field)\n      )\n    end)\n  end\n\n  @doc \"\"\"\n  Translates an error message using gettext.\n  \"\"\"\n  def translate_error({msg, opts}) do\n    if count = opts[:count] do\n      Gettext.dngettext(BorutaAdminWeb.Gettext, \"errors\", msg, msg, count, opts)\n    else\n      Gettext.dgettext(BorutaAdminWeb.Gettext, \"errors\", msg, opts)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/error_view.ex",
    "content": "defmodule BorutaAdminWeb.ErrorView do\n  use BorutaAdminWeb, :view\n\n  def render(\"400.json\", _assigns) do\n    %{\n      code: \"BAD_REQUEST\",\n      message: \"The requested with given parameters cannot be processed.\",\n      errors: %{\n        resource: [\"the requested with given parameters cannot be processed.\"]\n      }\n    }\n  end\n\n  def render(\"404.json\", _assigns) do\n    %{\n      code: \"NOT_FOUND\",\n      message: \"The requested resource could not be found.\",\n      errors: %{\n        resource: [\"the requested resource could not be found.\"]\n      }\n    }\n  end\n\n  def render(\"401.json\", _assigns) do\n    %{\n      code: \"UNAUTHORIZED\",\n      message: \"You are unauthorized to access this resource.\",\n      errors: %{\n        resource: [\"you are unauthorized to access this resource.\"]\n      }\n    }\n  end\n\n  def render(\"403.json\", _assigns) do\n    %{\n      code: \"FORBIDDEN\",\n      message: \"You are forbidden to access this resource.\",\n      errors: %{\n        resource: [\"you are forbidden to access this resource.\"]\n      }\n    }\n  end\n\n  def render(\"protected_resource.json\", _assigns) do\n    %{\n      code: \"FORBIDDEN\",\n      message: \"The resource is write protected.\",\n      errors: %{\n        resource: [\"is write protected.\"]\n      }\n    }\n  end\n\n  def template_not_found(template, _assigns) do\n    Phoenix.Controller.status_message_from_template(template)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/identity_provider_view.ex",
    "content": "defmodule BorutaAdminWeb.IdentityProviderView do\n  use BorutaAdminWeb, :view\n\n  alias BorutaAdminWeb.BackendView\n\n  def render(\"index.json\", %{identity_providers: identity_providers}) do\n    %{data: render_many(identity_providers, __MODULE__, \"identity_provider.json\")}\n  end\n\n  def render(\"show.json\", %{identity_provider: identity_provider}) do\n    %{data: render_one(identity_provider, __MODULE__, \"identity_provider.json\")}\n  end\n\n  def render(\"show_template.json\", %{template: template}) do\n    %{data: render_one(template, __MODULE__, \"template.json\", template: template)}\n  end\n\n  def render(\"identity_provider.json\", %{identity_provider: identity_provider}) do\n    %{\n      id: identity_provider.id,\n      name: identity_provider.name,\n      backend: render_one(identity_provider.backend, BackendView, \"backend.json\", backend: identity_provider.backend),\n      backend_id: identity_provider.backend_id,\n      check_password: identity_provider.check_password,\n      choose_session: identity_provider.choose_session,\n      totpable: identity_provider.totpable,\n      enforce_totp: identity_provider.enforce_totp,\n      webauthnable: identity_provider.webauthnable,\n      enforce_webauthn: identity_provider.enforce_webauthn,\n      registrable: identity_provider.registrable,\n      user_editable: identity_provider.user_editable,\n      consentable: identity_provider.consentable,\n      confirmable: identity_provider.confirmable\n    }\n  end\n\n  def render(\"template.json\", %{template: template}) do\n    %{\n      id: template.id,\n      content: template.content,\n      type: template.type,\n      identity_provider_id: template.identity_provider_id\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/key_pair_view.ex",
    "content": "defmodule BorutaAdminWeb.KeyPairView do\n  use BorutaAdminWeb, :view\n  alias BorutaAdminWeb.KeyPairView\n\n  def render(\"index.json\", %{key_pairs: key_pairs}) do\n    %{data: render_many(key_pairs, KeyPairView, \"key_pair.json\")}\n  end\n\n  def render(\"show.json\", %{key_pair: key_pair}) do\n    %{data: render_one(key_pair, KeyPairView, \"key_pair.json\")}\n  end\n\n  def render(\"key_pair.json\", %{key_pair: key_pair}) do\n    %{id: key_pair.id,\n      public_key: key_pair.public_key,\n      is_default: key_pair.is_default}\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/logs_view.ex",
    "content": "defmodule BorutaAdminWeb.LogsView do\n  use BorutaAdminWeb, :view\n\n  def render(\"index.json\", %{stats: stats}) do\n    stats\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/organization_view.ex",
    "content": "defmodule BorutaAdminWeb.OrganizationView do\n  use BorutaAdminWeb, :view\n\n  alias BorutaAdminWeb.OrganizationView\n\n  def render(\"index.json\", %{\n        organizations: organizations,\n        page_number: page_number,\n        page_size: page_size,\n        total_pages: total_pages,\n        total_entries: total_entries\n      }) do\n    %{\n      data: render_many(organizations, OrganizationView, \"organization.json\"),\n      page_number: page_number,\n      page_size: page_size,\n      total_pages: total_pages,\n      total_entries: total_entries\n    }\n  end\n\n  def render(\"show.json\", %{organization: organization}) do\n    %{data: render_one(organization, OrganizationView, \"organization.json\")}\n  end\n\n  def render(\"organization.json\", %{organization: organization}) do\n    %{\n      id: organization.id,\n      name: organization.name,\n      label: organization.label\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/page_view.ex",
    "content": "defmodule BorutaAdminWeb.PageView do\n  use BorutaAdminWeb, :view\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/role_view.ex",
    "content": "defmodule BorutaAdminWeb.RoleView do\n  use BorutaAdminWeb, :view\n  alias BorutaAdminWeb.RoleView\n\n  def render(\"index.json\", %{roles: roles}) do\n    %{data: render_many(roles, RoleView, \"role.json\")}\n  end\n\n  def render(\"show.json\", %{role: role}) do\n    %{data: render_one(role, RoleView, \"role.json\")}\n  end\n\n  def render(\"role.json\", %{role: role}) do\n    %{id: role.id,\n      name: role.name,\n      scopes:\n        Enum.map(role.scopes, fn scope ->\n          %{\n            id: scope.id,\n            name: scope.name,\n            public: scope.public\n          }\n        end)\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/scope_view.ex",
    "content": "defmodule BorutaAdminWeb.ScopeView do\n  use BorutaAdminWeb, :view\n  alias BorutaAdminWeb.ScopeView\n\n  def render(\"index.json\", %{scopes: scopes}) do\n    %{data: render_many(scopes, ScopeView, \"scope.json\")}\n  end\n\n  def render(\"show.json\", %{scope: scope}) do\n    %{data: render_one(scope, ScopeView, \"scope.json\")}\n  end\n\n  def render(\"scope.json\", %{scope: scope}) do\n    %{id: scope.id,\n      name: scope.name,\n      label: scope.label,\n      public: scope.public}\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/upstream_view.ex",
    "content": "defmodule BorutaAdminWeb.UpstreamView do\n  use BorutaAdminWeb, :view\n  alias BorutaAdminWeb.UpstreamView\n\n  def render(\"index.json\", %{upstreams: upstreams}) do\n    data =\n      Enum.map(upstreams, fn {node_name, upstreams} ->\n        {node_name, render_many(upstreams, UpstreamView, \"upstream.json\")}\n      end)\n      |> Enum.into(%{})\n\n    %{data: data}\n  end\n\n  def render(\"node_list.json\", %{nodes: nodes}) do\n    %{data: nodes}\n  end\n\n  def render(\"show.json\", %{upstream: upstream}) do\n    %{data: render_one(upstream, UpstreamView, \"upstream.json\")}\n  end\n\n  def render(\"upstream.json\", %{upstream: upstream}) do\n    %{\n      id: upstream.id,\n      node_name: upstream.node_name,\n      scheme: upstream.scheme,\n      host: upstream.host,\n      port: upstream.port,\n      uris: upstream.uris,\n      strip_uri: upstream.strip_uri,\n      authorize: upstream.authorize,\n      required_scopes: upstream.required_scopes,\n      pool_size: upstream.pool_size,\n      pool_count: upstream.pool_count,\n      max_idle_time: upstream.max_idle_time,\n      error_content_type: upstream.error_content_type,\n      forbidden_response: upstream.forbidden_response,\n      unauthorized_response: upstream.unauthorized_response,\n      forwarded_token_signature_alg: upstream.forwarded_token_signature_alg,\n      forwarded_token_secret: upstream.forwarded_token_secret,\n      forwarded_token_private_key: upstream.forwarded_token_private_key,\n      forwarded_token_public_key: upstream.forwarded_token_public_key\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web/views/user_view.ex",
    "content": "defmodule BorutaAdminWeb.UserView do\n  use BorutaAdminWeb, :view\n\n  alias BorutaAdminWeb.BackendView\n  alias BorutaAdminWeb.ChangesetView\n  alias BorutaAdminWeb.UserView\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.Role\n  alias BorutaIdentity.Accounts.UserRole\n\n  def render(\"index.json\", %{\n        users: users,\n        page_number: page_number,\n        page_size: page_size,\n        total_pages: total_pages,\n        total_entries: total_entries\n      }) do\n    %{\n      data: render_many(users, UserView, \"user.json\"),\n      page_number: page_number,\n      page_size: page_size,\n      total_pages: total_pages,\n      total_entries: total_entries\n    }\n  end\n\n  def render(\"show.json\", %{user: user}) do\n    %{data: render_one(user, UserView, \"user.json\")}\n  end\n\n  def render(\"user.json\", %{user: user}) do\n    %{\n      id: user.id,\n      uid: user.uid,\n      email: user.username,\n      totp_registered_at: user.totp_registered_at,\n      metadata: user.metadata,\n      federated_metadata: user.federated_metadata,\n      group: user.group,\n      authorized_scopes: Accounts.get_user_scopes(user.id),\n      organizations: Accounts.get_user_organizations(user.id),\n      roles:\n        Accounts.get_user_roles(user.id)\n        |> Enum.filter(fn\n          %Role{id: id} ->\n            Enum.find(user.roles, fn %UserRole{role_id: role_id} -> role_id == id end)\n\n          _ ->\n            false\n        end),\n      backend: render_one(user.backend, BackendView, \"backend.json\", backend: user.backend)\n    }\n  end\n\n  def render(\"import_result.json\", %{import_result: import_result}) do\n    import_result\n  end\n\n  defimpl Jason.Encoder, for: Boruta.Oauth.Scope do\n    def encode(scope, opts) do\n      Jason.Encode.map(Map.take(scope, [:id, :name, :public]), opts)\n    end\n  end\n\n  defimpl Jason.Encoder, for: BorutaIdentity.Accounts.Role do\n    def encode(role, opts) do\n      Jason.Encode.map(Map.take(role, [:id, :name, :scopes]), opts)\n    end\n  end\n\n  defimpl Jason.Encoder, for: BorutaIdentity.Organizations.Organization do\n    def encode(role, opts) do\n      Jason.Encode.map(Map.take(role, [:id, :name, :label]), opts)\n    end\n  end\n\n  defimpl Jason.Encoder, for: Ecto.Changeset do\n    def encode(changeset, _opts) do\n      changeset\n      |> ChangesetView.translate_errors()\n      |> Jason.encode!()\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/lib/boruta_admin_web.ex",
    "content": "defmodule BorutaAdminWeb do\n  @moduledoc \"\"\"\n  The entrypoint for defining your web interface, such\n  as controllers, views, channels and so on.\n\n  This can be used in your application as:\n\n      use BorutaAdminWeb, :controller\n      use BorutaAdminWeb, :view\n\n  The definitions below will be executed for every view,\n  controller, etc, so keep them short and clean, focused\n  on imports, uses and aliases.\n\n  Do NOT define functions inside the quoted expressions\n  below. Instead, define any helper function in modules\n  and import those modules here.\n  \"\"\"\n\n  def controller do\n    quote do\n      use Phoenix.Controller, namespace: BorutaAdminWeb\n\n      import Plug.Conn\n      import BorutaAdminWeb.Gettext\n      alias BorutaAdminWeb.Router.Helpers, as: Routes\n    end\n  end\n\n  def view do\n    quote do\n      use Phoenix.View,\n        root: \"lib/boruta_admin_web/templates\",\n        namespace: BorutaAdminWeb\n\n      # Import convenience functions from controllers\n      import Phoenix.Controller,\n        only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]\n\n      # Include shared imports and aliases for views\n      unquote(view_helpers())\n    end\n  end\n\n  def router do\n    quote do\n      use Phoenix.Router\n\n      import Plug.Conn\n      import Phoenix.Controller\n    end\n  end\n\n  def channel do\n    quote do\n      use Phoenix.Channel, log_join: false\n      import BorutaAdminWeb.Gettext\n    end\n  end\n\n  defp view_helpers do\n    quote do\n      # Use all HTML functionality (forms, tags, etc)\n      use Phoenix.HTML\n\n      # Import basic rendering functionality (render, render_layout, etc)\n      import Phoenix.View\n\n      import BorutaAdminWeb.ErrorHelpers\n      import BorutaAdminWeb.Gettext\n      alias BorutaAdminWeb.Router.Helpers, as: Routes\n    end\n  end\n\n  @doc \"\"\"\n  When used, dispatch to the appropriate controller/view/etc.\n  \"\"\"\n  defmacro __using__(which) when is_atom(which) do\n    apply(__MODULE__, which, [])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/mix.exs",
    "content": "defmodule BorutaAdmin.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :boruta_admin,\n      version: \"0.1.0\",\n      build_path: \"../../_build\",\n      config_path: \"../../config/config.exs\",\n      deps_path: \"../../deps\",\n      lockfile: \"../../mix.lock\",\n      elixir: \"~> 1.7\",\n      elixirc_paths: elixirc_paths(Mix.env()),\n      compilers: [:phoenix] ++ Mix.compilers(),\n      start_permanent: Mix.env() == :prod,\n      aliases: aliases(),\n      deps: deps()\n    ]\n  end\n\n  # Configuration for the OTP application.\n  #\n  # Type `mix help compile.app` for more information.\n  def application do\n    [\n      mod: {BorutaAdmin.Application, []},\n      extra_applications: [:logger, :runtime_tools]\n    ]\n  end\n\n  # Specifies which paths to compile per environment.\n  defp elixirc_paths(:test), do: [\"lib\", \"test/support\"]\n  defp elixirc_paths(_), do: [\"lib\"]\n\n  # Specifies your project dependencies.\n  #\n  # Type `mix help deps` for examples and options.\n  defp deps do\n    [\n      {:boruta_auth, in_umbrella: true},\n      {:boruta_gateway, in_umbrella: true},\n      {:boruta_identity, in_umbrella: true},\n      {:boruta_web, in_umbrella: true},\n      {:bypass, \"~> 2.1.0\", only: :test},\n      {:decorator, \"~> 1.4\"},\n      {:ecto_sql, \"~> 3.4\"},\n      {:ex_machina, \"~> 2.4\", only: :test},\n      {:finch, \"~> 0.8\"},\n      {:gettext, \"~> 0.11\"},\n      {:jason, \"~> 1.0\"},\n      {:phoenix, \"~> 1.6.0\", override: true},\n      {:phoenix_ecto, \"~> 4.1\"},\n      {:phoenix_html, \"~> 3.0\"},\n      {:phoenix_live_reload, \"~> 1.2\", only: :dev},\n      {:plug_cowboy, \"~> 2.0\"},\n      {:postgrex, \">= 0.0.0\"},\n      {:remote_ip, \"~> 1.1\"},\n      {:telemetry_metrics, \"~> 0.6\"},\n      {:telemetry_poller, \"~> 0.5\"}\n    ]\n  end\n\n  # Aliases are shortcuts or tasks specific to the current project.\n  # For example, to install project dependencies and perform other setup tasks, run:\n  #\n  #     $ mix setup\n  #\n  # See the documentation for `Mix` for more info on aliases.\n  defp aliases do\n    [\n      setup: [\"deps.get\", \"ecto.setup\", \"cmd npm install --prefix assets\"],\n      \"ecto.setup\": [\"ecto.create\", \"ecto.migrate\", \"run priv/repo/seeds.exs\"],\n      \"ecto.reset\": [\"ecto.drop\", \"ecto.setup\"],\n      test: [\"ecto.create --quiet\", \"ecto.migrate --quiet\", \"test\"]\n    ]\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/priv/examples/configuration.yml",
    "content": "# Example configuration file\n---\nversion: \"1.0\"\nconfiguration:\n  backend:\n    - id: 00000000-0000-0000-0000-000000000001\n      name: Example backend\n      verifiable_credentials:\n        - version: \"13\"\n          credential_identifier: BorutaCredential\n          format: jwt_vc\n          types: VerifiableCredential BorutaCredentialJwtVc\n          claims:\n            - type: attribute\n              name: boruta_username\n              label: boruta username\n              pointer: email\n          display:\n            name: Boruta username (JWT VC)\n            background_color: \"#ffd758\"\n            text_color: \"#333333\"\n            logo:\n              url: https://io.malach.it/assets/images/logo.png\n              alt_text: malachit logo\n      verifiable_presentations:\n        - presentation_identifier: BorutaCredentialJwtVc\n          presentation_definition: |\n            {\n              \"id\": \"credential\",\n              \"input_descriptors\": [\n                {\n                  \"id\": \"boruta_username\",\n                  \"format\": {\n                    \"jwt_vc\": {}\n                  },\n                  \"constraints\": {\n                    \"fields\": [\n                      {\n                        \"path\": [ \"$.boruta_username\" ],\n                        \"id\": \"Boruta account information\",\n                        \"purpose\": \"Present account information to obtain access or further credentials\"\n                      }\n                    ]\n                  }\n                }\n              ]\n            }\n  identity_provider:\n    - id: 00000000-0000-0000-0000-000000000001\n      name: Example identity provider\n      backend_id: 00000000-0000-0000-0000-000000000001\n      consentable: true\n      choose_session: true\n      registrable: true\n  client:\n    - id: 00000000-0000-0000-0000-000000000001\n      name: Example client\n      identity_provider:\n        id: 00000000-0000-0000-0000-000000000001\n      redirect_uris:\n        - https://redirect.uri.boruta\n        - \"{{PREAUTHORIZED_CODE_REDIRECT_URI}}\"\n        - \"{{PRESENTATION_REDIRECT_URI}}\"\n        - openid4vp://\n        - openid-credential-offer://\n  scope:\n    - name: BorutaCredentialJwtVc\n      label: boruta username\n      public: true\n"
  },
  {
    "path": "apps/boruta_admin/priv/gettext/en/LC_MESSAGES/errors.po",
    "content": "## `msgid`s in this file come from POT (.pot) files.\n##\n## Do not add, change, or remove `msgid`s manually here as\n## they're tied to the ones in the corresponding POT file\n## (with the same domain).\n##\n## Use `mix gettext.extract --merge` or `mix gettext.merge`\n## to merge POT files into PO files.\nmsgid \"\"\nmsgstr \"\"\n\"Language: en\\n\"\n\n## From Ecto.Changeset.cast/4\nmsgid \"can't be blank\"\nmsgstr \"\"\n\n## From Ecto.Changeset.unique_constraint/3\nmsgid \"has already been taken\"\nmsgstr \"\"\n\n## From Ecto.Changeset.put_change/3\nmsgid \"is invalid\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_acceptance/3\nmsgid \"must be accepted\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_format/3\nmsgid \"has invalid format\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_subset/3\nmsgid \"has an invalid entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_exclusion/3\nmsgid \"is reserved\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_confirmation/3\nmsgid \"does not match confirmation\"\nmsgstr \"\"\n\n## From Ecto.Changeset.no_assoc_constraint/3\nmsgid \"is still associated with this entry\"\nmsgstr \"\"\n\nmsgid \"are still associated with this entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_length/3\nmsgid \"should be %{count} character(s)\"\nmsgid_plural \"should be %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have %{count} item(s)\"\nmsgid_plural \"should have %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at least %{count} character(s)\"\nmsgid_plural \"should be at least %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at least %{count} item(s)\"\nmsgid_plural \"should have at least %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at most %{count} character(s)\"\nmsgid_plural \"should be at most %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at most %{count} item(s)\"\nmsgid_plural \"should have at most %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n## From Ecto.Changeset.validate_number/3\nmsgid \"must be less than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be less than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be equal to %{number}\"\nmsgstr \"\"\n"
  },
  {
    "path": "apps/boruta_admin/priv/gettext/errors.pot",
    "content": "## This is a PO Template file.\n##\n## `msgid`s here are often extracted from source code.\n## Add new translations manually only if they're dynamic\n## translations that can't be statically extracted.\n##\n## Run `mix gettext.extract` to bring this file up to\n## date. Leave `msgstr`s empty as changing them here has no\n## effect: edit them in PO (`.po`) files instead.\n\n## From Ecto.Changeset.cast/4\nmsgid \"can't be blank\"\nmsgstr \"\"\n\n## From Ecto.Changeset.unique_constraint/3\nmsgid \"has already been taken\"\nmsgstr \"\"\n\n## From Ecto.Changeset.put_change/3\nmsgid \"is invalid\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_acceptance/3\nmsgid \"must be accepted\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_format/3\nmsgid \"has invalid format\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_subset/3\nmsgid \"has an invalid entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_exclusion/3\nmsgid \"is reserved\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_confirmation/3\nmsgid \"does not match confirmation\"\nmsgstr \"\"\n\n## From Ecto.Changeset.no_assoc_constraint/3\nmsgid \"is still associated with this entry\"\nmsgstr \"\"\n\nmsgid \"are still associated with this entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_length/3\nmsgid \"should be %{count} character(s)\"\nmsgid_plural \"should be %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have %{count} item(s)\"\nmsgid_plural \"should have %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at least %{count} character(s)\"\nmsgid_plural \"should be at least %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at least %{count} item(s)\"\nmsgid_plural \"should have at least %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at most %{count} character(s)\"\nmsgid_plural \"should be at most %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at most %{count} item(s)\"\nmsgid_plural \"should have at most %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n## From Ecto.Changeset.validate_number/3\nmsgid \"must be less than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be less than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be equal to %{number}\"\nmsgstr \"\"\n"
  },
  {
    "path": "apps/boruta_admin/priv/repo/migrations/.formatter.exs",
    "content": "[\n  import_deps: [:ecto_sql],\n  inputs: [\"*.exs\"]\n]\n"
  },
  {
    "path": "apps/boruta_admin/priv/repo/migrations/20240508054424_create_configurations.exs",
    "content": "defmodule BorutaAdmin.Repo.Migrations.CreateConfigurations do\n  use Ecto.Migration\n\n  def change do\n    create table(:configurations, primary_key: false) do\n      add :id, :uuid, primary_key: true\n      add :name, :string, null: false\n      add :value, :text, null: false\n\n      timestamps()\n    end\n\n    create index(:configurations, [:name], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/priv/repo/seeds.exs",
    "content": "# Script for populating the database. You can run it as:\n#\n#     mix run priv/repo/seeds.exs\n#\n# Inside the script, you can read and write to any of your\n# repositories directly:\n#\n#     BorutaAdmin.Repo.insert!(%BorutaAdmin.SomeSchema{})\n#\n# We recommend using the bang functions (`insert!`, `update!`\n# and so on) as they will fail if something goes wrong.\n"
  },
  {
    "path": "apps/boruta_admin/priv/test/configuration_files/bad_backend_configuration.yml",
    "content": "---\nversion: \"1.0\"\nconfiguration:\n  backend:\n    - name: bad backend\n      additional: error\n"
  },
  {
    "path": "apps/boruta_admin/priv/test/configuration_files/bad_client_configuration.yml",
    "content": "---\nversion: \"1.0\"\nconfiguration:\n  client:\n    - access_token_ttl: 10\n"
  },
  {
    "path": "apps/boruta_admin/priv/test/configuration_files/bad_error_template_configuration.yml",
    "content": "---\nversion: \"1.0\"\nconfiguration:\n  error_template:\n    - type: \"999\"\n      content: test\n"
  },
  {
    "path": "apps/boruta_admin/priv/test/configuration_files/bad_identity_provider_configuration.yml",
    "content": "---\nversion: \"1.0\"\nconfiguration:\n  identity_provider:\n    - name: bad backend\n      additional: error\n"
  },
  {
    "path": "apps/boruta_admin/priv/test/configuration_files/bad_organization_configuration.yml",
    "content": "---\nversion: \"1.0\"\nconfiguration:\n  organization:\n    - name: \"\"\n      label: bad organization\n      additional: true\n"
  },
  {
    "path": "apps/boruta_admin/priv/test/configuration_files/bad_role_configuration.yml",
    "content": "---\nversion: \"1.0\"\nconfiguration:\n  role:\n    - name: \"\"\n      scopes: []\n"
  },
  {
    "path": "apps/boruta_admin/priv/test/configuration_files/bad_scope_configuration.yml",
    "content": "---\nversion: \"1.0\"\nconfiguration:\n  scope:\n    - name: \"\"\n      label: bad scope\n"
  },
  {
    "path": "apps/boruta_admin/priv/test/configuration_files/full_configuration.yml",
    "content": "---\nversion: \"1.0\"\nconfiguration:\n  node_name: \"full-configuration\"\n  gateway:\n    - authorize: true\n      error_content_type: \"test\"\n      forbidden_response: \"test\"\n      unauthorized_response: \"test\"\n      forwarded_token_secret: \"test\"\n      forwarded_token_signature_alg: \"HS384\"\n      host: \"httpbin.patatoid.fr\"\n      port: 80\n      uris: [\"/httpbin\"]\n      max_idle_time: 10\n      pool_count: 1\n      pool_size: 10\n      required_scopes:\n        GET: [\"test\"]\n      scheme: \"http\"\n      strip_uri: true\n  microgateway:\n    - authorize: true\n      error_content_type: \"test\"\n      forbidden_response: \"test\"\n      unauthorized_response: \"test\"\n      forwarded_token_secret: \"test\"\n      forwarded_token_signature_alg: \"HS384\"\n      host: \"httpbin.patatoid.fr\"\n      port: 80\n      uris: [\"/httpbin\"]\n      max_idle_time: 10\n      pool_count: 1\n      pool_size: 10\n      required_scopes:\n        GET: [\"test\"]\n      scheme: \"http\"\n      strip_uri: true\n  backend:\n    - id: 21b90c7e-8658-44c9-94c5-7a1af32045c4\n      name: test\n  identity_provider:\n    - id: dce4eec9-db5c-4f08-abbd-fc57c6a11f99\n      backend_id: 21b90c7e-8658-44c9-94c5-7a1af32045c4\n      name: test\n      templates:\n        - type: layout\n          content: test\n  client:\n    - identity_provider:\n        id: dce4eec9-db5c-4f08-abbd-fc57c6a11f99\n      name: test\n  role:\n    - id: 1582534e-098d-4221-9c53-4a3631da9d78\n      name: test\n      scopes:\n        - id: 9f163ee9-2d1b-4209-9dd2-f319cd063a9b\n  scope:\n    - id: 9f163ee9-2d1b-4209-9dd2-f319cd063a9b\n      name: test\n      name: test\n  error_template:\n    - type: \"500\"\n      content: test\n  organization:\n    - name: test\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin/configuration_loader_test.exs",
    "content": "defmodule BorutaAdmin.ConfigurationLoaderTest do\n  use BorutaAdmin.DataCase\n\n  alias Boruta.Ecto.Client\n  alias Boruta.Ecto.Scope\n  alias BorutaAdmin.ConfigurationLoader\n  alias BorutaGateway.Upstreams.Upstream\n  alias BorutaIdentity.Accounts.Role\n  alias BorutaIdentity.Configuration.ErrorTemplate\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.IdentityProviders.Template\n  alias BorutaIdentity.Organizations.Organization\n\n  test \"returns an error with a bad configuration file\" do\n    assert BorutaGateway.Repo.all(Upstream) |> Enum.empty?()\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_gateway)\n      |> Path.join(\"/test/configuration_files/bad_configuration.yml\")\n\n    assert ConfigurationLoader.from_file!(configuration_file_path) ==\n             {:error, \"Bad configuration file.\"}\n  end\n\n  test \"returns an error with a bad gateway configuration file\" do\n    assert BorutaGateway.Repo.all(Upstream) |> Enum.empty?()\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_gateway)\n      |> Path.join(\"/test/configuration_files/bad_gateway_configuration.yml\")\n\n    assert ConfigurationLoader.from_file!(configuration_file_path) ==\n             {:ok,\n              %{\n                gateway: [\"Required properties scheme, host, port, uris are missing at #.\"]\n              }}\n  end\n\n  test \"returns an error with a bad microgateway configuration file\" do\n    assert BorutaGateway.Repo.all(Upstream) |> Enum.empty?()\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_gateway)\n      |> Path.join(\"/test/configuration_files/bad_microgateway_configuration.yml\")\n\n    assert ConfigurationLoader.from_file!(configuration_file_path) ==\n             {:ok,\n              %{\n                gateway: [],\n                microgateway: [\n                  \"Required properties scheme, host, port, uris are missing at #.\"\n                ]\n              }}\n  end\n\n  test \"returns an error with a bad organization configuration file\" do\n    assert BorutaIdentity.Repo.all(Backend) |> Enum.count() == 1\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_admin)\n      |> Path.join(\"/test/configuration_files/bad_organization_configuration.yml\")\n\n    assert ConfigurationLoader.from_file!(configuration_file_path) ==\n             {:ok,\n              %{\n                organization: [\"Schema does not allow additional properties: #/additional.\"]\n              }}\n  end\n\n  test \"returns an error with a bad identity provider configuration file\" do\n    assert BorutaIdentity.Repo.all(IdentityProvider) |> Enum.empty?()\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_admin)\n      |> Path.join(\"/test/configuration_files/bad_identity_provider_configuration.yml\")\n\n    assert ConfigurationLoader.from_file!(configuration_file_path) ==\n             {:ok,\n              %{\n                identity_provider: [\"Schema does not allow additional properties: #/additional.\"]\n              }}\n  end\n\n  test \"returns an error with a bad backend configuration file\" do\n    assert BorutaIdentity.Repo.all(Backend) |> Enum.count() == 1\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_admin)\n      |> Path.join(\"/test/configuration_files/bad_backend_configuration.yml\")\n\n    assert ConfigurationLoader.from_file!(configuration_file_path) ==\n             {:ok,\n              %{\n                backend: [\"Schema does not allow additional properties: #/additional.\"]\n              }}\n  end\n\n  test \"returns an error with a bad client configuration file\" do\n    assert BorutaAuth.Repo.all(Client) |> Enum.count() == 1\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_admin)\n      |> Path.join(\"/test/configuration_files/bad_client_configuration.yml\")\n\n    assert {:ok,\n            %{\n              client: [\n                %Ecto.Changeset{\n                  errors: [identity_provider_id: {\"can't be blank\", [validation: :required]}]\n                }\n              ]\n            }} = ConfigurationLoader.from_file!(configuration_file_path)\n  end\n\n  test \"returns an error with a bad scope configuration file\" do\n    assert BorutaAuth.Repo.all(Scope) |> Enum.empty?()\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_admin)\n      |> Path.join(\"/test/configuration_files/bad_scope_configuration.yml\")\n\n    assert {:ok,\n            %{\n              scope: [\n                %Ecto.Changeset{errors: [name: {\"can't be blank\", [validation: :required]}]}\n              ]\n            }} = ConfigurationLoader.from_file!(configuration_file_path)\n  end\n\n  test \"returns an error with a bad role configuration file\" do\n    assert BorutaIdentity.Repo.all(Role) |> Enum.empty?()\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_admin)\n      |> Path.join(\"/test/configuration_files/bad_role_configuration.yml\")\n\n    assert {:ok,\n            %{\n              role: [%Ecto.Changeset{errors: [name: {\"can't be blank\", [validation: :required]}]}]\n            }} = ConfigurationLoader.from_file!(configuration_file_path)\n  end\n\n  test \"returns an error with a bad error template configuration file\" do\n    configuration_file_path =\n      :code.priv_dir(:boruta_admin)\n      |> Path.join(\"/test/configuration_files/bad_error_template_configuration.yml\")\n\n    assert {:ok,\n            %{\n              error_template: [\"Error template does not exist.\"]\n            }} = ConfigurationLoader.from_file!(configuration_file_path)\n  end\n\n  test \"loads a file\" do\n    assert BorutaGateway.Repo.all(Upstream) |> Enum.empty?()\n\n    Application.delete_env(ConfigurationLoader, :node_name)\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_admin)\n      |> Path.join(\"/test/configuration_files/full_configuration.yml\")\n\n    ConfigurationLoader.from_file!(configuration_file_path)\n\n    assert [\n             %Upstream{\n               scheme: \"http\",\n               host: \"httpbin.patatoid.fr\",\n               port: 80,\n               uris: [\"/httpbin\"],\n               required_scopes: %{\"GET\" => [\"test\"]},\n               strip_uri: true,\n               authorize: true,\n               pool_size: 10,\n               pool_count: 1,\n               max_idle_time: 10,\n               error_content_type: \"test\",\n               forbidden_response: \"test\",\n               unauthorized_response: \"test\",\n               forwarded_token_signature_alg: \"HS384\",\n               forwarded_token_secret: \"test\",\n               forwarded_token_public_key: nil,\n               forwarded_token_private_key: nil\n             },\n             %Upstream{\n               node_name: \"nonode@nohost\",\n               scheme: \"http\",\n               host: \"httpbin.patatoid.fr\",\n               port: 80,\n               uris: [\"/httpbin\"],\n               required_scopes: %{\"GET\" => [\"test\"]},\n               strip_uri: true,\n               authorize: true,\n               pool_size: 10,\n               pool_count: 1,\n               max_idle_time: 10,\n               error_content_type: \"test\",\n               forbidden_response: \"test\",\n               unauthorized_response: \"test\",\n               forwarded_token_signature_alg: \"HS384\",\n               forwarded_token_secret: \"test\",\n               forwarded_token_public_key: nil,\n               forwarded_token_private_key: nil\n             }\n           ] = BorutaGateway.Repo.all(Upstream)\n\n    # TODO test all possible configurations\n    assert %Backend{name: \"test\"} = BorutaIdentity.Repo.all(Backend) |> List.last()\n\n    assert %IdentityProvider{\n             name: \"test\",\n             templates: [%Template{content: \"test\", type: \"layout\"}]\n           } =\n             BorutaIdentity.Repo.all(IdentityProvider)\n             |> List.last()\n             |> BorutaIdentity.Repo.preload(:templates)\n\n    assert %Client{name: \"test\"} = BorutaAuth.Repo.all(Client) |> List.last()\n\n    assert %Scope{name: \"test\"} = BorutaAuth.Repo.all(Scope) |> List.last()\n\n    assert %Role{name: \"test\"} = BorutaIdentity.Repo.all(Role) |> List.last()\n\n    assert %Organization{name: \"test\"} = BorutaIdentity.Repo.all(Organization) |> List.last()\n\n    assert %ErrorTemplate{type: \"500\", content: \"test\"} =\n             BorutaIdentity.Repo.all(ErrorTemplate) |> List.last()\n  end\n\n  test \"loads example file\" do\n    assert BorutaGateway.Repo.all(Upstream) |> Enum.empty?()\n\n    Application.delete_env(ConfigurationLoader, :node_name)\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_admin)\n      |> Path.join(\"/examples/configuration.yml\")\n\n    assert {:ok,\n            %{\n              client: [\n                %Ecto.Changeset{\n                  errors: [\n                    redirect_uris: {\"`{{PREAUTHORIZED_CODE_REDIRECT_URI}}` is invalid\", []},\n                    redirect_uris: {\"`{{PRESENTATION_REDIRECT_URI}}` is invalid\", []}\n                  ]\n                }\n              ]\n            }} = ConfigurationLoader.from_file!(configuration_file_path)\n\n    assert %Backend{\n             name: \"Example backend\",\n             id: \"00000000-0000-0000-0000-000000000001\",\n             verifiable_credentials: [\n               %{\n                 \"claims\" => [\n                   %{\n                     \"label\" => \"boruta username\",\n                     \"name\" => \"boruta_username\",\n                     \"pointer\" => \"email\"\n                   }\n                 ],\n                 \"credential_identifier\" => \"BorutaCredentialJwtVc\",\n                 \"display\" => %{\n                   \"background_color\" => \"#ffd758\",\n                   \"logo\" => %{\n                     \"alt_text\" => \"malachit logo\",\n                     \"url\" => \"https://io.malach.it/assets/images/logo.png\"\n                   },\n                   \"name\" => \"Boruta username (JWT VC)\",\n                   \"text_color\" => \"#333333\"\n                 },\n                 \"format\" => \"jwt_vc\",\n                 \"types\" => \"VerifiableCredential BorutaCredentialJwtVc\",\n                 \"version\" => \"13\"\n               }\n             ],\n             verifiable_presentations: [\n               %{\n                 \"presentation_definition\" =>\n                   \"{\\n  \\\"id\\\": \\\"credential\\\",\\n  \\\"input_descriptors\\\": [\\n    {\\n      \\\"id\\\": \\\"boruta_username\\\",\\n      \\\"format\\\": {\\n        \\\"jwt_vc\\\": {}\\n      },\\n      \\\"constraints\\\": {\\n        \\\"fields\\\": [\\n          {\\n            \\\"path\\\": [ \\\"$.boruta_username\\\" ]\\n          }\\n        ]\\n      }\\n    }\\n  ]\\n}\\n\",\n                 \"presentation_identifier\" => \"BorutaCredentialJwtVc\"\n               }\n             ]\n           } = BorutaIdentity.Repo.all(Backend) |> List.last()\n\n    assert %IdentityProvider{\n             id: \"00000000-0000-0000-0000-000000000001\",\n             backend_id: \"00000000-0000-0000-0000-000000000001\",\n             name: \"Example identity provider\",\n             consentable: true,\n             choose_session: true,\n             registrable: true\n           } =\n             BorutaIdentity.Repo.all(IdentityProvider)\n             |> List.last()\n             |> BorutaIdentity.Repo.preload(:templates)\n\n    assert %Scope{\n             name: \"BorutaCredentialJwtVc\",\n             label: \"boruta username\",\n             public: true\n           } = BorutaAuth.Repo.all(Scope) |> List.last()\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/backend_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.BackendControllerTest do\n  use BorutaAdminWeb.ConnCase\n\n  import BorutaIdentity.Factory\n\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.Repo\n\n  @create_attrs %{name: \"some name\"}\n  @update_attrs %{name: \"some updated name\"}\n  @invalid_attrs %{name: nil, type: \"other\"}\n  @update_email_template_attrs %{\n    txt_content: \"some updated content\"\n  }\n  @invalid_email_template_attrs %{\n    txt_content: nil\n  }\n\n  setup %{conn: conn} do\n    {:ok, conn: put_req_header(conn, \"accept\", \"application/json\")}\n  end\n\n  # TODO test sub restriction\n  test \"returns a 401\", %{conn: conn} do\n    assert conn\n           |> get(Routes.admin_backend_path(conn, :index))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> get(Routes.admin_backend_path(conn, :show, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> post(Routes.admin_backend_path(conn, :create))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> patch(Routes.admin_backend_path(conn, :update, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> delete(Routes.admin_backend_path(conn, :delete, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n  end\n\n  describe \"with bad scope\" do\n    @tag authorized: [\"bad:scope\"]\n    test \"returns a 403\", %{conn: conn} do\n      assert conn\n             |> get(Routes.admin_backend_path(conn, :index))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> get(Routes.admin_backend_path(conn, :show, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> post(Routes.admin_backend_path(conn, :create))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> patch(Routes.admin_backend_path(conn, :update, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> delete(Routes.admin_backend_path(conn, :delete, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n    end\n  end\n\n  describe \"index\" do\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"lists all backends\", %{conn: conn} do\n      conn = get(conn, Routes.admin_backend_path(conn, :index))\n      assert is_list(json_response(conn, 200)[\"data\"])\n    end\n  end\n\n  describe \"show\" do\n    setup [:create_backend]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders not found\", %{conn: conn} do\n      assert_raise Ecto.NoResultsError, fn ->\n        get(conn, Routes.admin_backend_path(conn, :show, \"unexisting\"))\n      end\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"shows a backend\", %{conn: conn, backend: backend} do\n      conn = get(conn, Routes.admin_backend_path(conn, :show, backend))\n\n      name = backend.name\n\n      assert %{\n               \"id\" => _id,\n               \"name\" => ^name,\n               \"type\" => \"Elixir.BorutaIdentity.Accounts.Internal\"\n             } = json_response(conn, 200)[\"data\"]\n    end\n  end\n\n  describe \"create\" do\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders a bad request\", %{conn: conn} do\n      conn = post(conn, Routes.admin_backend_path(conn, :create), %{})\n\n      assert json_response(conn, 400) == %{\n               \"code\" => \"BAD_REQUEST\",\n               \"errors\" => %{\n                 \"resource\" => [\"the requested with given parameters cannot be processed.\"]\n               },\n               \"message\" => \"The requested with given parameters cannot be processed.\"\n             }\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders an error when params are invalid\", %{conn: conn} do\n      conn =\n        post(conn, Routes.admin_backend_path(conn, :create), %{\n          \"backend\" => @invalid_attrs\n        })\n\n      assert json_response(conn, 422) == %{\n               \"code\" => \"UNPROCESSABLE_ENTITY\",\n               \"errors\" => %{\"name\" => [\"can't be blank\"], \"type\" => [\"is invalid\"]},\n               \"message\" => \"Your request could not be processed.\"\n             }\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"creates a backend\", %{conn: conn} do\n      conn =\n        post(conn, Routes.admin_backend_path(conn, :create), %{\"backend\" => @create_attrs})\n\n      assert %{\n               \"id\" => _id,\n               \"name\" => \"some name\",\n               \"type\" => \"Elixir.BorutaIdentity.Accounts.Internal\"\n             } = json_response(conn, 201)[\"data\"]\n    end\n  end\n\n  describe \"update\" do\n    setup [:create_backend]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders not found\", %{conn: conn} do\n      assert_raise Ecto.NoResultsError, fn ->\n        patch(conn, Routes.admin_backend_path(conn, :update, \"unexisting\"), %{\"backend\" => %{}})\n      end\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders a bad request\", %{conn: conn} do\n      conn = patch(conn, Routes.admin_backend_path(conn, :update, \"id\"), %{})\n\n      assert json_response(conn, 400) == %{\n               \"code\" => \"BAD_REQUEST\",\n               \"errors\" => %{\n                 \"resource\" => [\"the requested with given parameters cannot be processed.\"]\n               },\n               \"message\" => \"The requested with given parameters cannot be processed.\"\n             }\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders an error when params are invalid\", %{conn: conn, backend: backend} do\n      conn =\n        patch(conn, Routes.admin_backend_path(conn, :update, backend), %{\n          \"backend\" => @invalid_attrs\n        })\n\n      assert json_response(conn, 422) == %{\n               \"code\" => \"UNPROCESSABLE_ENTITY\",\n               \"errors\" => %{\"name\" => [\"can't be blank\"], \"type\" => [\"is invalid\"]},\n               \"message\" => \"Your request could not be processed.\"\n             }\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"updates a backend\", %{conn: conn, backend: backend} do\n      conn =\n        patch(conn, Routes.admin_backend_path(conn, :update, backend), %{\"backend\" => @update_attrs})\n\n      assert %{\n               \"id\" => _id,\n               \"name\" => \"some updated name\",\n               \"type\" => \"Elixir.BorutaIdentity.Accounts.Internal\"\n             } = json_response(conn, 200)[\"data\"]\n    end\n  end\n\n  describe \"delete\" do\n    setup [:create_backend]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders not found\", %{conn: conn} do\n      assert_raise Ecto.NoResultsError, fn ->\n        get(conn, Routes.admin_backend_path(conn, :show, \"unexisting\"))\n      end\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"deletes a backend\", %{conn: conn, backend: backend} do\n      conn = delete(conn, Routes.admin_backend_path(conn, :delete, backend))\n\n      assert response(conn, 204)\n\n      refute Repo.get(Backend, backend.id)\n    end\n  end\n\n  describe \"show abckend email template\" do\n    setup [:create_backend]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders not found\", %{conn: conn, backend: %Backend{id: id}} do\n      assert_raise Ecto.NoResultsError, fn ->\n        get(conn, Routes.admin_backend_email_template_path(conn, :email_template, id, \"unexisting\"))\n      end\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders a backend email template\", %{\n      conn: conn,\n      backend: %Backend{id: id}\n    } do\n      conn =\n        get(\n          conn,\n          Routes.admin_backend_email_template_path(conn, :email_template, id, \"reset_password_instructions\")\n        )\n\n      assert %{\"backend_id\" => ^id, \"type\" => \"reset_password_instructions\"} =\n        json_response(conn, 200)[\"data\"]\n    end\n  end\n\n  describe \"update backend email template\" do\n    setup [:create_backend]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders backend template when data is valid\", %{\n      conn: conn,\n      backend: %Backend{id: backend_id}\n    } do\n      conn =\n        patch(\n          conn,\n          Routes.admin_backend_email_template_path(\n            conn,\n            :update_email_template,\n            backend_id,\n            \"reset_password_instructions\"\n          ),\n          template: @update_email_template_attrs\n        )\n\n      assert %{\"id\" => template_id, \"txt_content\" => \"some updated content\"} =\n               json_response(conn, 200)[\"data\"]\n\n      conn =\n        get(\n          conn,\n          Routes.admin_backend_email_template_path(\n            conn,\n            :email_template,\n            backend_id,\n            \"reset_password_instructions\"\n          )\n        )\n\n      assert %{\n               \"id\" => ^template_id,\n               \"txt_content\" => \"some updated content\",\n               \"type\" => \"reset_password_instructions\",\n               \"backend_id\" => ^backend_id\n             } = json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders errors when data is invalid\", %{\n      conn: conn,\n      backend: backend\n    } do\n      conn =\n        patch(\n          conn,\n          Routes.admin_backend_email_template_path(\n            conn,\n            :update_email_template,\n            backend,\n            \"reset_password_instructions\"\n          ),\n          template: @invalid_email_template_attrs\n        )\n\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n  end\n\n  describe \"delete backend email template\" do\n    setup [:create_backend]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"respond a 404 when backend does not exist\", %{\n      conn: conn\n    } do\n      backend_id = SecureRandom.uuid()\n      type = \"reset_password_instructions\"\n\n      assert_error_sent(404, fn ->\n        delete(\n          conn,\n          Routes.admin_backend_email_template_path(\n            conn,\n            :delete_email_template,\n            backend_id,\n            type\n          )\n        )\n      end)\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"respond a 404 when template does not exist\", %{\n      conn: conn,\n      backend: %Backend{id: backend_id}\n    } do\n      type = \"reset_password_instructions\"\n\n      assert_error_sent(404, fn ->\n        delete(\n          conn,\n          Routes.admin_backend_email_template_path(\n            conn,\n            :delete_email_template,\n            backend_id,\n            type\n          )\n        )\n      end)\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"deletes backend template when template exists\", %{\n      conn: conn,\n      backend: %Backend{id: backend_id} = backend\n    } do\n      type = \"reset_password_instructions\"\n      insert(:email_template, type: type, backend: backend)\n\n      conn =\n        delete(\n          conn,\n          Routes.admin_backend_email_template_path(\n            conn,\n            :delete_email_template,\n            backend_id,\n            type\n          )\n        )\n\n      assert %{\"id\" => nil, \"type\" => \"reset_password_instructions\"} = json_response(conn, 200)[\"data\"]\n    end\n  end\n\n  def fixture(:backend) do\n    {:ok, backend} = IdentityProviders.create_backend(@create_attrs)\n    backend\n  end\n\n  defp create_backend(_) do\n    backend = fixture(:backend)\n    %{backend: backend}\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/client_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.ClientControllerTest do\n  import Boruta.Factory\n\n  use BorutaAdminWeb.ConnCase\n\n  alias Boruta.Ecto.Client\n  alias BorutaIdentity.IdentityProviders.ClientIdentityProvider\n\n  @create_attrs %{\n    redirect_uris: [\"http://redirect.uri\"],\n    access_token_ttl: 10,\n    authorization_code_ttl: 10,\n    identity_provider: nil\n  }\n  @update_attrs %{\n    redirect_uris: [\"http://updated.redirect.uri\"]\n  }\n  @invalid_attrs %{\n    redirect_uris: [\"bad_uri\"]\n  }\n\n  setup %{conn: conn} do\n    {:ok, conn: put_req_header(conn, \"accept\", \"application/json\")}\n  end\n\n  # TODO test sub restriction\n  test \"returns a 401\", %{conn: conn} do\n    conn = get(conn, Routes.admin_client_path(conn, :index))\n    assert response(conn, 401)\n  end\n\n  describe \"with bad scope\" do\n    @tag authorized: [\"bad:scope\"]\n    test \"returns a 403\", %{conn: conn} do\n      conn = get(conn, Routes.admin_client_path(conn, :index))\n      assert response(conn, 403)\n    end\n  end\n\n  describe \"index\" do\n    @tag authorized: [\"clients:manage:all\"]\n    test \"lists all clients\", %{conn: conn} do\n      conn = get(conn, Routes.admin_client_path(conn, :index))\n      assert length(json_response(conn, 200)[\"data\"]) == 2\n    end\n  end\n\n  describe \"create client\" do\n    setup %{conn: conn} do\n      identity_provider = BorutaIdentity.Factory.insert(:identity_provider)\n\n      {:ok, conn: conn, identity_provider: identity_provider}\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"renders errors when data is invalid\", %{conn: conn} do\n      conn = post(conn, Routes.admin_client_path(conn, :create), client: @invalid_attrs)\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"renders errors when identity provider is missing\", %{conn: conn} do\n      create_attrs = %{@create_attrs | identity_provider: %{id: SecureRandom.uuid()}}\n\n      create = post(conn, Routes.admin_client_path(conn, :create), client: create_attrs)\n\n      assert %{\"identity_provider_id\" => [\"does not exist\"]} =\n               json_response(create, 422)[\"errors\"]\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"renders errors when identity provider has invalid uuid\", %{conn: conn} do\n      create_attrs = %{@create_attrs | identity_provider: %{id: \"bad_uuid\"}}\n\n      create = post(conn, Routes.admin_client_path(conn, :create), client: create_attrs)\n\n      assert %{\"identity_provider_id\" => [\"has invalid format\"]} =\n               json_response(create, 422)[\"errors\"]\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"renders client when data is valid\", %{conn: conn, identity_provider: identity_provider} do\n      create_attrs = %{@create_attrs | identity_provider: %{id: identity_provider.id}}\n\n      create = post(conn, Routes.admin_client_path(conn, :create), client: create_attrs)\n      assert %{\"id\" => _id} = json_response(create, 201)[\"data\"]\n    end\n  end\n\n  describe \"update client\" do\n    setup %{conn: conn} do\n      client = insert(:client)\n      identity_provider = BorutaIdentity.Factory.insert(:identity_provider)\n\n      BorutaIdentity.Factory.insert(:client_identity_provider,\n        client_id: client.id,\n        identity_provider: identity_provider\n      )\n\n      {:ok, conn: conn, client: client}\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"renders errors when data is invalid\", %{conn: conn, client: client} do\n      conn = put(conn, Routes.admin_client_path(conn, :update, client), client: @invalid_attrs)\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"renders errors when identity provider is invalid\", %{conn: conn, client: client} do\n      update_attrs = Map.put(@update_attrs, \"identity_provider\", %{\"id\" => SecureRandom.uuid()})\n\n      conn = put(conn, Routes.admin_client_path(conn, :update, client), client: update_attrs)\n      assert %{\"identity_provider_id\" => [\"does not exist\"]} = json_response(conn, 422)[\"errors\"]\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"cannot update administration ui client\", %{conn: conn, client: client} do\n      current_admin_ui_client_id = System.get_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", \"\")\n      System.put_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", client.id)\n\n      conn = put(conn, Routes.admin_client_path(conn, :update, client), client: @update_attrs)\n      assert response(conn, 403)\n\n      System.put_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", current_admin_ui_client_id)\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"updates client identity provider when data is valid\", %{\n      conn: conn,\n      client: %Client{id: id} = client\n    } do\n      identity_provider = BorutaIdentity.Factory.insert(:identity_provider)\n      update_attrs = Map.put(@update_attrs, \"identity_provider\", %{\"id\" => identity_provider.id})\n\n      conn = put(conn, Routes.admin_client_path(conn, :update, client), client: update_attrs)\n      assert %{\"id\" => ^id} = json_response(conn, 200)[\"data\"]\n\n      assert BorutaIdentity.Repo.get_by(ClientIdentityProvider,\n               client_id: id,\n               identity_provider_id: identity_provider.id\n             )\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"renders client when data is valid\", %{conn: conn, client: %Client{id: id} = client} do\n      conn = put(conn, Routes.admin_client_path(conn, :update, client), client: @update_attrs)\n\n      assert %{\n               \"id\" => ^id,\n               \"redirect_uris\" => [\"http://updated.redirect.uri\"]\n             } = json_response(conn, 200)[\"data\"]\n    end\n\n    @tag :skip\n    test \"updates a client with a global key pair\"\n  end\n\n  describe \"regenerate client key pair\" do\n    setup %{conn: conn} do\n      client = insert(:client)\n      identity_provider = BorutaIdentity.Factory.insert(:identity_provider)\n\n      BorutaIdentity.Factory.insert(:client_identity_provider,\n        client_id: client.id,\n        identity_provider: identity_provider\n      )\n\n      {:ok, conn: conn, client: client}\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"regenerates client key pair\", %{conn: conn, client: client} do\n      public_key = client.public_key\n\n      conn = post(conn, Routes.admin_client_path(conn, :regenerate_key_pair, client))\n      assert %{\"data\" => %{\n        \"public_key\" => new_public_key\n      }} = json_response(conn, 200)\n\n      assert new_public_key != public_key\n    end\n  end\n\n  describe \"delete client\" do\n    setup %{conn: conn} do\n      client = insert(:client)\n\n      {:ok, conn: conn, client: client}\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"cannot delete administration ui client\", %{conn: conn, client: client} do\n      current_admin_ui_client_id = System.get_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", \"\")\n      System.put_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", client.id)\n\n      conn = delete(conn, Routes.admin_client_path(conn, :delete, client))\n      assert response(conn, 403)\n\n      System.put_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", current_admin_ui_client_id)\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"returns an error when client does not exist\", %{conn: conn} do\n      assert_error_sent(404, fn ->\n        delete(conn, Routes.admin_client_path(conn, :delete, SecureRandom.uuid()))\n      end)\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"deletes chosen client\", %{conn: conn, client: client} do\n      conn = delete(conn, Routes.admin_client_path(conn, :delete, client))\n      assert response(conn, 204)\n\n      assert_error_sent(404, fn ->\n        get(conn, Routes.admin_client_path(conn, :show, client))\n      end)\n    end\n\n    @tag authorized: [\"clients:manage:all\"]\n    test \"deletes client identity provider association\", %{conn: conn, client: client} do\n      BorutaIdentity.Factory.insert(:client_identity_provider, client_id: client.id)\n\n      conn = delete(conn, Routes.admin_client_path(conn, :delete, client))\n      assert response(conn, 204)\n\n      refute BorutaIdentity.Repo.get_by(ClientIdentityProvider, client_id: client.id)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/configuration_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.ConfigurationControllerTest do\n  use BorutaAdminWeb.ConnCase\n\n  import BorutaIdentity.Factory\n\n  @update_error_template_attrs %{\n    content: \"some updated content\"\n  }\n  @invalid_attrs %{content: nil}\n\n  # TODO test sub restriction\n  test \"returns a 401\", %{conn: conn} do\n    assert conn\n           |> get(\n             Routes.admin_configuration_error_template_path(\n               conn,\n               :error_template,\n               \"template_type\"\n             )\n           )\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> patch(\n             Routes.admin_configuration_error_template_path(\n               conn,\n               :update_error_template,\n               \"template_type\"\n             )\n           )\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> delete(\n             Routes.admin_configuration_error_template_path(\n               conn,\n               :delete_error_template,\n               \"template_type\"\n             )\n           )\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n  end\n\n  describe \"with bad scope\" do\n    @tag authorized: [\"bad:scope\"]\n    test \"returns a 403\", %{conn: conn} do\n      assert conn\n             |> get(\n               Routes.admin_configuration_error_template_path(\n                 conn,\n                 :error_template,\n                 \"template_type\"\n               )\n             )\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> patch(\n               Routes.admin_configuration_error_template_path(\n                 conn,\n                 :update_error_template,\n                 \"template_type\"\n               )\n             )\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> delete(\n               Routes.admin_configuration_error_template_path(\n                 conn,\n                 :delete_error_template,\n                 \"template_type\"\n               )\n             )\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n    end\n  end\n\n  @tag :skip\n  test \"get an configuration template\"\n\n  describe \"update configuration template\" do\n    @tag authorized: [\"configuration:manage:all\"]\n    test \"renders configuration when data is valid\", %{\n      conn: conn\n    } do\n      conn =\n        patch(\n          conn,\n          Routes.admin_configuration_error_template_path(\n            conn,\n            :update_error_template,\n            \"400\"\n          ),\n          template: @update_error_template_attrs\n        )\n\n      assert %{\"id\" => template_id, \"content\" => \"some updated content\"} =\n               json_response(conn, 200)[\"data\"]\n\n      conn =\n        get(\n          conn,\n          Routes.admin_configuration_error_template_path(\n            conn,\n            :error_template,\n            \"400\"\n          )\n        )\n\n      assert %{\n               \"id\" => ^template_id,\n               \"content\" => \"some updated content\",\n               \"type\" => \"400\"\n             } = json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"configuration:manage:all\"]\n    test \"renders errors when data is invalid\", %{conn: conn} do\n      conn =\n        patch(\n          conn,\n          Routes.admin_configuration_error_template_path(conn, :update_error_template, \"400\"),\n          template: @invalid_attrs\n        )\n\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n  end\n\n  describe \"delete error template\" do\n    @tag authorized: [\"configuration:manage:all\"]\n    test \"respond a 404 when error template does not exist\", %{\n      conn: conn\n    } do\n      type = \"400\"\n\n      assert_error_sent(404, fn ->\n        delete(\n          conn,\n          Routes.admin_configuration_error_template_path(\n            conn,\n            :delete_error_template,\n            type\n          )\n        )\n      end)\n    end\n\n    @tag authorized: [\"configuration:manage:all\"]\n    test \"deletes configuration template when template exists\", %{\n      conn: conn\n    } do\n      type = \"400\"\n      insert(:error_template, type: type)\n\n      conn =\n        delete(\n          conn,\n          Routes.admin_configuration_error_template_path(\n            conn,\n            :delete_error_template,\n            type\n          )\n        )\n\n      assert %{\"id\" => nil, \"type\" => \"400\"} = json_response(conn, 200)[\"data\"]\n    end\n  end\n\n  describe \"upsert configuration\" do\n    @tag authorized: [\"configuration:manage:all\", \"clients:manage:all\"]\n    test \"apply configuration file\", %{conn: conn} do\n      conn =\n        post(\n          conn,\n          Routes.admin_configuration_upload_configuration_file_path(\n            conn,\n            :upload_configuration_file\n          ),\n          %{\n            \"file\" => %Plug.Upload{\n              path:\n                :code.priv_dir(:boruta_admin)\n                |> Path.join(\"/test/configuration_files/bad_client_configuration.yml\"),\n              filename: \"file.yml\"\n            },\n            \"options\" => %{\n              \"hash_password\" => \"true\"\n            }\n          }\n        )\n\n      assert json_response(conn, 200) == %{\n               \"errors\" => %{\"client\" => [%{\"identity_provider_id\" => [\"can't be blank\"]}]},\n               \"file_content\" =>\n                 \"---\\nversion: \\\"1.0\\\"\\nconfiguration:\\n  client:\\n    - access_token_ttl: 10\\n\"\n             }\n    end\n\n    @tag authorized: [\"configuration:manage:all\"]\n    test \"does apply not authorized resources\", %{conn: conn} do\n      conn =\n        post(\n          conn,\n          Routes.admin_configuration_upload_configuration_file_path(\n            conn,\n            :upload_configuration_file\n          ),\n          %{\n            \"file\" => %Plug.Upload{\n              path:\n                :code.priv_dir(:boruta_admin)\n                |> Path.join(\"/test/configuration_files/bad_client_configuration.yml\"),\n              filename: \"file.yml\"\n            },\n            \"options\" => %{\n              \"hash_password\" => \"true\"\n            }\n          }\n        )\n\n      assert json_response(conn, 200) == %{\n               \"errors\" => %{},\n               \"file_content\" =>\n                 \"---\\nversion: \\\"1.0\\\"\\nconfiguration:\\n  client:\\n    - access_token_ttl: 10\\n\"\n             }\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/identity_provider_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.IdentityProviderControllerTest do\n  use BorutaAdminWeb.ConnCase\n\n  import BorutaIdentity.Factory\n\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n\n  @create_attrs %{\n    name: \"some name\"\n  }\n  @update_attrs %{\n    name: \"some updated name\"\n  }\n  @update_template_attrs %{\n    content: \"some updated content\"\n  }\n  @invalid_attrs %{name: nil}\n  @invalid_template_attrs %{content: nil}\n\n  def fixture(:identity_provider) do\n    insert(:identity_provider, @create_attrs)\n  end\n\n  setup %{conn: conn} do\n    {:ok, conn: put_req_header(conn, \"accept\", \"application/json\")}\n  end\n\n  # TODO test sub restriction\n  test \"returns a 401\", %{conn: conn} do\n    assert conn\n           |> get(Routes.admin_identity_provider_path(conn, :index))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> post(Routes.admin_identity_provider_path(conn, :create))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> patch(Routes.admin_identity_provider_path(conn, :update, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> delete(Routes.admin_identity_provider_path(conn, :delete, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> get(\n             Routes.admin_identity_provider_template_path(\n               conn,\n               :template,\n               \"identity_provider_id\",\n               \"template_type\"\n             )\n           )\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> patch(\n             Routes.admin_identity_provider_template_path(\n               conn,\n               :update_template,\n               \"identity_provider_id\",\n               \"template_type\"\n             )\n           )\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> delete(\n             Routes.admin_identity_provider_template_path(\n               conn,\n               :delete_template,\n               \"identity_provider_id\",\n               \"template_type\"\n             )\n           )\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n  end\n\n  describe \"with bad scope\" do\n    @tag authorized: [\"bad:scope\"]\n    test \"returns a 403\", %{conn: conn} do\n      assert conn\n             |> get(Routes.admin_identity_provider_path(conn, :index))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> post(Routes.admin_identity_provider_path(conn, :create))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> patch(Routes.admin_identity_provider_path(conn, :update, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> delete(Routes.admin_identity_provider_path(conn, :delete, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> get(\n               Routes.admin_identity_provider_template_path(\n                 conn,\n                 :template,\n                 \"identity_provider_id\",\n                 \"template_type\"\n               )\n             )\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> patch(\n               Routes.admin_identity_provider_template_path(\n                 conn,\n                 :update_template,\n                 \"identity_provider_id\",\n                 \"template_type\"\n               )\n             )\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> delete(\n               Routes.admin_identity_provider_template_path(\n                 conn,\n                 :delete_template,\n                 \"identity_provider_id\",\n                 \"template_type\"\n               )\n             )\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n    end\n  end\n\n  describe \"index\" do\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"lists all identity_providers\", %{conn: conn} do\n      conn = get(conn, Routes.admin_identity_provider_path(conn, :index))\n      assert json_response(conn, 200)[\"data\"] == []\n    end\n  end\n\n  describe \"show\" do\n    setup [:create_identity_provider]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders not found\", %{conn: conn} do\n      assert_raise Ecto.NoResultsError, fn ->\n        get(conn, Routes.admin_identity_provider_path(conn, :show, SecureRandom.uuid()))\n      end\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders a identity provider\", %{\n      conn: conn,\n      identity_provider: %IdentityProvider{id: id} = identity_provider\n    } do\n      conn = get(conn, Routes.admin_identity_provider_path(conn, :show, identity_provider))\n      assert %{\"id\" => ^id} = json_response(conn, 200)[\"data\"]\n    end\n  end\n\n  describe \"show template\" do\n    setup [:create_identity_provider]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders not found\", %{conn: conn, identity_provider: %IdentityProvider{id: id}} do\n      assert_raise Ecto.NoResultsError, fn ->\n        get(conn, Routes.admin_identity_provider_template_path(conn, :template, id, \"unexisting\"))\n      end\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders a identity provider template\", %{\n      conn: conn,\n      identity_provider: %IdentityProvider{id: id}\n    } do\n      conn =\n        get(\n          conn,\n          Routes.admin_identity_provider_template_path(conn, :template, id, \"new_registration\")\n        )\n\n      assert %{\"identity_provider_id\" => ^id, \"type\" => \"new_registration\"} =\n               json_response(conn, 200)[\"data\"]\n    end\n  end\n\n  describe \"create identity_provider\" do\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders identity_provider when data is valid\", %{conn: conn} do\n      backend_id = insert(:backend).id\n\n      conn =\n        post(conn, Routes.admin_identity_provider_path(conn, :create),\n          identity_provider: Map.put(@create_attrs, :backend_id, backend_id)\n        )\n\n      assert %{\"id\" => id} = json_response(conn, 201)[\"data\"]\n\n      conn = get(conn, Routes.admin_identity_provider_path(conn, :show, id))\n\n      assert %{\n               \"id\" => ^id,\n               \"name\" => \"some name\",\n               \"backend\" => %{\"id\" => ^backend_id}\n             } = json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders errors when data is invalid\", %{conn: conn} do\n      conn =\n        post(conn, Routes.admin_identity_provider_path(conn, :create),\n          identity_provider: @invalid_attrs\n        )\n\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n  end\n\n  describe \"update identity_provider template\" do\n    setup [:create_identity_provider]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders identity_provider template when data is valid\", %{\n      conn: conn,\n      identity_provider: %IdentityProvider{id: identity_provider_id}\n    } do\n      conn =\n        patch(\n          conn,\n          Routes.admin_identity_provider_template_path(\n            conn,\n            :update_template,\n            identity_provider_id,\n            \"new_registration\"\n          ),\n          template: @update_template_attrs\n        )\n\n      assert %{\"id\" => template_id, \"content\" => \"some updated content\"} =\n               json_response(conn, 200)[\"data\"]\n\n      conn =\n        get(\n          conn,\n          Routes.admin_identity_provider_template_path(\n            conn,\n            :template,\n            identity_provider_id,\n            \"new_registration\"\n          )\n        )\n\n      assert %{\n               \"id\" => ^template_id,\n               \"content\" => \"some updated content\",\n               \"type\" => \"new_registration\",\n               \"identity_provider_id\" => ^identity_provider_id\n             } = json_response(conn, 200)[\"data\"]\n    end\n\n    # NOTE the transaction sandbox avoids the test\n    @tag :skip\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"do not reset when updated twice with same content\", %{\n      conn: conn,\n      identity_provider: %IdentityProvider{id: identity_provider_id}\n    } do\n      conn =\n        patch(\n          conn,\n          Routes.admin_identity_provider_template_path(\n            conn,\n            :update_template,\n            identity_provider_id,\n            \"new_registration\"\n          ),\n          template: @update_template_attrs\n        )\n\n      assert %{\"id\" => template_id, \"content\" => \"some updated content\"} =\n               json_response(conn, 200)[\"data\"]\n\n      conn =\n        patch(\n          conn,\n          Routes.admin_identity_provider_template_path(\n            conn,\n            :update_template,\n            identity_provider_id,\n            \"new_registration\"\n          ),\n          template: Map.put(@update_template_attrs, :id, template_id)\n        )\n\n      assert %{\"id\" => template_id, \"content\" => \"some updated content\"} =\n               json_response(conn, 200)[\"data\"]\n\n      conn =\n        get(\n          conn,\n          Routes.admin_identity_provider_template_path(\n            conn,\n            :template,\n            identity_provider_id,\n            \"new_registration\"\n          )\n        )\n\n      assert %{\n               \"id\" => ^template_id,\n               \"content\" => \"some updated content\",\n               \"type\" => \"new_registration\",\n               \"identity_provider_id\" => ^identity_provider_id\n             } = json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders errors when data is invalid\", %{\n      conn: conn,\n      identity_provider: identity_provider\n    } do\n      conn =\n        patch(\n          conn,\n          Routes.admin_identity_provider_template_path(\n            conn,\n            :update_template,\n            identity_provider,\n            \"new_registration\"\n          ),\n          template: @invalid_template_attrs\n        )\n\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n  end\n\n  describe \"delete identity_provider template\" do\n    setup [:create_identity_provider]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"respond a 404 when identity provider does not exist\", %{\n      conn: conn\n    } do\n      identity_provider_id = SecureRandom.uuid()\n      type = \"new_registration\"\n\n      assert_error_sent(404, fn ->\n        delete(\n          conn,\n          Routes.admin_identity_provider_template_path(\n            conn,\n            :delete_template,\n            identity_provider_id,\n            type\n          )\n        )\n      end)\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"respond a 404 when template does not exist\", %{\n      conn: conn,\n      identity_provider: %IdentityProvider{id: identity_provider_id}\n    } do\n      type = \"new_registration\"\n\n      assert_error_sent(404, fn ->\n        delete(\n          conn,\n          Routes.admin_identity_provider_template_path(\n            conn,\n            :delete_template,\n            identity_provider_id,\n            type\n          )\n        )\n      end)\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"deletes identity_provider template when template exists\", %{\n      conn: conn,\n      identity_provider: %IdentityProvider{id: identity_provider_id} = identity_provider\n    } do\n      type = \"new_registration\"\n      insert(:template, type: type, identity_provider: identity_provider)\n\n      conn =\n        delete(\n          conn,\n          Routes.admin_identity_provider_template_path(\n            conn,\n            :delete_template,\n            identity_provider_id,\n            type\n          )\n        )\n\n      assert %{\"id\" => nil, \"type\" => \"new_registration\"} = json_response(conn, 200)[\"data\"]\n    end\n  end\n\n  describe \"update identity_provider\" do\n    setup [:create_identity_provider]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders identity_provider when data is valid\", %{\n      conn: conn,\n      identity_provider: %IdentityProvider{id: id} = identity_provider\n    } do\n      conn =\n        put(conn, Routes.admin_identity_provider_path(conn, :update, identity_provider),\n          identity_provider: @update_attrs\n        )\n\n      assert %{\"id\" => ^id} = json_response(conn, 200)[\"data\"]\n\n      conn = get(conn, Routes.admin_identity_provider_path(conn, :show, id))\n\n      assert %{\n               \"id\" => ^id,\n               \"name\" => \"some updated name\"\n             } = json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"renders errors when data is invalid\", %{\n      conn: conn,\n      identity_provider: identity_provider\n    } do\n      conn =\n        put(conn, Routes.admin_identity_provider_path(conn, :update, identity_provider),\n          identity_provider: @invalid_attrs\n        )\n\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n  end\n\n  describe \"delete identity_provider\" do\n    setup [:create_identity_provider]\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"cannot delete admin ui identity_provider\", %{conn: conn} do\n      client_identity_provider = insert(:client_identity_provider)\n      current_admin_ui_client_id = System.get_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", \"\")\n      System.put_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", client_identity_provider.client_id)\n\n      conn =\n        delete(\n          conn,\n          Routes.admin_identity_provider_path(\n            conn,\n            :delete,\n            client_identity_provider.identity_provider\n          )\n        )\n\n      assert response(conn, 403)\n\n      System.put_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", current_admin_ui_client_id)\n    end\n\n    @tag authorized: [\"identity-providers:manage:all\"]\n    test \"deletes chosen identity_provider\", %{conn: conn, identity_provider: identity_provider} do\n      conn = delete(conn, Routes.admin_identity_provider_path(conn, :delete, identity_provider))\n      assert response(conn, 204)\n\n      assert_error_sent(404, fn ->\n        get(conn, Routes.admin_identity_provider_path(conn, :show, identity_provider))\n      end)\n    end\n  end\n\n  defp create_identity_provider(_) do\n    identity_provider = fixture(:identity_provider)\n    %{identity_provider: identity_provider}\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/key_pair_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.KeyPairControllerTest do\n  use BorutaAdminWeb.ConnCase\n\n  # TODO test sub restriction\n  test \"returns a 401\", %{conn: conn} do\n    assert conn\n           |> get(Routes.admin_key_pair_path(conn, :index))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> post(Routes.admin_key_pair_path(conn, :create))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> post(Routes.admin_key_pair_path(conn, :rotate, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> delete(Routes.admin_key_pair_path(conn, :delete, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n  end\n\n  describe \"with bad scope\" do\n    @tag authorized: [\"bad:scope\"]\n    test \"returns a 403\", %{conn: conn} do\n      assert conn\n             |> get(Routes.admin_key_pair_path(conn, :index))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> post(Routes.admin_key_pair_path(conn, :create))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> post(Routes.admin_key_pair_path(conn, :rotate, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> delete(Routes.admin_key_pair_path(conn, :delete, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n    end\n  end\n\n  describe \"index\" do\n    @tag authorized: [\"clients:manage:all\"]\n    test \"lists all key pairs\", %{conn: conn} do\n      conn = get(conn, Routes.admin_key_pair_path(conn, :index))\n      assert json_response(conn, 200)[\"data\"] == []\n    end\n  end\n\n  @tag :skip\n  test \"create a key pair\"\n\n  @tag :skip\n  test \"rotate a key pair\"\n\n  @tag :skip\n  test \"delete\"\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/logs_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.LogsControllerTest do\n  use BorutaAdminWeb.ConnCase\n\n  alias BorutaAuth.LogRotate\n\n  @request_log_lines [\n    \"request_id=Fwd0KILP8T4HsB4AAA3h [info] boruta_web POST /oauth/introspect - sent 200 from 0.0.0.0 in 2ms\",\n    \"request_id=FweNn-2vW71XZiUAAljD [info] boruta_web GET /oauth/authorize - sent 200 from 0.0.0.0 in 16ms\",\n    \"request_id=FweINeYU7G053agAAApG [info] boruta_web POST /oauth/token - sent 401 from 0.0.0.0 in 952µs\"\n  ]\n\n  @business_log_lines [\n    \"request_id=Fwh6kT_QfosEujUAAADC [info] boruta_web authorization authorize - success client_id=6a2f41a3-c54c-fce8-32d2-0324e1c32e20 sub=9b15219f-30a9-4a98-8c2e-296d0a53c638 type=token access_token=QjkypPrdh6iFsgYmp40wzqxTPs6JOFDRrRJXxKPNK0Kjp6LAF83tpHtqtCKlkzYByu3YvhwC1JJZbXBia0cwUF expires_in=3600\",\n    \"request_id=Fwh6kXSdqY_TBZEAAA3B [info] boruta_web authorization introspect - success client_id=6a2f41a3-c54c-fce8-32d2-0324e1c32e20 sub=7133cbcc-3f1f-448b-bc5a-8f551a3d3883 access_token=QjkypPrdh6iFsgYmp40wzqxTPs6JOFDRrRJXxKPNK0Kjp6LAF83tpHtqtCKlkzYByu3YvhwC1JJZbXBia0cwUF active=true\",\n    \"request_id=Fwh6liuATTbaqC4AAAJm [info] boruta_web authorization introspect - failure client_id=6a2f41a3-c54c-fce8-32d2-0324e1c32e20 sub=7133cbcc-3f1f-448b-bc5a-8f551a3d3883 access_token=QjkypPrdh6iFsgYmp40wzqxTPs6JOFDRrRJXxKPNK0Kjp6LAF83tpHtqtCKlkzYByu3YvhwC1JJZbXBia0cwUF active=true\"\n  ]\n\n  setup %{conn: conn} do\n    {:ok, conn: put_req_header(conn, \"accept\", \"application/json\")}\n  end\n\n  # TODO test sub restriction\n  test \"returns a 401\", %{conn: conn} do\n    assert conn\n           |> get(Routes.admin_logs_path(conn, :index))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n  end\n\n  describe \"with bad scope\" do\n    @tag authorized: [\"bad:scope\"]\n    test \"returns a 403\", %{conn: conn} do\n      assert conn\n             |> get(Routes.admin_logs_path(conn, :index))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n    end\n  end\n\n  describe \"index requesting requests logs\" do\n    @tag authorized: [\"logs:read:all\"]\n    test \"return today's logs\", %{conn: conn} do\n      File.mkdir(\"./log\")\n      File.rm(LogRotate.path(:boruta_web, :request, Date.utc_today()))\n\n      before_lines =\n        request_log_line_serie(fn i ->\n          DateTime.utc_now()\n          |> DateTime.add(-1 * 20 * 60, :second)\n          |> DateTime.add(i * 60, :second)\n        end)\n\n      log_lines =\n        request_log_line_serie(fn i ->\n          DateTime.utc_now()\n          |> DateTime.add(-1 * 10 * 60, :second)\n          |> DateTime.add(i * 60, :second)\n        end)\n\n      after_lines =\n        request_log_line_serie(fn i -> DateTime.utc_now() |> DateTime.add(i * 60, :second) end)\n\n      File.write!(\n        LogRotate.path(:boruta_web, :request, Date.utc_today()),\n        Enum.map_join([before_lines, log_lines, after_lines], fn serie ->\n          Enum.join(serie, \"\\n\") <> \"\\n\"\n        end) <> \"\\n\"\n      )\n\n      start_at =\n        DateTime.utc_now() |> DateTime.add(-1 * 10 * 60, :second) |> DateTime.to_iso8601()\n\n      end_at = DateTime.utc_now() |> DateTime.to_iso8601()\n\n      conn =\n        get(conn, Routes.admin_logs_path(conn, :index), %{\n          start_at: start_at,\n          end_at: end_at,\n          application: \"boruta_web\",\n          type: \"request\"\n        })\n\n      assert %{\n               \"time_scale_unit\" => \"second\",\n               \"overflow\" => false,\n               \"log_lines\" => ^log_lines,\n               \"log_count\" => 30\n             } = json_response(conn, 200)\n\n      File.rm!(LogRotate.path(:boruta_web, :request, Date.utc_today()))\n    end\n\n    @tag :skip\n    test \"compute request times\"\n\n    @tag :skip\n    test \"compute request counts\"\n\n    @tag :skip\n    test \"compute status codes\"\n\n    @tag :skip\n    test \"filter logs\"\n\n    @tag authorized: [\"logs:read:all\"]\n    test \"groups direct post requests by route in dashboard stats\", %{conn: conn} do\n      File.mkdir(\"./log\")\n      File.rm(LogRotate.path(:boruta_web, :request, Date.utc_today()))\n\n      log_time = DateTime.utc_now() |> DateTime.add(-60, :second) |> DateTime.truncate(:second)\n\n      direct_post_lines = [\n        \"#{DateTime.to_iso8601(log_time)} request_id=direct-post-1 [info] boruta_web POST /openid/direct_post/code-1 - sent 200 from 0.0.0.0 in 2ms\",\n        \"#{DateTime.to_iso8601(log_time)} request_id=direct-post-2 [info] boruta_web POST /openid/direct_post/code-2 - sent 302 from 0.0.0.0 in 4ms\"\n      ]\n\n      File.write!(\n        LogRotate.path(:boruta_web, :request, Date.utc_today()),\n        Enum.join(direct_post_lines, \"\\n\") <> \"\\n\"\n      )\n\n      start_at = log_time |> DateTime.add(-1, :second) |> DateTime.to_iso8601()\n      end_at = log_time |> DateTime.add(1, :second) |> DateTime.to_iso8601()\n\n      conn =\n        get(conn, Routes.admin_logs_path(conn, :index), %{\n          start_at: start_at,\n          end_at: end_at,\n          application: \"boruta_web\",\n          type: \"request\"\n        })\n\n      direct_post_label = \"boruta_web - POST /openid/direct_post/:code_id\"\n\n      assert %{\n               \"labels\" => [^direct_post_label],\n               \"log_lines\" => ^direct_post_lines,\n               \"log_count\" => 2,\n               \"status_codes\" => %{\n                 ^direct_post_label => %{\n                   \"200\" => 1,\n                   \"302\" => 1\n                 }\n               },\n               \"request_counts\" => %{\n                 ^direct_post_label => request_counts\n               }\n             } = json_response(conn, 200)\n\n      assert Enum.sum(Map.values(request_counts)) == 2\n\n      File.rm!(LogRotate.path(:boruta_web, :request, Date.utc_today()))\n    end\n\n    @tag authorized: [\"logs:read:all\"]\n    test \"skips lines before start_at\", %{conn: conn} do\n      File.mkdir(\"./log\")\n      File.rm(LogRotate.path(:boruta_web, :request, Date.utc_today()))\n\n      before_lines =\n        request_log_line_serie(fn i ->\n          DateTime.utc_now()\n          |> DateTime.add(-1 * 20 * 60, :second)\n          |> DateTime.add(i * 60, :second)\n        end)\n\n      log_lines =\n        request_log_line_serie(fn i ->\n          DateTime.utc_now()\n          |> DateTime.add(-1 * 10 * 60, :second)\n          |> DateTime.add(i * 60, :second)\n        end)\n\n      File.write!(\n        LogRotate.path(:boruta_web, :request, Date.utc_today()),\n        Enum.map_join([before_lines, log_lines], fn serie ->\n          Enum.join(serie, \"\\n\") <> \"\\n\"\n        end) <> \"\\n\"\n      )\n\n      start_at =\n        DateTime.utc_now() |> DateTime.add(-1 * 10 * 60, :second) |> DateTime.to_iso8601()\n\n      end_at = DateTime.utc_now() |> DateTime.to_iso8601()\n\n      conn =\n        get(conn, Routes.admin_logs_path(conn, :index), %{\n          start_at: start_at,\n          end_at: end_at,\n          application: \"boruta_web\",\n          type: \"request\"\n        })\n\n      assert %{\n               \"time_scale_unit\" => \"second\",\n               \"overflow\" => false,\n               \"log_lines\" => ^log_lines,\n               \"log_count\" => 30\n             } = json_response(conn, 200)\n\n      File.rm!(LogRotate.path(:boruta_web, :request, Date.utc_today()))\n    end\n\n    @tag authorized: [\"logs:read:all\"]\n    test \"skips lines after end_at\", %{conn: conn} do\n      File.mkdir(\"./log\")\n      File.rm(LogRotate.path(:boruta_web, :request, Date.utc_today()))\n\n      log_lines =\n        request_log_line_serie(fn i ->\n          DateTime.utc_now()\n          |> DateTime.add(-1 * 10 * 60, :second)\n          |> DateTime.add(i * 60, :second)\n        end)\n\n      after_lines =\n        request_log_line_serie(fn i ->\n          DateTime.utc_now() |> DateTime.add(i * 60, :second)\n        end)\n\n      File.write!(\n        LogRotate.path(:boruta_web, :request, Date.utc_today()),\n        Enum.map_join([log_lines, after_lines], fn serie ->\n          Enum.join(serie, \"\\n\") <> \"\\n\"\n        end) <> \"\\n\"\n      )\n\n      start_at =\n        DateTime.utc_now() |> DateTime.add(-1 * 10 * 60, :second) |> DateTime.to_iso8601()\n\n      end_at = DateTime.utc_now() |> DateTime.to_iso8601()\n\n      conn =\n        get(conn, Routes.admin_logs_path(conn, :index), %{\n          start_at: start_at,\n          end_at: end_at,\n          application: \"boruta_web\",\n          type: \"request\"\n        })\n\n      assert %{\n               \"time_scale_unit\" => \"second\",\n               \"overflow\" => false,\n               \"log_lines\" => ^log_lines,\n               \"log_count\" => 30\n             } = json_response(conn, 200)\n\n      File.rm!(LogRotate.path(:boruta_web, :request, Date.utc_today()))\n    end\n\n    @tag authorized: [\"logs:read:all\"]\n    test \"return multiple day logs\", %{conn: conn} do\n      first_day = Date.utc_today() |> Date.add(-10)\n      second_day = Date.utc_today() |> Date.add(-8)\n\n      File.mkdir(\"./log\")\n      File.rm(LogRotate.path(:boruta_web, :request, first_day))\n      File.rm(LogRotate.path(:boruta_web, :request, second_day))\n\n      [first_day_log_lines, second_day_log_lines] =\n        Enum.map([10, 8], fn day_shift ->\n          request_log_line_serie(fn i ->\n            DateTime.utc_now()\n            |> DateTime.add(-1 * 24 * 3600 * day_shift, :second)\n            |> DateTime.add(i * 60, :second)\n          end)\n        end)\n\n      File.write!(\n        LogRotate.path(:boruta_web, :request, first_day),\n        Enum.join(first_day_log_lines, \"\\n\")\n      )\n\n      File.write!(\n        LogRotate.path(:boruta_web, :request, second_day),\n        Enum.join(second_day_log_lines, \"\\n\")\n      )\n\n      start_at =\n        DateTime.utc_now()\n        |> DateTime.add(-1 * 10 * 24 * 3600 - 1, :second)\n        |> DateTime.to_iso8601()\n\n      end_at = DateTime.utc_now() |> DateTime.to_iso8601()\n\n      conn =\n        get(conn, Routes.admin_logs_path(conn, :index), %{\n          start_at: start_at,\n          end_at: end_at,\n          application: \"boruta_web\",\n          type: \"request\"\n        })\n\n      log_lines = first_day_log_lines ++ second_day_log_lines\n\n      assert %{\n               \"time_scale_unit\" => \"hour\",\n               \"overflow\" => false,\n               \"log_lines\" => ^log_lines,\n               \"log_count\" => 60\n             } = json_response(conn, 200)\n\n      File.rm!(LogRotate.path(:boruta_web, :request, first_day))\n      File.rm!(LogRotate.path(:boruta_web, :request, second_day))\n    end\n  end\n\n  describe \"index requesting business events logs\" do\n    @tag authorized: [\"logs:read:all\"]\n    test \"return today's logs\", %{conn: conn} do\n      File.mkdir(\"./log\")\n      File.rm(LogRotate.path(:boruta_web, :business, Date.utc_today()))\n\n      before_lines =\n        business_log_line_serie(fn i ->\n          DateTime.utc_now()\n          |> DateTime.add(-1 * 20 * 60, :second)\n          |> DateTime.add(i * 60, :second)\n        end)\n\n      log_lines =\n        business_log_line_serie(fn i ->\n          DateTime.utc_now()\n          |> DateTime.add(-1 * 10 * 60, :second)\n          |> DateTime.add(i * 60, :second)\n        end)\n\n      after_lines =\n        business_log_line_serie(fn i -> DateTime.utc_now() |> DateTime.add(i * 60, :second) end)\n\n      File.write!(\n        LogRotate.path(:boruta_web, :business, Date.utc_today()),\n        Enum.map_join([before_lines, log_lines, after_lines], fn serie ->\n          Enum.join(serie, \"\\n\") <> \"\\n\"\n        end) <> \"\\n\"\n      )\n\n      start_at =\n        DateTime.utc_now() |> DateTime.add(-1 * 10 * 60, :second) |> DateTime.to_iso8601()\n\n      end_at = DateTime.utc_now() |> DateTime.to_iso8601()\n\n      conn =\n        get(conn, Routes.admin_logs_path(conn, :index), %{\n          start_at: start_at,\n          end_at: end_at,\n          application: \"boruta_web\",\n          type: \"business\"\n        })\n\n      assert %{\n               \"time_scale_unit\" => \"second\",\n               \"overflow\" => false,\n               \"log_lines\" => ^log_lines,\n               \"log_count\" => 30\n             } = json_response(conn, 200)\n\n      File.rm!(LogRotate.path(:boruta_web, :business, Date.utc_today()))\n    end\n\n    @tag :skip\n    test \"compute business event counts\"\n\n    @tag :skip\n    test \"compute counts\"\n\n    @tag :skip\n    test \"filter logs\"\n\n    @tag authorized: [\"logs:read:all\"]\n    test \"skips lines before start_at\", %{conn: conn} do\n      File.mkdir(\"./log\")\n      File.rm(LogRotate.path(:boruta_web, :business, Date.utc_today()))\n\n      before_lines =\n        business_log_line_serie(fn i ->\n          DateTime.utc_now()\n          |> DateTime.add(-1 * 20 * 60, :second)\n          |> DateTime.add(i * 60, :second)\n        end)\n\n      log_lines =\n        business_log_line_serie(fn i ->\n          DateTime.utc_now()\n          |> DateTime.add(-1 * 10 * 60, :second)\n          |> DateTime.add(i * 60, :second)\n        end)\n\n      File.write!(\n        LogRotate.path(:boruta_web, :business, Date.utc_today()),\n        Enum.map_join([before_lines, log_lines], fn serie ->\n          Enum.join(serie, \"\\n\") <> \"\\n\"\n        end) <> \"\\n\"\n      )\n\n      start_at =\n        DateTime.utc_now() |> DateTime.add(-1 * 10 * 60, :second) |> DateTime.to_iso8601()\n\n      end_at = DateTime.utc_now() |> DateTime.to_iso8601()\n\n      conn =\n        get(conn, Routes.admin_logs_path(conn, :index), %{\n          start_at: start_at,\n          end_at: end_at,\n          application: \"boruta_web\",\n          type: \"business\"\n        })\n\n      assert %{\n               \"time_scale_unit\" => \"second\",\n               \"overflow\" => false,\n               \"log_lines\" => ^log_lines,\n               \"log_count\" => 30\n             } = json_response(conn, 200)\n\n      File.rm!(LogRotate.path(:boruta_web, :business, Date.utc_today()))\n    end\n\n    @tag authorized: [\"logs:read:all\"]\n    test \"skips lines after end_at\", %{conn: conn} do\n      File.mkdir(\"./log\")\n      File.rm(LogRotate.path(:boruta_web, :business, Date.utc_today()))\n\n      log_lines =\n        business_log_line_serie(fn i ->\n          DateTime.utc_now()\n          |> DateTime.add(-1 * 10 * 60, :second)\n          |> DateTime.add(i * 60, :second)\n        end)\n\n      after_lines =\n        business_log_line_serie(fn i ->\n          DateTime.utc_now() |> DateTime.add(i * 60, :second)\n        end)\n\n      File.write!(\n        LogRotate.path(:boruta_web, :business, Date.utc_today()),\n        Enum.map_join([log_lines, after_lines], fn serie ->\n          Enum.join(serie, \"\\n\") <> \"\\n\"\n        end) <> \"\\n\"\n      )\n\n      start_at =\n        DateTime.utc_now() |> DateTime.add(-1 * 10 * 60, :second) |> DateTime.to_iso8601()\n\n      end_at = DateTime.utc_now() |> DateTime.to_iso8601()\n\n      conn =\n        get(conn, Routes.admin_logs_path(conn, :index), %{\n          start_at: start_at,\n          end_at: end_at,\n          application: \"boruta_web\",\n          type: \"business\"\n        })\n\n      assert %{\n               \"time_scale_unit\" => \"second\",\n               \"overflow\" => false,\n               \"log_lines\" => ^log_lines,\n               \"log_count\" => 30\n             } = json_response(conn, 200)\n\n      File.rm!(LogRotate.path(:boruta_web, :business, Date.utc_today()))\n    end\n\n    @tag authorized: [\"logs:read:all\"]\n    test \"return multiple day logs\", %{conn: conn} do\n      first_day = Date.utc_today() |> Date.add(-10)\n      second_day = Date.utc_today() |> Date.add(-8)\n\n      File.mkdir(\"./log\")\n      File.rm(LogRotate.path(:boruta_web, :business, first_day))\n      File.rm(LogRotate.path(:boruta_web, :business, second_day))\n\n      [first_day_log_lines, second_day_log_lines] =\n        Enum.map([10, 8], fn day_shift ->\n          business_log_line_serie(fn i ->\n            DateTime.utc_now()\n            |> DateTime.add(-1 * 24 * 3600 * day_shift, :second)\n            |> DateTime.add(i * 60, :second)\n          end)\n        end)\n\n      File.write!(\n        LogRotate.path(:boruta_web, :business, first_day),\n        Enum.join(first_day_log_lines, \"\\n\")\n      )\n\n      File.write!(\n        LogRotate.path(:boruta_web, :business, second_day),\n        Enum.join(second_day_log_lines, \"\\n\")\n      )\n\n      start_at =\n        DateTime.utc_now()\n        |> DateTime.add(-1 * 10 * 24 * 3600 - 1, :second)\n        |> DateTime.to_iso8601()\n\n      end_at = DateTime.utc_now() |> DateTime.to_iso8601()\n\n      conn =\n        get(conn, Routes.admin_logs_path(conn, :index), %{\n          start_at: start_at,\n          end_at: end_at,\n          application: \"boruta_web\",\n          type: \"business\"\n        })\n\n      log_lines = first_day_log_lines ++ second_day_log_lines\n\n      assert %{\n               \"time_scale_unit\" => \"hour\",\n               \"overflow\" => false,\n               \"log_lines\" => ^log_lines,\n               \"log_count\" => 60\n             } = json_response(conn, 200)\n\n      File.rm!(LogRotate.path(:boruta_web, :business, first_day))\n      File.rm!(LogRotate.path(:boruta_web, :business, second_day))\n    end\n  end\n\n  defp request_log_line_serie(fun) do\n    Enum.flat_map(1..10, fn i ->\n      log_time = fun.(i)\n\n      Enum.map(@request_log_lines, fn log -> \"#{DateTime.to_iso8601(log_time)} #{log}\" end)\n    end)\n  end\n\n  defp business_log_line_serie(fun) do\n    Enum.flat_map(1..10, fn i ->\n      log_time = fun.(i)\n\n      Enum.map(@business_log_lines, fn log -> \"#{DateTime.to_iso8601(log_time)} #{log}\" end)\n    end)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/organization_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.OrganizationControllerTest do\n  use BorutaAdminWeb.ConnCase\n\n  import BorutaIdentity.Factory\n\n  alias BorutaIdentity.Organizations.Organization\n  alias BorutaIdentity.Repo\n\n  setup %{conn: conn} do\n    {:ok, conn: put_req_header(conn, \"accept\", \"application/json\")}\n  end\n\n  test \"returns a 401\", %{conn: conn} do\n    assert conn\n           |> get(Routes.admin_organization_path(conn, :index))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> post(Routes.admin_organization_path(conn, :create))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> patch(Routes.admin_organization_path(conn, :update, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> delete(Routes.admin_organization_path(conn, :delete, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n  end\n\n  describe \"with bad scope\" do\n    @tag authorized: [\"bad:scope\"]\n    test \"returns a 403\", %{conn: conn} do\n      assert conn\n             |> get(Routes.admin_organization_path(conn, :index))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> post(Routes.admin_organization_path(conn, :create))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> patch(Routes.admin_organization_path(conn, :update, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> delete(Routes.admin_organization_path(conn, :delete, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n    end\n  end\n\n  describe \"index\" do\n    @tag authorized: [\"users:manage:all\"]\n    test \"lists all organizations\", %{conn: conn} do\n      conn = get(conn, Routes.admin_organization_path(conn, :index))\n      assert json_response(conn, 200)[\"data\"] == []\n    end\n  end\n\n  describe \"create organization\" do\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders bad request\", %{\n      conn: conn\n    } do\n      conn = post(conn, Routes.admin_organization_path(conn, :create), %{})\n\n      assert json_response(conn, 400)\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders an error when data is invalid\", %{\n      conn: conn\n    } do\n      name = nil\n\n      conn =\n        post(conn, Routes.admin_organization_path(conn, :create), %{\n          \"organization\" => %{\n            \"name\" => name\n          }\n        })\n\n      assert json_response(conn, 422) == %{\n               \"code\" => \"UNPROCESSABLE_ENTITY\",\n               \"errors\" => %{\"name\" => [\"can't be blank\"]},\n               \"message\" => \"Your request could not be processed.\"\n             }\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders organization when data is valid\", %{\n      conn: conn\n    } do\n      name = \"Organization name\"\n\n      conn =\n        post(conn, Routes.admin_organization_path(conn, :create), %{\n          \"organization\" => %{\n            \"name\" => name\n          }\n        })\n\n      assert %{\"id\" => _id, \"name\" => ^name} = json_response(conn, 200)[\"data\"]\n    end\n  end\n\n  describe \"update organization\" do\n    setup do\n      organization = insert(:organization)\n\n      {:ok, organization: organization}\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders an error when bad request\", %{\n      conn: conn,\n      organization: organization\n    } do\n      conn = put(conn, Routes.admin_organization_path(conn, :update, organization), %{})\n\n      assert json_response(conn, 400)\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"updates organization with metadata\", %{\n      conn: conn,\n      organization: %Organization{id: id} = organization\n    } do\n      name = \"Organization name\"\n\n      conn =\n        put(conn, Routes.admin_organization_path(conn, :update, organization),\n          organization: %{\n            \"name\" => name\n          }\n        )\n\n      assert %{\"id\" => ^id, \"name\" => ^name} = json_response(conn, 200)[\"data\"]\n\n      assert %Organization{name: ^name} = Repo.get!(Organization, id)\n    end\n  end\n\n  describe \"delete organization\" do\n    @tag authorized: [\"users:manage:all\"]\n    test \"returns a 404\", %{conn: conn} do\n      organization_id = SecureRandom.uuid()\n\n      conn = delete(conn, Routes.admin_organization_path(conn, :delete, organization_id))\n\n      assert response(conn, 404)\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"deletes the organization\", %{conn: conn} do\n      %Organization{id: organization_id} = insert(:organization)\n\n      conn = delete(conn, Routes.admin_organization_path(conn, :delete, organization_id))\n\n      assert response(conn, 204)\n      refute BorutaIdentity.Repo.get(Organization, organization_id)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/page_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.PageControllerTest do\n  use BorutaAdminWeb.ConnCase\n\n  test \"GET /\", %{conn: conn} do\n    conn = get(conn, \"/\")\n    assert html_response(conn, 200) =~ \"app\"\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/role_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.RoleControllerTest do\n  import BorutaIdentity.Factory\n\n  use BorutaAdminWeb.ConnCase\n\n  alias BorutaIdentity.Accounts.Role\n\n  @create_attrs %{\n    name: \"some name\",\n  }\n  @update_attrs %{\n    name: \"some updated name\"\n  }\n  @invalid_attrs %{name: nil}\n  @protected_roles []\n\n  setup %{conn: conn} do\n    {:ok, conn: put_req_header(conn, \"accept\", \"application/json\")}\n  end\n\n  # TODO test sub restriction\n  test \"returns a 401\", %{conn: conn} do\n    assert conn\n           |> get(Routes.admin_role_path(conn, :index))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> post(Routes.admin_role_path(conn, :create))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> patch(Routes.admin_role_path(conn, :update, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> delete(Routes.admin_role_path(conn, :delete, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n  end\n\n  describe \"with bad role\" do\n    @tag authorized: [\"bad:scope\"]\n    test \"returns a 403\", %{conn: conn} do\n      assert conn\n             |> get(Routes.admin_role_path(conn, :index))\n             |> json_response(403) == %{\n               \"code\" =>\"FORBIDDEN\",\n               \"message\" =>\"You are forbidden to access this resource.\",\n               \"errors\" =>%{\n                 \"resource\" =>[\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> post(Routes.admin_role_path(conn, :create))\n             |> json_response(403) == %{\n               \"code\" =>\"FORBIDDEN\",\n               \"message\" =>\"You are forbidden to access this resource.\",\n               \"errors\" =>%{\n                 \"resource\" =>[\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> patch(Routes.admin_role_path(conn, :update, \"id\"))\n             |> json_response(403) == %{\n               \"code\" =>\"FORBIDDEN\",\n               \"message\" =>\"You are forbidden to access this resource.\",\n               \"errors\" =>%{\n                 \"resource\" =>[\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> delete(Routes.admin_role_path(conn, :delete, \"id\"))\n             |> json_response(403) == %{\n               \"code\" =>\"FORBIDDEN\",\n               \"message\" =>\"You are forbidden to access this resource.\",\n               \"errors\" =>%{\n                 \"resource\" =>[\"you are forbidden to access this resource.\"]\n               }\n             }\n    end\n  end\n\n  describe \"index\" do\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"lists all roles\", %{conn: conn} do\n      conn = get(conn, Routes.admin_role_path(conn, :index))\n      assert json_response(conn, 200)[\"data\"] == []\n    end\n  end\n\n  describe \"create role\" do\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"renders role when data is valid\", %{conn: conn} do\n      conn = post(conn, Routes.admin_role_path(conn, :create), role: @create_attrs)\n\n      assert %{\"id\" => _id, \"name\" => \"some name\"} =\n               json_response(conn, 201)[\"data\"]\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"renders errors when data is invalid\", %{conn: conn} do\n      conn = post(conn, Routes.admin_role_path(conn, :create), role: @invalid_attrs)\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n  end\n\n  describe \"update role\" do\n    setup %{conn: conn} do\n      role = insert(:role)\n\n      {:ok, conn: conn, existing_role: role}\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"renders role when data is valid\", %{conn: conn, existing_role: %Role{id: id} = role} do\n      conn = put(conn, Routes.admin_role_path(conn, :update, role), role: @update_attrs)\n      assert %{\"id\" => ^id} = json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"cannot update protected roles\", %{conn: conn} do\n      Enum.map(@protected_roles, fn name ->\n        conn = put(conn, Routes.admin_role_path(conn, :update, insert(:role, name: name)), role: @update_attrs)\n\n        assert response(conn, 403)\n      end)\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"renders errors when data is invalid\", %{conn: conn, existing_role: role} do\n      conn = put(conn, Routes.admin_role_path(conn, :update, role), role: @invalid_attrs)\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n  end\n\n  describe \"delete role\" do\n    setup %{conn: conn} do\n      role = insert(:role)\n\n      {:ok, conn: conn, existing_role: role}\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"cannot delete protected roles\", %{conn: conn} do\n      Enum.map(@protected_roles, fn name ->\n        conn = delete(conn, Routes.admin_role_path(conn, :delete, insert(:role, name: name)))\n\n        assert response(conn, 403)\n      end)\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"deletes chosen role\", %{conn: conn, existing_role: role} do\n      conn = delete(conn, Routes.admin_role_path(conn, :delete, role))\n      assert response(conn, 204)\n\n      assert_error_sent(404, fn ->\n        get(conn, Routes.admin_role_path(conn, :show, role))\n      end)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/scope_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.ScopeControllerTest do\n  import Boruta.Factory\n\n  use BorutaAdminWeb.ConnCase\n\n  alias Boruta.Ecto.Scope\n\n  @create_attrs %{\n    name: \"some:name\",\n    public: true\n  }\n  @update_attrs %{\n    name: \"some:updated:name\",\n    public: false\n  }\n  @invalid_attrs %{name: nil, public: nil}\n  @protected_scopes [\n    \"users:manage:all\",\n    \"clients:manage:all\",\n    \"identity-providers:manage:all\",\n    \"scopes:manage:all\",\n    \"upstreams:manage:all\"\n  ]\n\n  setup %{conn: conn} do\n    {:ok, conn: put_req_header(conn, \"accept\", \"application/json\")}\n  end\n\n  # TODO test sub restriction\n  test \"returns a 401\", %{conn: conn} do\n    assert conn\n           |> get(Routes.admin_scope_path(conn, :index))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> post(Routes.admin_scope_path(conn, :create))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> patch(Routes.admin_scope_path(conn, :update, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> delete(Routes.admin_scope_path(conn, :delete, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n  end\n\n  describe \"with bad scope\" do\n    @tag authorized: [\"bad:scope\"]\n    test \"returns a 403\", %{conn: conn} do\n      assert conn\n             |> get(Routes.admin_scope_path(conn, :index))\n             |> json_response(403) == %{\n               \"code\" =>\"FORBIDDEN\",\n               \"message\" =>\"You are forbidden to access this resource.\",\n               \"errors\" =>%{\n                 \"resource\" =>[\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> post(Routes.admin_scope_path(conn, :create))\n             |> json_response(403) == %{\n               \"code\" =>\"FORBIDDEN\",\n               \"message\" =>\"You are forbidden to access this resource.\",\n               \"errors\" =>%{\n                 \"resource\" =>[\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> patch(Routes.admin_scope_path(conn, :update, \"id\"))\n             |> json_response(403) == %{\n               \"code\" =>\"FORBIDDEN\",\n               \"message\" =>\"You are forbidden to access this resource.\",\n               \"errors\" =>%{\n                 \"resource\" =>[\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> delete(Routes.admin_scope_path(conn, :delete, \"id\"))\n             |> json_response(403) == %{\n               \"code\" =>\"FORBIDDEN\",\n               \"message\" =>\"You are forbidden to access this resource.\",\n               \"errors\" =>%{\n                 \"resource\" =>[\"you are forbidden to access this resource.\"]\n               }\n             }\n    end\n  end\n\n  describe \"index\" do\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"lists all scopes\", %{conn: conn} do\n      conn = get(conn, Routes.admin_scope_path(conn, :index))\n      assert json_response(conn, 200)[\"data\"] == []\n    end\n  end\n\n  describe \"create scope\" do\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"renders scope when data is valid\", %{conn: conn} do\n      conn = post(conn, Routes.admin_scope_path(conn, :create), scope: @create_attrs)\n\n      assert %{\"id\" => _id, \"name\" => \"some:name\", \"public\" => true} =\n               json_response(conn, 201)[\"data\"]\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"renders errors when data is invalid\", %{conn: conn} do\n      conn = post(conn, Routes.admin_scope_path(conn, :create), scope: @invalid_attrs)\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n  end\n\n  describe \"update scope\" do\n    setup %{conn: conn} do\n      scope = insert(:scope)\n\n      {:ok, conn: conn, existing_scope: scope}\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"renders scope when data is valid\", %{conn: conn, existing_scope: %Scope{id: id} = scope} do\n      conn = put(conn, Routes.admin_scope_path(conn, :update, scope), scope: @update_attrs)\n      assert %{\"id\" => ^id} = json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"cannot update protected scopes\", %{conn: conn} do\n      Enum.map(@protected_scopes, fn name ->\n        conn = put(conn, Routes.admin_scope_path(conn, :update, insert(:scope, name: name)), scope: @update_attrs)\n\n        assert response(conn, 403)\n      end)\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"renders errors when data is invalid\", %{conn: conn, existing_scope: scope} do\n      conn = put(conn, Routes.admin_scope_path(conn, :update, scope), scope: @invalid_attrs)\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n  end\n\n  describe \"delete scope\" do\n    setup %{conn: conn} do\n      scope = insert(:scope)\n\n      {:ok, conn: conn, existing_scope: scope}\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"cannot delete protected scopes\", %{conn: conn} do\n      Enum.map(@protected_scopes, fn name ->\n        conn = delete(conn, Routes.admin_scope_path(conn, :delete, insert(:scope, name: name)))\n\n        assert response(conn, 403)\n      end)\n    end\n\n    @tag authorized: [\"scopes:manage:all\"]\n    test \"deletes chosen scope\", %{conn: conn, existing_scope: scope} do\n      conn = delete(conn, Routes.admin_scope_path(conn, :delete, scope))\n      assert response(conn, 204)\n\n      assert_error_sent(404, fn ->\n        get(conn, Routes.admin_scope_path(conn, :show, scope))\n      end)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/upstream_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.UpstreamControllerTest do\n  use BorutaAdminWeb.ConnCase, async: false\n\n  alias BorutaGateway.Upstreams\n  alias BorutaGateway.Upstreams.Upstream\n\n  @create_attrs %{\n    scheme: \"https\",\n    host: \"host.test\",\n    port: 7777\n  }\n  @update_attrs %{\n    host: \"host.update\"\n  }\n  @invalid_attrs %{\n    host: nil\n  }\n\n  def fixture(:upstream) do\n    {:ok, upstream} = Upstreams.create_upstream(@create_attrs)\n    upstream\n  end\n\n  setup %{conn: conn} do\n    {:ok, conn: put_req_header(conn, \"accept\", \"application/json\")}\n  end\n\n  # TODO test sub restriction\n  test \"returns a 401\", %{conn: conn} do\n    assert conn\n           |> get(Routes.admin_upstream_path(conn, :index))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> post(Routes.admin_upstream_path(conn, :create))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> patch(Routes.admin_upstream_path(conn, :update, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> delete(Routes.admin_upstream_path(conn, :delete, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n  end\n\n  describe \"with bad scope\" do\n    @tag authorized: [\"bad:scope\"]\n    test \"returns a 403\", %{conn: conn} do\n      assert conn\n             |> get(Routes.admin_upstream_path(conn, :index))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> post(Routes.admin_upstream_path(conn, :create))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> patch(Routes.admin_upstream_path(conn, :update, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> delete(Routes.admin_upstream_path(conn, :delete, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n    end\n  end\n\n  describe \"index\" do\n    @tag authorized: [\"upstreams:manage:all\"]\n    test \"lists all upstreams\", %{conn: conn} do\n      conn = get(conn, Routes.admin_upstream_path(conn, :index))\n      assert json_response(conn, 200)[\"data\"] == %{}\n    end\n  end\n\n  describe \"node_list\" do\n    @tag authorized: [\"upstreams:manage:all\"]\n    test \"lists all boruta nodes\", %{conn: conn} do\n      configuration_file_path =\n        :code.priv_dir(:boruta_gateway)\n        |> Path.join(\"/test/configuration_files/full_configuration.yml\")\n      Application.put_env(:boruta_gateway, :configuration_path, configuration_file_path)\n\n      conn = get(conn, Routes.admin_upstream_path(conn, :node_list))\n      assert json_response(conn, 200)[\"data\"] == [\"full-configuration\"]\n    end\n\n    @tag authorized: [\"upstreams:manage:all\"]\n    test \"lists all boruta nodes from config\", %{conn: conn} do\n      configuration_file_path =\n        :code.priv_dir(:boruta_gateway)\n        |> Path.join(\"/test/configuration_files/full_configuration.yml\")\n      Application.put_env(:boruta_gateway, :configuration_path, configuration_file_path)\n\n      conn = get(conn, Routes.admin_upstream_path(conn, :node_list))\n      assert json_response(conn, 200)[\"data\"] == [\"full-configuration\"]\n    end\n  end\n\n  describe \"create upstream\" do\n    @tag authorized: [\"upstreams:manage:all\"]\n    test \"renders upstream when data is valid\", %{conn: conn} do\n      conn = post(conn, Routes.admin_upstream_path(conn, :create), upstream: @create_attrs)\n      assert %{\"id\" => _id} = json_response(conn, 201)[\"data\"]\n    end\n\n    @tag authorized: [\"upstreams:manage:all\"]\n    test \"renders errors when data is invalid\", %{conn: conn} do\n      conn = post(conn, Routes.admin_upstream_path(conn, :create), upstream: @invalid_attrs)\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n  end\n\n  describe \"update upstream\" do\n    setup %{conn: conn} do\n      upstream = fixture(:upstream)\n\n      {:ok, conn: conn, upstream: upstream}\n    end\n\n    @tag authorized: [\"upstreams:manage:all\"]\n    test \"renders upstream when data is valid\", %{\n      conn: conn,\n      upstream: %Upstream{id: id} = upstream\n    } do\n      conn =\n        put(conn, Routes.admin_upstream_path(conn, :update, upstream), upstream: @update_attrs)\n\n      assert %{\"id\" => ^id} = json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"upstreams:manage:all\"]\n    test \"renders errors when data is invalid\", %{conn: conn, upstream: upstream} do\n      conn =\n        put(conn, Routes.admin_upstream_path(conn, :update, upstream), upstream: @invalid_attrs)\n\n      assert json_response(conn, 422)[\"errors\"] != %{}\n    end\n  end\n\n  describe \"delete upstream\" do\n    setup %{conn: conn} do\n      upstream = fixture(:upstream)\n\n      {:ok, conn: conn, upstream: upstream}\n    end\n\n    @tag authorized: [\"upstreams:manage:all\"]\n    test \"deletes chosen upstream\", %{conn: conn, upstream: upstream} do\n      conn = delete(conn, Routes.admin_upstream_path(conn, :delete, upstream))\n      assert response(conn, 204)\n\n      assert_error_sent(404, fn ->\n        get(conn, Routes.admin_upstream_path(conn, :show, upstream))\n      end)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/controllers/user_controller_test.exs",
    "content": "defmodule BorutaAdminWeb.UserControllerTest do\n  use BorutaAdminWeb.ConnCase\n\n  import BorutaIdentity.AccountsFixtures\n  import BorutaIdentity.Factory\n\n  alias Boruta.Ecto.Admin\n  alias BorutaIdentity.Accounts.Internal\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Repo\n\n  setup %{conn: conn} do\n    {:ok, conn: put_req_header(conn, \"accept\", \"application/json\")}\n  end\n\n  # TODO test sub restriction\n  test \"returns a 401\", %{conn: conn} do\n    assert conn\n           |> get(Routes.admin_user_path(conn, :index))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> post(Routes.admin_user_path(conn, :create))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> patch(Routes.admin_user_path(conn, :update, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n\n    assert conn\n           |> delete(Routes.admin_user_path(conn, :delete, \"id\"))\n           |> json_response(401) == %{\n             \"code\" => \"UNAUTHORIZED\",\n             \"message\" => \"You are unauthorized to access this resource.\",\n             \"errors\" => %{\n               \"resource\" => [\"you are unauthorized to access this resource.\"]\n             }\n           }\n  end\n\n  describe \"with bad scope\" do\n    @tag authorized: [\"bad:scope\"]\n    test \"returns a 403\", %{conn: conn} do\n      assert conn\n             |> get(Routes.admin_user_path(conn, :index))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> post(Routes.admin_user_path(conn, :create))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> patch(Routes.admin_user_path(conn, :update, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n\n      assert conn\n             |> delete(Routes.admin_user_path(conn, :delete, \"id\"))\n             |> json_response(403) == %{\n               \"code\" => \"FORBIDDEN\",\n               \"message\" => \"You are forbidden to access this resource.\",\n               \"errors\" => %{\n                 \"resource\" => [\"you are forbidden to access this resource.\"]\n               }\n             }\n    end\n  end\n\n  describe \"index\" do\n    @tag authorized: [\"users:manage:all\"]\n    test \"lists all users\", %{conn: conn} do\n      conn = get(conn, Routes.admin_user_path(conn, :index))\n      assert json_response(conn, 200)[\"data\"] == []\n    end\n  end\n\n  describe \"create user\" do\n    setup %{conn: conn} do\n      {:ok, scope} = Admin.create_scope(%{name: \"some:scope\"})\n      role = insert(:role)\n      organization = insert(:organization)\n      insert(:role_scope, role_id: role.id, scope_id: scope.id)\n\n      {:ok,\n       conn: conn, existing_scope: scope, existing_role: role, existing_organization: organization}\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders bad request\", %{\n      conn: conn\n    } do\n      conn = post(conn, Routes.admin_user_path(conn, :create), %{})\n\n      assert json_response(conn, 400)\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders an error when data is invalid\", %{\n      conn: conn\n    } do\n      email = unique_user_email()\n\n      conn =\n        post(conn, Routes.admin_user_path(conn, :create), %{\n          \"backend_id\" => insert(:backend).id,\n          \"user\" => %{\n            \"email\" => email\n          }\n        })\n\n      assert json_response(conn, 422) == %{\n               \"code\" => \"UNPROCESSABLE_ENTITY\",\n               \"errors\" => %{\"password\" => [\"can't be blank\"]},\n               \"message\" => \"Your request could not be processed.\"\n             }\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders user when data is valid\", %{\n      conn: conn\n    } do\n      email = unique_user_email()\n\n      conn =\n        post(conn, Routes.admin_user_path(conn, :create), %{\n          \"backend_id\" => insert(:backend).id,\n          \"user\" => %{\n            \"email\" => email,\n            \"password\" => valid_user_password()\n          }\n        })\n\n      assert %{\"id\" => _id, \"email\" => ^email} = json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"creates user with authorized scopes\", %{\n      conn: conn,\n      existing_scope: scope\n    } do\n      conn =\n        post(conn, Routes.admin_user_path(conn, :create), %{\n          \"backend_id\" => insert(:backend).id,\n          \"user\" => %{\n            \"email\" => unique_user_email(),\n            \"password\" => valid_user_password(),\n            \"authorized_scopes\" => [%{\"id\" => scope.id}]\n          }\n        })\n\n      scope_id = scope.id\n\n      assert %{\"id\" => _id, \"authorized_scopes\" => [%{\"id\" => ^scope_id}]} =\n               json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"creates user with organizations\", %{\n      conn: conn,\n      existing_organization: organization\n    } do\n      conn =\n        post(conn, Routes.admin_user_path(conn, :create), %{\n          \"backend_id\" => insert(:backend).id,\n          \"user\" => %{\n            \"email\" => unique_user_email(),\n            \"password\" => valid_user_password(),\n            \"organizations\" => [%{\"id\" => organization.id}]\n          }\n        })\n\n      organization_id = organization.id\n\n      assert %{\"id\" => _id, \"organizations\" => [%{\"id\" => ^organization_id}]} =\n               json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"creates user with roles\", %{\n      conn: conn,\n      existing_scope: scope,\n      existing_role: role\n    } do\n      conn =\n        post(conn, Routes.admin_user_path(conn, :create), %{\n          \"backend_id\" => insert(:backend).id,\n          \"user\" => %{\n            \"email\" => unique_user_email(),\n            \"password\" => valid_user_password(),\n            \"roles\" => [%{\"id\" => role.id}]\n          }\n        })\n\n      scope_id = scope.id\n      role_id = role.id\n\n      assert %{\"id\" => _id, \"roles\" => [%{\"id\" => ^role_id, \"scopes\" => [%{\"id\" => ^scope_id}]}]} =\n               json_response(conn, 200)[\"data\"]\n    end\n  end\n\n  describe \"import users\" do\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders bad request\", %{\n      conn: conn\n    } do\n      conn = post(conn, Routes.admin_user_path(conn, :create), %{})\n\n      assert json_response(conn, 400)\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders an error when data is invalid\", %{\n      conn: conn\n    } do\n      email = unique_user_email()\n\n      conn =\n        post(conn, Routes.admin_user_path(conn, :create), %{\n          \"backend_id\" => insert(:backend).id,\n          \"user\" => %{\n            \"email\" => email\n          }\n        })\n\n      assert json_response(conn, 422) == %{\n               \"code\" => \"UNPROCESSABLE_ENTITY\",\n               \"errors\" => %{\"password\" => [\"can't be blank\"]},\n               \"message\" => \"Your request could not be processed.\"\n             }\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders import result when data is valid\", %{\n      conn: conn\n    } do\n      conn =\n        post(conn, Routes.admin_user_path(conn, :create), %{\n          \"backend_id\" => insert(:backend).id,\n          \"file\" => %Plug.Upload{\n            path: Path.join(__DIR__, \"./../../data/import_users_password_valid.csv\"),\n            filename: \"users.csv\"\n          },\n          \"options\" => %{\n            \"hash_password\" => \"true\"\n          }\n        })\n\n      assert json_response(conn, 200) == %{\n               \"error_count\" => 0,\n               \"errors\" => [],\n               \"success_count\" => 2\n             }\n\n      assert Repo.all(Internal.User) |> Enum.count() == 2\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders import result when data is valid with custom headers\", %{\n      conn: conn\n    } do\n      conn =\n        post(conn, Routes.admin_user_path(conn, :create), %{\n          \"backend_id\" => insert(:backend).id,\n          \"file\" => %Plug.Upload{\n            path:\n              Path.join(__DIR__, \"./../../data/import_users_password_custom_headers_valid.csv\"),\n            filename: \"users.csv\"\n          },\n          \"options\" => %{\n            \"hash_password\" => \"true\",\n            \"username_header\" => \"username_header\",\n            \"password_header\" => \"password_header\"\n          }\n        })\n\n      assert json_response(conn, 200) == %{\n               \"error_count\" => 0,\n               \"errors\" => [],\n               \"success_count\" => 2\n             }\n\n      assert Repo.all(Internal.User) |> Enum.count() == 2\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders import result users when data is invalid\", %{\n      conn: conn\n    } do\n      conn =\n        post(conn, Routes.admin_user_path(conn, :create), %{\n          \"backend_id\" => insert(:backend).id,\n          \"file\" => %Plug.Upload{\n            path: Path.join(__DIR__, \"./../../data/import_users_password_invalid.csv\"),\n            filename: \"users.csv\"\n          },\n          \"options\" => %{\n            \"hash_password\" => \"true\"\n          }\n        })\n\n      assert json_response(conn, 200) == %{\n               \"error_count\" => 3,\n               \"errors\" => [\n                 %{\n                   \"changeset\" => %{\"password\" => [\"should be at least 12 character(s)\"]},\n                   \"line\" => 1\n                 },\n                 %{\n                   \"changeset\" => %{\n                     \"email\" => [\"can't be blank\"],\n                     \"password\" => [\"should be at least 12 character(s)\"]\n                   },\n                   \"line\" => 2\n                 },\n                 %{\"changeset\" => %{\"email\" => [\"can't be blank\"]}, \"line\" => 3}\n               ],\n               \"success_count\" => 1\n             }\n\n      assert Repo.all(Internal.User) |> Enum.count() == 1\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders import result users when data is valid with hashed password\", %{\n      conn: conn\n    } do\n      conn =\n        post(conn, Routes.admin_user_path(conn, :create), %{\n          \"backend_id\" => insert(:backend).id,\n          \"file\" => %Plug.Upload{\n            path: Path.join(__DIR__, \"./../../data/import_users_hashed_password_valid.csv\"),\n            filename: \"users.csv\"\n          },\n          \"options\" => %{\n            \"hash_password\" => false\n          }\n        })\n\n      assert json_response(conn, 200) == %{\n               \"error_count\" => 0,\n               \"errors\" => [],\n               \"success_count\" => 2\n             }\n\n      assert Repo.all(Internal.User) |> Enum.count() == 2\n    end\n  end\n\n  describe \"update user\" do\n    setup %{conn: conn} do\n      user = user_fixture()\n      {:ok, scope} = Admin.create_scope(%{name: \"some:scope\"})\n      role = insert(:role)\n      insert(:role_scope, role_id: role.id, scope_id: scope.id)\n\n      {:ok, conn: conn, user: user, existing_scope: scope, existing_role: role}\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"renders an error when bad request\", %{\n      conn: conn,\n      user: user\n    } do\n      conn = put(conn, Routes.admin_user_path(conn, :update, user), %{})\n\n      assert json_response(conn, 400)\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"updates user with metadata\", %{\n      conn: conn,\n      user: %User{id: id} = user\n    } do\n      {:ok, _backend} =\n        Ecto.Changeset.change(user.backend, %{metadata_fields: [%{attribute_name: \"test\"}]})\n        |> Repo.update()\n\n      metadata = %{\"test\" => %{\"value\" => \"test value\", \"status\" => \"valid\", \"display\" => []}}\n\n      conn =\n        put(conn, Routes.admin_user_path(conn, :update, user),\n          user: %{\n            \"metadata\" => metadata\n          }\n        )\n\n      assert %{\n               \"id\" => ^id,\n               \"metadata\" => %{\n                 \"test\" => %{\"value\" => \"test value\", \"status\" => \"valid\", \"display\" => []}\n               }\n             } = json_response(conn, 200)[\"data\"]\n\n      assert %User{metadata: ^metadata} = Repo.get!(User, id)\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"updates user with group\", %{\n      conn: conn,\n      user: %User{id: id} = user\n    } do\n      group = \"group1 group2\"\n\n      conn =\n        put(conn, Routes.admin_user_path(conn, :update, user),\n          user: %{\n            \"group\" => group\n          }\n        )\n\n      assert %{\"id\" => ^id, \"group\" => ^group} = json_response(conn, 200)[\"data\"]\n      assert %User{group: ^group} = Repo.get!(User, id)\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"updates user with authorized scopes\", %{\n      conn: conn,\n      user: %User{id: id} = user,\n      existing_scope: scope\n    } do\n      conn =\n        put(conn, Routes.admin_user_path(conn, :update, user),\n          user: %{\n            \"authorized_scopes\" => [%{\"id\" => scope.id}]\n          }\n        )\n\n      assert %{\"id\" => ^id} = json_response(conn, 200)[\"data\"]\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"updates user with roles\", %{\n      conn: conn,\n      user: %User{id: id} = user,\n      existing_scope: scope,\n      existing_role: role\n    } do\n      conn =\n        put(conn, Routes.admin_user_path(conn, :update, user),\n          user: %{\n            \"roles\" => [%{\"id\" => role.id}]\n          }\n        )\n\n      scope_id = scope.id\n      role_id = role.id\n\n      assert %{\"id\" => ^id, \"roles\" => [%{\"id\" => ^role_id, \"scopes\" => [%{\"id\" => ^scope_id}]}]} =\n               json_response(conn, 200)[\"data\"]\n    end\n\n    @tag user_authorized: [\"users:manage:all\"]\n    test \"cannot update current user\", %{\n      conn: conn,\n      existing_scope: scope,\n      resource_owner: resource_owner\n    } do\n      conn =\n        put(conn, Routes.admin_user_path(conn, :update, resource_owner.sub),\n          user: %{\n            \"authorized_scopes\" => [%{\"name\" => scope.name}]\n          }\n        )\n\n      assert response(conn, 403)\n    end\n  end\n\n  describe \"delete\" do\n    @tag authorized: [\"users:manage:all\"]\n    test \"returns a 404\", %{conn: conn} do\n      user_id = SecureRandom.uuid()\n\n      conn = delete(conn, Routes.admin_user_path(conn, :delete, user_id))\n\n      assert response(conn, 404)\n    end\n\n    @tag authorized: [\"users:manage:all\"]\n    test \"deletes the user\", %{conn: conn} do\n      %{id: user_id, uid: user_uid} = user_fixture()\n\n      conn = delete(conn, Routes.admin_user_path(conn, :delete, user_id))\n\n      assert response(conn, 204)\n      refute BorutaIdentity.Repo.get(User, user_id)\n      refute BorutaIdentity.Repo.get(Internal.User, user_uid)\n    end\n\n    @tag user_authorized: [\"users:manage:all\"]\n    test \"cannot delete current user\", %{conn: conn, resource_owner: resource_owner} do\n      conn = delete(conn, Routes.admin_user_path(conn, :delete, resource_owner.sub))\n\n      assert response(conn, 403)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/views/error_view_test.exs",
    "content": "defmodule BorutaAdminWeb.ErrorViewTest do\n  use BorutaAdminWeb.ConnCase, async: true\n\n  import Phoenix.View\n\n  test \"renders 404.html\" do\n    assert render_to_string(BorutaAdminWeb.ErrorView, \"404.html\", []) =~ \"Page not found\"\n  end\n\n  test \"renders 500.html\" do\n    assert render_to_string(BorutaAdminWeb.ErrorView, \"500.html\", []) =~ \"Internal server error\"\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/boruta_admin_web/views/page_view_test.exs",
    "content": "defmodule BorutaAdminWeb.PageViewTest do\n  use BorutaAdminWeb.ConnCase, async: true\n\n  import Phoenix.View\n\n  test \"renders admin page\", %{conn: conn} do\n    conn = get(conn, \"/\")\n\n    assert render_to_string(BorutaAdminWeb.PageView, \"admin.html\", conn: conn) =~ \"Administration panel\"\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/data/import_users_hashed_password_valid.csv",
    "content": "username,password\ntest1@test.test,\"$argon2id$v=19$m=131072,t=8,p=4$5hVLIQVnI5VrT90Oox0G6A$SSP9eON/XDV9V/6iZ165ii9wK/Av4NPpyYFmfa0HBvc\"\ntest2@test.test,\"$argon2id$v=19$m=131072,t=8,p=4$5hVLIQVnI5VrT90Oox0G6A$SSP9eON/XDV9V/6iZ165ii9wK/Av4NPpyYFmfa0HBvc\"\n"
  },
  {
    "path": "apps/boruta_admin/test/data/import_users_password_custom_headers_valid.csv",
    "content": "username_header,password_header\ntest1@test.test,averysecretpassword\ntest2@test.test,averysecretpassword\n"
  },
  {
    "path": "apps/boruta_admin/test/data/import_users_password_invalid.csv",
    "content": "username,password\ntest1@test.test,short\n,short\n,averysecretpassword\ntest2@test.test,averysecretpassword\n"
  },
  {
    "path": "apps/boruta_admin/test/data/import_users_password_valid.csv",
    "content": "username,password\ntest1@test.test,averysecretpassword\ntest2@test.test,averysecretpassword\n"
  },
  {
    "path": "apps/boruta_admin/test/support/boruta_factory.ex",
    "content": "defmodule Boruta.Factory do\n  @moduledoc false\n\n  use ExMachina.Ecto, repo: BorutaAuth.Repo\n\n  alias Boruta.Ecto\n\n  def client_factory do\n    %Ecto.Client{\n      secret: SecureRandom.urlsafe_base64(),\n      redirect_uris: [\"https://redirect.uri/oauth2-redirect-path\"],\n      access_token_ttl: 3600,\n      authorization_code_ttl: 60,\n      private_key:\n        \"-----BEGIN RSA PRIVATE KEY-----\\nMIIEowIBAAKCAQEA1PaP/gbXix5itjRCaegvI/B3aFOeoxlwPPLvfLHGA4QfDmVO\\nf8cU8OuZFAYzLArW3PnnwWWy39nVJOx42QRVGCGdUCmV7shDHRsr86+2DlL7pwUa\\n9QyHsTj84fAJn2Fv9h9mqrIvUzAtEYRlGFvjVTGCwzEullpsB0GJafopUTFby8Wd\\nSq3dGLJBB1r+Q8QtZnAxxvolhwOmYkBkkidefmm48X7hFXL2cSJm2G7wQyinOey/\\nU8xDZ68mgTakiqS2RtjnFD0dnpBl5CYTe4s6oZKEyFiFNiW4KkR1GVjsKwY9oC2t\\npyQ0AEUMvk9T9VdIltSIiAvOKlwFzL49cgwZDwIDAQABAoIBAG0dg/upL8k1IWiv\\n8BNphrXIYLYQmiiBQTPJWZGvWIC2sl7i40yvCXjDjiRnZNK9HwgL94XtALCXYRFR\\nJD41bRA3MO5A0HSPIWwJXwS10/cU56HVCNHjwKa6Rz/QiG2kNASMZEMzlvHtrjna\\ndx36/sjI3HH8gh1BaTZyiuDE72SMkPbL838jfL1YY9uJ0u6hWFDbdn3sqPfJ6Cnz\\n1cu0piT35nkilnIGCNYA0i3lyMeo4XrdXaAJdN9nnqbCi5ewQWqaHbrIIY5LTgzJ\\nYlOr3IiecyokFxHCbULXle60u0KqXYgBHmlQJJr1Dj4c9AkQmefjC2jRMlhOrIzo\\nIkIUeMECgYEA+MNLB+w6vv1ogqzM3M1OLt6bziWJCn+XkziuMrCiY9KeDD+S70+E\\nhfbhM5RjCE3wxC/k59039laT973BmdMHxrDd2zSjOFmCIORv5yrD5oBHMaMZcwuQ\\n45Xisi4aoQoOhyznSnjo/RjeQB7qEDzXFznLLNT79HzqyAtCWD3UIu8CgYEA2yik\\n9FKl7HJEY94D2K6vNh1AHGnkwIQC72pXzlUrVuwQYngj6/Gkhw8ayFBApHfwVCXj\\no9rDYPdNrrAs0Zz0JsiJp6bOCEKCrMYE16UiejUUAg/OZ5eg6+3m3/iWatkzLUuK\\n1LIkVBJlEyY0uPuAaBF0V0VleNvfCGhVYOn46+ECgYAUD4OsduNh5YOZDiBTKgdF\\nBlSgMiyz+QgbKjX6Bn6B+EkgibvqqonwV7FffHbkA40H9SjLfe52YhL6poXHRtpY\\nroillcAX2jgBOQrBJJS5sNyM5y81NNiRUdP/NHKXS/1R71ATlF6NkoTRvOx5NL7P\\ns6xryB0tYSl5ylamUQ4bZwKBgHF6FB9mA//wErVbKcayfIqajq2nrwh30kVBXQG7\\nW9uAE+PIrWDoF/bOvWFnHHGMoOYRUFNxXKUCqDiBhFNs34aNY6lpV1kzhxIK3ksC\\neF2qyhdfM9Kz0mEXJ+pkfw4INNWJPfNv4hueArPtnnMB1rUMBJ+DkU0JG+zwiPTL\\ncVZBAoGBAM6kOsh5KGn3aI83g9ZO0TrKLXXFotxJt31Wu11ydj9K33/Qj3UXcxd4\\nJPXr600F0DkLeUKBob6BALeHFWcrSz5FGLGRqdRxdv+L6g18WH5m2xEs7o6M6e5I\\nIhyUC60ZewJ2M8rV4KgCJJdZE2kENlSgjU92IDVPT9Oetrc7hQJd\\n-----END RSA PRIVATE KEY-----\\n\\n\",\n      public_key:\n        \"-----BEGIN RSA PUBLIC KEY-----\\nMIIBCgKCAQEA1PaP/gbXix5itjRCaegvI/B3aFOeoxlwPPLvfLHGA4QfDmVOf8cU\\n8OuZFAYzLArW3PnnwWWy39nVJOx42QRVGCGdUCmV7shDHRsr86+2DlL7pwUa9QyH\\nsTj84fAJn2Fv9h9mqrIvUzAtEYRlGFvjVTGCwzEullpsB0GJafopUTFby8WdSq3d\\nGLJBB1r+Q8QtZnAxxvolhwOmYkBkkidefmm48X7hFXL2cSJm2G7wQyinOey/U8xD\\nZ68mgTakiqS2RtjnFD0dnpBl5CYTe4s6oZKEyFiFNiW4KkR1GVjsKwY9oC2tpyQ0\\nAEUMvk9T9VdIltSIiAvOKlwFzL49cgwZDwIDAQAB\\n-----END RSA PUBLIC KEY-----\\n\\n\"\n    }\n  end\n\n  def scope_factory do\n    %Ecto.Scope{\n      name: SecureRandom.hex(10),\n      public: false\n    }\n  end\n\n  def token_factory do\n    %Ecto.Token{\n      client: build(:client),\n      type: \"access_token\",\n      value: Boruta.TokenGenerator.generate(),\n      expires_at: :os.system_time(:seconds) + 10\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/support/boruta_identity_factory.ex",
    "content": "defmodule BorutaIdentity.Factory do\n  @moduledoc false\n\n  use ExMachina.Ecto, repo: BorutaIdentity.Repo\n\n  alias BorutaIdentity.Accounts.Consent\n  alias BorutaIdentity.Accounts.EmailTemplate\n  alias BorutaIdentity.Accounts.Internal\n  alias BorutaIdentity.Accounts.Role\n  alias BorutaIdentity.Accounts.RoleScope\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Configuration.ErrorTemplate\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.IdentityProviders.ClientIdentityProvider\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.IdentityProviders.Template\n  alias BorutaIdentity.Organizations.Organization\n\n  # @password \"hello world!\"\n  @hashed_password \"$argon2id$v=19$m=131072,t=8,p=4$9lPv7KsJogno0FlnhaRQXA$TeTY9FYjR1HJtZzg+N1z0oDC+0Mn7buPpOMhDP+M2Ik\"\n\n  def user_factory do\n    %User{\n      username: \"user#{System.unique_integer()}@example.com\",\n      uid: SecureRandom.hex(),\n      backend: build(:backend)\n    }\n  end\n\n  def internal_user_factory do\n    %Internal.User{\n      email: \"user#{System.unique_integer()}@example.com\",\n      hashed_password: @hashed_password,\n      backend: build(:backend)\n    }\n  end\n\n  def consent_factory do\n    %Consent{\n      client_id: SecureRandom.uuid(),\n      scopes: []\n    }\n  end\n\n  def client_identity_provider_factory do\n    %ClientIdentityProvider{\n      client_id: SecureRandom.uuid(),\n      identity_provider: build(:identity_provider)\n    }\n  end\n\n  def identity_provider_factory do\n    %IdentityProvider{\n      name: sequence(:name, &\"identity provider #{&1}\"),\n      backend: build(:backend)\n    }\n  end\n\n  def backend_factory do\n    %Backend{\n      name: \"backend name\",\n      type: \"Elixir.BorutaIdentity.Accounts.Internal\"\n    }\n  end\n\n  def default_backend_factory do\n    %Backend{\n      name: \"backend name\",\n      type: \"Elixir.BorutaIdentity.Accounts.Internal\",\n      is_default: true\n    }\n  end\n\n  def template_factory do\n    %Template{\n      type: \"new_registration\",\n      content: \"new registration template content\",\n      identity_provider: build(:identity_provider)\n    }\n  end\n\n  def error_template_factory do\n    %ErrorTemplate{\n      type: \"400\",\n      content: \"error template content\"\n    }\n  end\n\n  def email_template_factory do\n    %EmailTemplate{\n      type: \"template_type\",\n      txt_content: \"template content\",\n      html_content: \"template content\"\n    }\n  end\n\n  def reset_password_instructions_email_template_factory do\n    %EmailTemplate{\n      type: \"reset_password_instructions\",\n      txt_content: EmailTemplate.default_txt_content(:reset_password_instructions),\n      html_content: EmailTemplate.default_html_content(:reset_password_instructions)\n    }\n  end\n\n  def role_factory do\n    %Role{\n      name: SecureRandom.hex(32)\n    }\n  end\n\n  def role_scope_factory do\n    %RoleScope{}\n  end\n\n  def organization_factory do\n    %Organization{\n      name: \"Organization \" <> SecureRandom.hex()\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/support/conn_case.ex",
    "content": "defmodule BorutaAdminWeb.ConnCase do\n  @moduledoc \"\"\"\n  This module defines the test case to be used by\n  tests that require setting up a connection.\n\n  Such tests rely on `Phoenix.ConnTest` and also\n  import other functionality to make it easier\n  to build common data structures and query the data layer.\n\n  Finally, if the test case interacts with the database,\n  we enable the SQL sandbox, so changes done to the database\n  are reverted at the end of every test. If you are using\n  PostgreSQL, you can even run database tests asynchronously\n  by setting `use BorutaAdminWeb.ConnCase, async: true`, although\n  this option is not recommended for other databases.\n  \"\"\"\n\n  use ExUnit.CaseTemplate\n\n  import BorutaIdentity.AccountsFixtures\n\n  alias Boruta.Ecto.OauthMapper\n  alias Boruta.Oauth.IntrospectResponse\n  alias Ecto.Adapters.SQL.Sandbox\n\n  using do\n    quote do\n      # Import conveniences for testing with connections\n      import Plug.Conn\n      import Phoenix.ConnTest\n      import BorutaAdminWeb.ConnCase\n\n      alias BorutaAdminWeb.Router.Helpers, as: Routes\n\n      # The default endpoint for testing\n      @endpoint BorutaAdminWeb.Endpoint\n    end\n  end\n\n  setup tags do\n    :ok = Sandbox.checkout(BorutaAdmin.Repo)\n    :ok = Sandbox.checkout(BorutaAuth.Repo)\n    :ok = Sandbox.checkout(BorutaGateway.Repo)\n    :ok = Sandbox.checkout(BorutaIdentity.Repo)\n    :ok = Sandbox.checkout(BorutaWeb.Repo)\n\n    unless tags[:async] do\n      Sandbox.mode(BorutaAuth.Repo, {:shared, self()})\n      Sandbox.mode(BorutaAdmin.Repo, {:shared, self()})\n      Sandbox.mode(BorutaGateway.Repo, {:shared, self()})\n      Sandbox.mode(BorutaIdentity.Repo, {:shared, self()})\n      Sandbox.mode(BorutaWeb.Repo, {:shared, self()})\n    end\n\n    conn = Phoenix.ConnTest.build_conn()\n\n    {:ok, merge_tags_params(conn, tags)}\n  end\n\n  def merge_tags_params(conn, tags) do\n    Enum.reduce(tags, [conn: conn], fn\n      {:authorized, scopes}, params ->\n        Keyword.merge(\n          params,\n          authorized_params(conn, scopes)\n        )\n\n      {:user_authorized, scopes}, params ->\n        Keyword.merge(\n          params,\n          user_authorized_params(conn, scopes)\n        )\n\n      _, params ->\n        params\n    end)\n  end\n\n  def authorized_params(conn, scopes) do\n    token =\n      Boruta.Factory.insert(\n        :token,\n        type: \"access_token\",\n        scope: Enum.join(scopes, \" \")\n      )\n\n    conn = Plug.Conn.put_req_header(conn, \"authorization\", \"Bearer #{token.value}\")\n\n    [conn: conn]\n  end\n\n  def user_authorized_params(conn, scopes) do\n    %{id: sub} = user_fixture()\n    resource_owner = %Boruta.Oauth.ResourceOwner{sub: sub}\n\n    token =\n      Boruta.Factory.insert(:token,\n        type: \"access_token\",\n        scope: Enum.join(scopes, \" \"),\n        sub: resource_owner.sub\n      )\n\n    conn = Plug.Conn.put_req_header(conn, \"authorization\", \"Bearer #{token.value}\")\n\n    # TODO test external oauth provider\n    # %URI{port: port} =\n    #   URI.parse(\n    #     Application.get_env(:boruta_web, BorutaAdminWeb.Authorization)[:oauth2][\n    #       :site\n    #     ]\n    #   )\n\n    # bypass = Bypass.open(port: port)\n    # Bypass.up(bypass)\n\n    # userinfo =\n    #   with {:ok, token} <- Boruta.Oauth.Token.userinfo(token) do\n    #     UserinfoResponse.from_userinfo(token, token.client)\n    #     |> UserinfoResponse.payload()\n    #   end\n\n    # Bypass.stub(bypass, \"POST\", \"/oauth/userinfo\", fn conn ->\n    #   Plug.Conn.resp(conn, 200, Jason.encode!(userinfo))\n    # end)\n\n    [conn: conn, resource_owner: resource_owner]\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/support/data_case.ex",
    "content": "defmodule BorutaAdmin.DataCase do\n  @moduledoc \"\"\"\n  This module defines the setup for tests requiring\n  access to the application's data layer.\n\n  You may define functions here to be used as helpers in\n  your tests.\n\n  Finally, if the test case interacts with the database,\n  we enable the SQL sandbox, so changes done to the database\n  are reverted at the end of every test. If you are using\n  PostgreSQL, you can even run database tests asynchronously\n  by setting `use BorutaAdmin.DataCase, async: true`, although\n  this option is not recommended for other databases.\n  \"\"\"\n\n  use ExUnit.CaseTemplate\n\n  alias Ecto.Adapters.SQL.Sandbox\n\n  using do\n    quote do\n      alias BorutaAdmin.Repo\n\n      import Ecto\n      import Ecto.Changeset\n      import Ecto.Query\n      import BorutaAdmin.DataCase\n    end\n  end\n\n  setup tags do\n    :ok = Sandbox.checkout(BorutaAdmin.Repo)\n    :ok = Sandbox.checkout(BorutaAuth.Repo)\n    :ok = Sandbox.checkout(BorutaGateway.Repo)\n    :ok = Sandbox.checkout(BorutaIdentity.Repo)\n    :ok = Sandbox.checkout(BorutaWeb.Repo)\n\n    unless tags[:async] do\n      Sandbox.mode(BorutaAuth.Repo, {:shared, self()})\n      Sandbox.mode(BorutaAdmin.Repo, {:shared, self()})\n      Sandbox.mode(BorutaGateway.Repo, {:shared, self()})\n      Sandbox.mode(BorutaIdentity.Repo, {:shared, self()})\n      Sandbox.mode(BorutaWeb.Repo, {:shared, self()})\n    end\n\n    :ok\n  end\n\n  @doc \"\"\"\n  A helper that transforms changeset errors into a map of messages.\n\n      assert {:error, changeset} = Accounts.create_user(%{password: \"short\"})\n      assert \"password is too short\" in errors_on(changeset).password\n      assert %{password: [\"password is too short\"]} = errors_on(changeset)\n\n  \"\"\"\n  def errors_on(changeset) do\n    Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->\n      Regex.replace(~r\"%{(\\w+)}\", message, fn _, key ->\n        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()\n      end)\n    end)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_admin/test/test_helper.exs",
    "content": "ExUnit.start()\n\nMox.defmock(BorutaIdentity.LdapRepoMock, for: BorutaIdentity.LdapRepo)\n\nEcto.Adapters.SQL.Sandbox.mode(BorutaAdmin.Repo, :manual)\nEcto.Adapters.SQL.Sandbox.mode(BorutaAuth.Repo, :manual)\nEcto.Adapters.SQL.Sandbox.mode(BorutaGateway.Repo, :manual)\nEcto.Adapters.SQL.Sandbox.mode(BorutaIdentity.Repo, :manual)\nEcto.Adapters.SQL.Sandbox.mode(BorutaWeb.Repo, :manual)\n"
  },
  {
    "path": "apps/boruta_auth/.formatter.exs",
    "content": "# Used by \"mix format\"\n[\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": "apps/boruta_auth/.gitignore",
    "content": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up here.\n/cover/\n\n# The directory Mix downloads your dependencies sources to.\n/deps/\n\n# Where third-party dependencies like ExDoc output generated docs.\n/doc/\n\n# Ignore .fetch files in case you like to edit your project deps locally.\n/.fetch\n\n# If the VM crashes, it generates a dump, let's ignore it too.\nerl_crash.dump\n\n# Also ignore archive artifacts (built via \"mix archive.build\").\n*.ez\n\n# Ignore package tarball (built via \"mix hex.build\").\nboruta_auth-*.tar\n\n# Temporary files, for example, from tests.\n/tmp/\n"
  },
  {
    "path": "apps/boruta_auth/config/config.exs",
    "content": "import Config\n\nconfig :boruta_auth,\n  ecto_repos: [BorutaAuth.Repo]\n\nconfig :boruta, Boruta.Oauth,\n  repo: BorutaAuth.Repo,\n  contexts: [\n    resource_owners: BorutaIdentity.ResourceOwners\n  ],\n  issuer: System.get_env(\"BORUTA_OAUTH_BASE_URL\", \"http://localhost:4000\")\n\nconfig :boruta_auth, BorutaAuth.Scheduler,\n  jobs: [\n    {\"@daily\", {BorutaAuth.LogRotate, :rotate, []}}\n  ]\n\nconfig :boruta_auth, BorutaAuth.LogRotate,\n  max_retention_days: String.to_integer(System.get_env(\"MAX_LOG_RETENTION_DAYS\", \"60\"))\n\nimport_config \"#{Mix.env()}.exs\"\n"
  },
  {
    "path": "apps/boruta_auth/config/dev.exs",
    "content": "import Config\n\nconfig :boruta_identity, BorutaIdentity.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\"\n\nconfig :boruta_auth, BorutaAuth.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\"\n"
  },
  {
    "path": "apps/boruta_auth/config/prod.exs",
    "content": "import Config\n"
  },
  {
    "path": "apps/boruta_auth/config/test.exs",
    "content": "import Config\n\nconfig :boruta_identity, BorutaIdentity.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_identity_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 5\n\nconfig :boruta_auth, BorutaAuth.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_identity_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 5\n"
  },
  {
    "path": "apps/boruta_auth/lib/boruta_auth/application.ex",
    "content": "defmodule BorutaAuth.Application do\n  @moduledoc false\n\n  use Application\n\n  def start(_type, _args) do\n    children = [\n      BorutaAuth.Repo,\n      BorutaAuth.Scheduler\n    ]\n\n    BorutaAuth.LogRotate.rotate()\n    setup_database()\n\n    opts = [strategy: :one_for_one, name: BorutaAuth.Supervisor]\n    Supervisor.start_link(children, opts)\n  end\n  def setup_database do\n    Enum.each([BorutaAuth.Repo], fn repo ->\n      repo.__adapter__.storage_up(repo.config)\n    end)\n\n    :ok\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/lib/boruta_auth/key_pairs/schemas/key_pair.ex",
    "content": "defmodule BorutaAuth.KeyPairs.KeyPair do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias BorutaAuth.Repo\n\n  @type t :: %__MODULE__{\n          id: String.t(),\n          public_key: String.t(),\n          private_key: String.t(),\n          is_default: boolean() | nil\n        }\n\n  @primary_key {:id, :binary_id, autogenerate: true}\n  schema \"key_pairs\" do\n    field(:public_key, :string)\n    field(:private_key, :string)\n    field(:is_default, :boolean)\n\n    timestamps()\n  end\n\n  @spec default!() :: t()\n  def default! do\n    case Repo.get_by(__MODULE__, is_default: true) do\n      nil ->\n        {:ok, key_pair} =\n          change(%__MODULE__{}, %{is_default: true})\n          |> generate_key_pair()\n          |> Repo.insert()\n\n        key_pair\n\n      key_pair ->\n        key_pair\n    end\n  end\n\n  @doc false\n  def changeset(key_pair, attrs) do\n    key_pair\n    |> cast(attrs, [:is_default])\n    |> generate_key_pair()\n    |> validate_required([:public_key, :private_key])\n    |> set_default()\n  end\n\n  def delete_changeset(key_pair) do\n    change(key_pair)\n    |> check_default()\n  end\n\n  def rotate_changeset(key_pair) do\n    private_key = JOSE.JWK.generate_key({:rsa, 2048, 65_537})\n    public_key = JOSE.JWK.to_public(private_key)\n\n    {_type, public_pem} = JOSE.JWK.to_pem(public_key)\n    {_type, private_pem} = JOSE.JWK.to_pem(private_key)\n\n    change(key_pair)\n    |> put_change(:public_key, public_pem)\n    |> put_change(:private_key, private_pem)\n  end\n\n  defp set_default(%Ecto.Changeset{changes: %{is_default: true}} = changeset) do\n    # TODO use a transaction to change default key_pair\n    case change(default!(), %{is_default: false}) |> Repo.update() do\n      {:ok, _key_pair} ->\n        changeset\n\n      {:error, changeset} ->\n        add_error(\n          changeset,\n          :is_default,\n          \"Cannot remove value from the existing default key_pair.\"\n        )\n    end\n  rescue\n    Ecto.NoResultsError -> changeset\n  end\n\n  defp set_default(changeset), do: changeset\n\n  defp generate_key_pair(changeset) do\n    case get_field(changeset, :private_key) do\n      nil ->\n        private_key = JOSE.JWK.generate_key({:rsa, 1024, 65_537})\n        public_key = JOSE.JWK.to_public(private_key)\n\n        {_type, public_pem} = JOSE.JWK.to_pem(public_key)\n        {_type, private_pem} = JOSE.JWK.to_pem(private_key)\n\n        changeset\n        |> put_change(:public_key, public_pem)\n        |> put_change(:private_key, private_pem)\n\n      _private_key ->\n        changeset\n    end\n  end\n\n  defp check_default(changeset) do\n    case get_field(changeset, :is_default) do\n      true ->\n        add_error(\n          changeset,\n          :is_default,\n          \"Cannot delete a default key pair.\"\n        )\n\n      _ ->\n        changeset\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/lib/boruta_auth/key_pairs.ex",
    "content": "defmodule BorutaAuth.KeyPairs do\n  @moduledoc false\n\n  import Ecto.Query\n\n  alias Boruta.Ecto.Client\n  alias Boruta.Oauth.Client.Crypto\n  alias BorutaAuth.KeyPairs.KeyPair\n  alias BorutaAuth.Repo\n\n  def list_key_pairs do\n    Repo.all(KeyPair)\n  end\n\n  def get_key_pair!(id) do\n    Repo.get!(KeyPair, id)\n  end\n\n  def list_jwks do\n    Repo.all(KeyPair)\n    |> Enum.map(&rsa_key/1)\n  end\n\n  def create_key_pair(attrs \\\\ %{}) do\n    KeyPair.changeset(%KeyPair{}, attrs)\n    |> Repo.insert()\n  end\n\n  def update_key_pair(key_pair, attrs \\\\ %{}) do\n    KeyPair.changeset(key_pair, attrs)\n    |> Repo.update()\n  end\n\n  def delete_key_pair(%KeyPair{} = key_pair) do\n    key_pair\n    |> KeyPair.delete_changeset()\n    |> Repo.delete()\n  end\n\n  def rotate(%KeyPair{private_key: private_key} = key_pair) do\n    with {:ok, key_pair} <- KeyPair.rotate_changeset(key_pair) |> Repo.update() do\n      Repo.update_all(\n        from(c in Client, where: c.private_key == ^private_key, select: c.id),\n        set: [\n          public_key: key_pair.public_key,\n          private_key: key_pair.private_key\n        ]\n      )\n      {:ok, key_pair}\n    end\n  end\n\n  defp rsa_key(%KeyPair{} = key_pair) do\n    {_type, jwk} = key_pair.public_key |> :jose_jwk.from_pem() |> :jose_jwk.to_map()\n\n    Map.put(jwk, \"kid\", Crypto.kid_from_private_key(key_pair.private_key))\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/lib/boruta_auth/log_rotate.ex",
    "content": "defmodule BorutaAuth.LogRotate do\n  @moduledoc false\n\n  def rotate do\n    today = Date.utc_today()\n    max_retention_days = Application.get_env(:boruta_auth, BorutaAuth.LogRotate)[:max_retention_days]\n    log_dates = log_dates(\n      %{today|month: 1, day: 1},\n      Date.add(today, -1 * max_retention_days)\n    )\n\n    Enum.map([:request, :business], fn type ->\n      Enum.map([:boruta_web, :boruta_identity, :boruta_admin, :boruta_gateway], fn application ->\n        _files_deleted? = Enum.map(log_dates, &path(application, type, &1))\n        |> Enum.filter(&File.exists?/1)\n        |> Enum.map(&File.rm/1)\n\n        Logger.configure_backend({LoggerFileBackend, :\"#{application}_#{type}_logger\"},\n          path: path(application, type, Date.utc_today())\n        )\n      end)\n    end)\n  end\n\n  @spec path(application :: atom(), type :: atom(), date :: Date.t()) :: path :: String.t()\n  def path(application, type, date) do\n    \"./log/#{Date.to_string(date)}_#{application}_#{type}.log\"\n  end\n\n  defp log_dates(start_date, end_date) do\n    if Date.compare(start_date, end_date) == :gt do\n      []\n    else\n      [start_date | log_dates(Date.add(start_date, 1), end_date)]\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/lib/boruta_auth/repo.ex",
    "content": "defmodule BorutaAuth.Repo do\n  use Ecto.Repo,\n    otp_app: :boruta_auth,\n    adapter: Ecto.Adapters.Postgres\nend\n"
  },
  {
    "path": "apps/boruta_auth/lib/boruta_auth/scheduler.ex",
    "content": "defmodule BorutaAuth.Scheduler do\n  @moduledoc false\n\n  use Quantum, otp_app: :boruta_auth\nend\n"
  },
  {
    "path": "apps/boruta_auth/lib/boruta_auth.ex",
    "content": "defmodule BorutaAuth do\n  @moduledoc \"\"\"\n  Documentation for `BorutaAuth`.\n  \"\"\"\n\n  @doc \"\"\"\n  Hello world.\n\n  ## Examples\n\n      iex> BorutaAuth.hello()\n      :world\n\n  \"\"\"\n  def hello do\n    :world\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/mix.exs",
    "content": "defmodule BorutaAuth.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :boruta_auth,\n      version: \"0.1.0\",\n      build_path: \"../../_build\",\n      config_path: \"../../config/config.exs\",\n      deps_path: \"../../deps\",\n      lockfile: \"../../mix.lock\",\n      elixir: \"~> 1.12\",\n      start_permanent: Mix.env() == :prod,\n      deps: deps()\n    ]\n  end\n\n  def application do\n    [\n      mod: {BorutaAuth.Application, []},\n      extra_applications: [:logger]\n    ]\n  end\n\n  defp deps do\n    [\n      {:boruta, git: \"https://github.com/malach-it/boruta_auth.git\", branch: \"provider-policies-registration\"},\n      {:logger_file_backend, \"~> 0.0.13\"},\n      {:quantum, \"~> 3.0\"}\n    ]\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/boruta.seeds.exs",
    "content": "BorutaAuth.Repo.insert(\n  %Boruta.Ecto.Scope{\n    name: \"openid\",\n    label: \"OpenID Connect capabilities\",\n    public: true\n  },\n  on_conflict: :nothing\n)\n\nBorutaAuth.Repo.insert(\n  %Boruta.Ecto.Scope{\n    name: \"email\",\n    label: \"Email\",\n    public: true\n  },\n  on_conflict: :nothing\n)\n\nBorutaAuth.Repo.insert(\n  %Boruta.Ecto.Scope{\n    name: \"profile\",\n    label: \"Profile\",\n    public: true\n  },\n  on_conflict: :nothing\n)\n\nBorutaAuth.Repo.insert(\n  %Boruta.Ecto.Scope{\n    name: \"scopes:manage:all\",\n    label: \"Manage all scopes\"\n  },\n  on_conflict: :nothing\n)\n\nBorutaAuth.Repo.insert(\n  %Boruta.Ecto.Scope{\n    name: \"roles:manage:all\",\n    label: \"Manage all roles\"\n  },\n  on_conflict: :nothing\n)\n\nBorutaAuth.Repo.insert(\n  %Boruta.Ecto.Scope{\n    name: \"clients:manage:all\",\n    label: \"Manage all clients\"\n  },\n  on_conflict: :nothing\n)\n\nBorutaAuth.Repo.insert(\n  %Boruta.Ecto.Scope{\n    name: \"upstreams:manage:all\",\n    label: \"Manage all upstreams\"\n  },\n  on_conflict: :nothing\n)\n\nBorutaAuth.Repo.insert(\n  %Boruta.Ecto.Scope{\n    name: \"users:manage:all\",\n    label: \"Manage all users\"\n  },\n  on_conflict: :nothing\n)\n\nBorutaAuth.Repo.insert(\n  %Boruta.Ecto.Scope{\n    name: \"identity-providers:manage:all\",\n    label: \"Manage all identity providers\"\n  },\n  on_conflict: :nothing\n)\n\nBorutaAuth.Repo.insert(\n  %Boruta.Ecto.Scope{\n    name: \"configuration:manage:all\",\n    label: \"Manage all configuration\"\n  },\n  on_conflict: :nothing\n)\n\nBorutaAuth.Repo.insert(\n  %Boruta.Ecto.Scope{\n    name: \"logs:read:all\",\n    label: \"Read all logs\"\n  },\n  on_conflict: :nothing\n)\n\nclient_id = System.get_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", \"6a2f41a3-c54c-fce8-32d2-0324e1c32e20\")\n\nclient =\n  case Boruta.Ecto.Admin.create_client(%{\n         name: \"Boruta administration panel\",\n         secret: System.get_env(\"BORUTA_ADMIN_OAUTH_CLIENT_SECRET\"),\n         id: client_id,\n         redirect_uris: [\n           \"#{System.get_env(\"BORUTA_ADMIN_BASE_URL\", \"http://localhost:4001\")}/oauth-callback\"\n         ],\n         access_token_ttl: 3600,\n         authorization_code_ttl: 60,\n         public_revoke: true\n       }) do\n    {:ok, client} -> client\n    {:error, _error} -> Boruta.Ecto.Admin.get_client!(client_id)\n  end\n\nbackend = BorutaIdentity.IdentityProviders.Backend.default!()\n\nBorutaIdentity.IdentityProviders.create_identity_provider(%{\n  name: \"Default\",\n  registrable: true,\n  backend_id: backend.id\n})\n\nidentity_provider =\n  case BorutaIdentity.IdentityProviders.create_identity_provider(%{\n         name: \"Boruta administration interface\",\n         registrable: false,\n         backend_id: backend.id\n       }) do\n    {:ok, identity_provider} ->\n      identity_provider\n\n    {:error, _error} ->\n      BorutaIdentity.IdentityProviders.list_identity_providers()\n      |> Enum.find(fn %{name: name} -> name == \"Boruta administration interface\" end)\n  end\n\nBorutaIdentity.IdentityProviders.upsert_client_identity_provider(client.id, identity_provider.id)\n\nemail = System.get_env(\"BORUTA_ADMIN_EMAIL\")\n\nuser =\n  case BorutaIdentity.Accounts.Internal.User.registration_changeset(\n         %BorutaIdentity.Accounts.Internal.User{},\n         %{\n           email: email,\n           password: System.get_env(\"BORUTA_ADMIN_PASSWORD\"),\n           password_confirmation: System.get_env(\"BORUTA_ADMIN_PASSWORD\"),\n           confirmed_at: DateTime.utc_now()\n         },\n         %{backend: backend}\n       )\n       |> BorutaIdentity.Repo.insert() do\n    {:ok, user} ->\n      user = BorutaIdentity.Accounts.Internal.domain_user!(user, backend)\n\n      Boruta.Ecto.Admin.get_scopes_by_names([\n        \"users:manage:all\",\n        \"clients:manage:all\",\n        \"scopes:manage:all\",\n        \"roles:manage:all\",\n        \"upstreams:manage:all\",\n        \"identity-providers:manage:all\",\n        \"configuration:manage:all\",\n        \"logs:read:all\"\n      ])\n      |> Enum.map(fn %{id: scope_id} ->\n        %BorutaIdentity.Accounts.UserAuthorizedScope{\n          scope_id: scope_id,\n          user_id: user.id\n        }\n        |> Ecto.Changeset.change()\n        |> BorutaIdentity.Repo.insert(on_conflict: :nothing)\n      end)\n\n    {:error, _error} ->\n      nil\n  end\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20201129024828_create_boruta.exs",
    "content": "defmodule BorutaWeb.Repo.Migrations.CreateBoruta do\n  use Ecto.Migration\n\n  def change do\n    create table(:clients, primary_key: false) do\n      add(:id, :uuid, primary_key: true)\n      add(:name, :string)\n      add(:secret, :string)\n      add(:redirect_uris, {:array, :string})\n      add(:scope, :string)\n      add(:authorize_scope, :boolean, default: false)\n      add(:supported_grant_types, {:array, :string})\n      add(:authorization_code_ttl, :integer, null: false)\n      add(:access_token_ttl, :integer, null: false)\n      add(:pkce, :boolean, default: false)\n\n      timestamps()\n    end\n\n    create table(:tokens, primary_key: false) do\n      add(:id, :uuid, primary_key: true)\n      add(:type, :string)\n      add(:value, :string)\n      add(:refresh_token, :string)\n      add(:expires_at, :integer)\n      add(:redirect_uri, :string)\n      add(:state, :string)\n      add(:scope, :string)\n      add(:revoked_at, :utc_datetime)\n      add(:code_challenge_hash, :string)\n      add(:code_challenge_method, :string)\n\n      add(:client_id, references(:clients, type: :uuid, on_delete: :nilify_all))\n      add(:sub, :string)\n\n      timestamps()\n    end\n\n    create table(:scopes, primary_key: false) do\n      add :id, :binary_id, primary_key: true\n      add :name, :string\n      add :public, :boolean, default: false, null: false\n\n      timestamps()\n    end\n\n    create table(:clients_scopes) do\n      add(:client_id, references(:clients, type: :uuid, on_delete: :delete_all))\n      add(:scope_id, references(:scopes, type: :uuid, on_delete: :delete_all))\n    end\n\n    create unique_index(:clients, [:id, :secret])\n    create index(\"tokens\", [:value])\n    create unique_index(\"tokens\", [:client_id, :value])\n    create unique_index(\"tokens\", [:client_id, :refresh_token])\n    create unique_index(\"scopes\", [:name])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20210114202055_usec_timestamps.exs",
    "content": "defmodule Boruta.Repo.Migrations.UsecTimestamps do\n  use Ecto.Migration\n\n  def change do\n    alter table(:tokens) do\n      modify :inserted_at, :utc_datetime_usec\n      modify :updated_at, :utc_datetime_usec\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20210202095024_add_key_pair_to_clients.exs",
    "content": "defmodule Boruta.Repo.Migrations.AddKeyPairToClients do\n  use Ecto.Migration\n\n  def change do\n    alter table(:clients) do\n      add :public_key, :text, null: false\n      add :private_key, :text, null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20210301123331_add_label_to_scopes.exs",
    "content": "defmodule Boruta.Repo.Migrations.AddLabelToScopes do\n  use Ecto.Migration\n\n  def change do\n    alter table(:scopes) do\n      add :label, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20210514211510_add_default_redirect_uris_to_clients.exs",
    "content": "defmodule BorutaWeb.Repo.Migrations.AddDefaultRedirectUrisToClients do\n  use Ecto.Migration\n\n  import Ecto.Query\n\n  alias Boruta.Ecto.Client\n  alias BorutaWeb.Repo\n\n  def change do\n    alter table(:clients) do\n      modify :redirect_uris, {:array, :string}, null: false, default: [], using: \"array[redirect_uri]\"\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20210919174149_openid_connect.exs",
    "content": "defmodule BorutaWeb.Repo.Migrations.OpenidConnect do\n  use Ecto.Migration\n\n  use Boruta.Migrations.OpenidConnect\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20210919174150_clients_refresh_tokens.exs",
    "content": "defmodule BorutaWeb.Repo.Migrations.ClientsRefreshTokens do\n  use Ecto.Migration\n\n  use Boruta.Migrations.ClientsRefreshTokens\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20211013161324_clients_public_revoke.exs",
    "content": "defmodule BorutaWeb.Repo.Migrations.ClientsPublicRevoke do\n  use Ecto.Migration\n\n  use Boruta.Migrations.ClientsPublicRevoke\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20220113221532_store_previous_token.exs",
    "content": "defmodule BorutaWeb.Repo.Migrations.StorePreviousToken do\n  use Ecto.Migration\n\n  use Boruta.Migrations.StorePreviousToken\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20220603211852_id_token_signature_alg_configuration.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.IdTokenSignatureAlgConfiguration do\n  use Ecto.Migration\n\n  use Boruta.Migrations.IdTokenSignatureAlgConfiguration\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20220625203958_confidential_clients.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.ConfidentialClients do\n  use Ecto.Migration\n\n  use Boruta.Migrations.ConfidentialClients\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20220824105115_refresh_token_rotation.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.RefreshTokenRotation do\n  use Ecto.Migration\n\n  use Boruta.Migrations.RefreshTokenRotation\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20221025084535_authorization_code_chains.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.AuthorizationCodeChains do\n  use Ecto.Migration\n\n  use Boruta.Migrations.AuthorizationCodeChains\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20221122131429_client_authentication_methods.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.ClientAuthenticationMethods do\n  use Ecto.Migration\n\n  use Boruta.Migrations.ClientAuthenticationMethods\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20221129120152_signed_userinfo_response.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.SignedUserinfoResponse do\n  use Ecto.Migration\n\n  use Boruta.Migrations.SignedUserinfoResponse\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20230506151359_optional_public_key_for_oauth_clients.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.OptionalPublicKeyForOauthClients do\n  use Ecto.Migration\n\n  use Boruta.Migrations.OptionalPublicKeyForOauthClients\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20230514134306_clients_jwks_uri.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.ClientsJwksUri do\n  use Ecto.Migration\n\n  use Boruta.Migrations.ClientsJwksUri\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20230515093131_create_key_pairs.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.CreateKeyPairs do\n  use Ecto.Migration\n\n  def change do\n    create table(:key_pairs, primary_key: false) do\n      add :id, :uuid, primary_key: true\n      add :public_key, :text, null: false\n      add :private_key, :text, null: false\n      add :is_default, :boolean\n\n      timestamps()\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20230515152140_client_id_token_kid.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.ClientIdTokenKid do\n  use Ecto.Migration\n\n  use Boruta.Migrations.ClientIdTokenKid\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20231217091349_add_metadata_to_clients.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.AddMetadataToClients do\n  use Ecto.Migration\n\n  use Boruta.Migrations.AddMetadataToClients\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20231217144905_oid4vci_implementation.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.Oid4vciImplementation do\n  use Ecto.Migration\n\n  use Boruta.Migrations.Oid4vciImplementation\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20240127081327_siopv2_implementation.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.Siopv2Implementation do\n  use Ecto.Migration\n\n  use Boruta.Migrations.Siopv2Implementation\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20240321101558_dpop_implementation.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.DpopImplementation do\n  use Ecto.Migration\n\n  use Boruta.Migrations.DpopImplementation\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20240417052138_par_implementation.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.ParImplementation do\n  use Ecto.Migration\n\n  use Boruta.Migrations.ParImplementation\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20240506083712_c_nonce_implementation.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.CNonceImplementation do\n  use Ecto.Migration\n\n  use Boruta.Migrations.CNonceImplementation\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20240812111902_defered_credentials.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.DeferedCredentials do\n  use Ecto.Migration\n\n  use Boruta.Migrations.DeferedCredentials\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20240824191208_clients_did.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.ClientsDid do\n  use Ecto.Migration\n\n  use Boruta.Migrations.ClientsDid\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20240908082918_verifiable_presentation_definitions.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.VerifiablePresentationDefinitions do\n  use Ecto.Migration\n\n  use Boruta.Migrations.VerifiablePresentationDefinitions\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20240914084657_clients_response_mode.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.ClientsResponseMode do\n  use Ecto.Migration\n\n  use Boruta.Migrations.ClientsResponseMode\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20241021132955_clients_key_pair_types.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.ClientsKeyPairTypes do\n  use Ecto.Migration\n\n  use Boruta.Migrations.ClientsKeyPairTypes\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20241209110846_tokens_tx_code.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.TokensTxCode do\n  use Ecto.Migration\n\n  use Boruta.Migrations.TokensTxCode\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20241220104923_clients_signatures_adapters.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.ClientsSignaturesAdapters do\n  use Ecto.Migration\n\n  use Boruta.Migrations.ClientsSignaturesAdapters\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20250315084213_fix_oauth_clients_did.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.FixOauthClientsDid do\n  use Ecto.Migration\n\n  use Boruta.Migrations.FixOauthClientsDid\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20250413070457_agent_credentials.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.AgentCredentials do\n  use Ecto.Migration\n\n  use Boruta.Migrations.AgentCredentials\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20250524160749_public_client_id.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.PublicClientId do\n  use Ecto.Migration\n\n  use Boruta.Migrations.PublicClientId\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20260324152715_codes_response_type.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.CodesResponseType do\n  use Ecto.Migration\n\n  use Boruta.Migrations.CodesResponseType\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20260324152716_code_metadata_policy.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.CodeMetadataPolicy do\n  use Ecto.Migration\n\n  use Boruta.Migrations.CodeMetadataPolicy\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20260330112657_siopv2_encryption.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.Siopv2Encryption do\n  use Ecto.Migration\n\n  use Boruta.Migrations.Siopv2Encryption\nend\n"
  },
  {
    "path": "apps/boruta_auth/priv/repo/migrations/20260428121802_requested_scope.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.RequestedScope do\n  use Ecto.Migration\n\n  use Boruta.Migrations.RequestedScope\nend\n"
  },
  {
    "path": "apps/boruta_auth/test/boruta_auth/key_pairs_test.exs",
    "content": "defmodule BorutaAuth.KeyPairsTest do\n  use ExUnit.Case\n\n  @tag :skip\n  test \"list_key_pairs/0\"\n\n  @tag :skip\n  test \"list_jwks/0\"\n\n  @tag :skip\n  test \"get_key_pair!/1\"\n\n  @tag :skip\n  test \"create_key_pair/1\"\n\n  @tag :skip\n  test \"update_key_pair/2\"\n\n  @tag :skip\n  test \"rotate/1\"\n\n  @tag :skip\n  test \"delete_key_pair/0\"\nend\n"
  },
  {
    "path": "apps/boruta_auth/test/test_helper.exs",
    "content": "ExUnit.start()\n"
  },
  {
    "path": "apps/boruta_gateway/.formatter.exs",
    "content": "[\n  import_deps: [:ecto],\n  inputs: [\"*.{ex,exs}\", \"priv/*/seeds.exs\", \"{config,lib,test}/**/*.{ex,exs}\"],\n  subdirectories: [\"priv/*/migrations\"]\n]\n"
  },
  {
    "path": "apps/boruta_gateway/.gitignore",
    "content": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up here.\n/cover/\n\n# The directory Mix downloads your dependencies sources to.\n/deps/\n\n# Where 3rd-party dependencies like ExDoc output generated docs.\n/doc/\n\n# Ignore .fetch files in case you like to edit your project deps locally.\n/.fetch\n\n# If the VM crashes, it generates a dump, let's ignore it too.\nerl_crash.dump\n\n# Also ignore archive artifacts (built via \"mix archive.build\").\n*.ez\n\n# Ignore package tarball (built via \"mix hex.build\").\nboruta_gateway-*.tar\n"
  },
  {
    "path": "apps/boruta_gateway/config/config.exs",
    "content": "import Config\n\nconfig :boruta_gateway,\n  ecto_repos: [BorutaGateway.Repo, BorutaAuth.Repo]\n\nimport_config \"#{Mix.env()}.exs\"\n"
  },
  {
    "path": "apps/boruta_gateway/config/dev.exs",
    "content": "import Config\n\nconfig :boruta_gateway, BorutaGateway.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 5\n\nconfig :boruta_gateway,\n  server: true,\n  sidecar_server: true,\n  port: System.get_env(\"BORUTA_GATEWAY_PORT\", \"5000\") |> String.to_integer(),\n  sidecar_port: System.get_env(\"BORUTA_GATEWAY_SIDECAR_PORT\", \"5001\") |> String.to_integer()\n"
  },
  {
    "path": "apps/boruta_gateway/config/prod.exs",
    "content": "import Config\n"
  },
  {
    "path": "apps/boruta_gateway/config/test.exs",
    "content": "import Config\n\nconfig :boruta_gateway, BorutaGateway.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_gateway_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool: Ecto.Adapters.SQL.Sandbox\n\nconfig :boruta_auth, BorutaAuth.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_auth_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool: Ecto.Adapters.SQL.Sandbox\n\nconfig :boruta_gateway,\n  server: true,\n  sidecar_server: true,\n  port: 7777,\n  sidecar_port: 7778\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/application.ex",
    "content": "defmodule BorutaGateway.Application do\n  # See https://hexdocs.pm/elixir/Application.html\n  # for more information on OTP Applications\n  @moduledoc false\n\n  use Application\n\n  alias BorutaGateway.Logger\n  alias BorutaGateway.Upstreams\n  alias BorutaGateway.Upstreams.ClientSupervisor\n\n  def start(_type, _args) do\n    children = [\n      BorutaGateway.Repo,\n      %{\n        id: Upstreams.Store,\n        start: {Upstreams.Store, :start_link, []}\n      },\n      {ClientSupervisor, strategy: :one_for_one}\n    ]\n\n    Logger.start()\n\n    children =\n      case Application.get_env(:boruta_gateway, :server) do\n        true ->\n          [\n            %{\n              start:\n                {BorutaGateway.Server, :start_link,\n                 [\n                   [\n                     port: Application.fetch_env!(:boruta_gateway, :port),\n                     router: BorutaGateway.Router\n                   ]\n                 ]},\n              id: :server\n            }\n            | children\n          ]\n\n        _ ->\n          children\n      end\n\n    children =\n      case Application.get_env(:boruta_gateway, :sidecar_server) do\n        true ->\n          [\n            %{\n              start:\n                {BorutaGateway.Server, :start_link,\n                 [\n                   [\n                     port: Application.fetch_env!(:boruta_gateway, :sidecar_port),\n                     router: BorutaGateway.SidecarRouter\n                   ]\n                 ]},\n              id: :sidecar_server\n            }\n            | children\n          ]\n\n        _ ->\n          children\n      end\n\n    setup_database()\n    Supervisor.start_link(children, strategy: :one_for_one, name: BorutaGateway.Supervisor)\n  end\n\n  def setup_database do\n    Enum.each([BorutaGateway.Repo], fn repo ->\n      repo.__adapter__.storage_up(repo.config)\n    end)\n\n    Enum.each([BorutaGateway.Repo], fn repo ->\n      Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))\n    end)\n\n    :ok\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/configuration_loader.ex",
    "content": "defmodule BorutaGateway.ConfigurationLoader do\n  @moduledoc false\n\n  alias BorutaGateway.ConfigurationSchemas.GatewaySchema\n  alias BorutaGateway.Upstreams\n\n  @spec node_name() :: node_name :: String.t()\n  def node_name do\n    case Application.get_env(__MODULE__, :node_name) do\n      nil ->\n        path = Application.get_env(:boruta_gateway, :configuration_path)\n\n        %{\n          \"configuration\" => %{\n            \"node_name\" => node_name\n          }\n        } = YamlElixir.read_from_file!(path)\n\n        Application.put_env(__MODULE__, :node_name, node_name)\n        node_name\n\n      node_name ->\n        node_name\n    end\n  rescue\n    _ ->\n      node_name = Atom.to_string(node())\n      Application.put_env(__MODULE__, :node_name, node_name)\n      node_name\n  end\n\n  @spec from_file!(configuration_file_path :: String.t()) :: :ok\n  def from_file!(path) do\n    %{\"configuration\" => configuration} = YamlElixir.read_from_file!(path)\n\n    load_configuration!(configuration)\n  end\n\n  defp load_configuration!(%{\"gateway\" => gateway_configurations} = configuration) do\n    _created_upstreams =\n      Enum.map(gateway_configurations, fn gateway_configuration ->\n        case ExJsonSchema.Validator.validate(GatewaySchema.gateway(), gateway_configuration) do\n          :ok ->\n            {:ok, _upstream} = Upstreams.create_upstream(gateway_configuration)\n\n            :ok\n\n          {:error, errors} ->\n            raise inspect(errors)\n        end\n      end)\n\n    load_configuration!(Map.delete(configuration, \"gateway\"))\n  end\n\n  defp load_configuration!(%{\"microgateway\" => microgateway_configurations} = configuration) do\n    _created_upstreams =\n      Enum.map(microgateway_configurations, fn microgateway_configuration ->\n        microgateway_configuration =\n          Map.put(\n            microgateway_configuration,\n            \"node_name\",\n            node_name()\n          )\n\n        case ExJsonSchema.Validator.validate(\n               GatewaySchema.microgateway(),\n               microgateway_configuration\n             ) do\n          :ok ->\n            {:ok, _upstream} = Upstreams.create_upstream(microgateway_configuration)\n\n            :ok\n\n          {:error, errors} ->\n            raise inspect(errors)\n        end\n      end)\n\n    load_configuration!(Map.delete(configuration, \"microgateway\"))\n  end\n\n  defp load_configuration!(%{}), do: :ok\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/configuration_schemas/gateway.ex",
    "content": "defmodule BorutaGateway.ConfigurationSchemas.GatewaySchema do\n  @moduledoc false\n\n  alias ExJsonSchema.Schema\n\n  def gateway do\n    %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"authorize\" => %{\"type\" => \"boolean\"},\n        \"error_content_type\" => %{\"type\" => \"string\"},\n        \"forbidden_response\" => %{\"type\" => \"string\"},\n        \"unauthorized_response\" => %{\"type\" => \"string\"},\n        \"forwarded_token_private_key\" => %{\"type\" => \"string\"},\n        \"forwarded_token_public_key\" => %{\"type\" => \"string\"},\n        \"forwarded_token_secret\" => %{\"type\" => \"string\"},\n        \"forwarded_token_signature_alg\" => %{\"type\" => \"string\"},\n        \"scheme\" => %{\"type\" => \"string\", \"pattern\" => \"^(http|https)$\"},\n        \"host\" => %{\"type\" => \"string\"},\n        \"port\" => %{\"type\" => \"number\"},\n        \"uris\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"string\"\n          }\n        },\n        \"strip_uri\" => %{\"type\" => \"boolean\"},\n        \"pool_count\" => %{\"type\" => \"number\"},\n        \"pool_size\" => %{\"type\" => \"number\"},\n        \"max_idle_time\" => %{\"type\" => \"number\"},\n        \"required_scopes\" => %{\n          \"type\" => \"object\",\n          \"patternProperties\" => %{\n            \"(GET|POST|PUT|HEAD|OPTIONS|PATCH|DELETE|\\\\*)\" => %{\n              \"type\" => \"array\",\n              \"items\" => %{\n                \"type\" => \"string\",\n                \"pattern\" => \".+\"\n              },\n              \"minItems\" => 1\n            }\n          },\n          \"additionalProperties\" => false\n        }\n      },\n      \"required\" => [\"scheme\", \"host\", \"port\", \"uris\"]\n    }\n    |> Schema.resolve()\n  end\n\n  def microgateway do\n    %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"node_name\" => %{\"type\" => \"string\"},\n        \"authorize\" => %{\"type\" => \"boolean\"},\n        \"error_content_type\" => %{\"type\" => \"string\"},\n        \"forbidden_response\" => %{\"type\" => \"string\"},\n        \"unauthorized_response\" => %{\"type\" => \"string\"},\n        \"forwarded_token_private_key\" => %{\"type\" => \"string\"},\n        \"forwarded_token_public_key\" => %{\"type\" => \"string\"},\n        \"forwarded_token_secret\" => %{\"type\" => \"string\"},\n        \"forwarded_token_signature_alg\" => %{\"type\" => \"string\"},\n        \"scheme\" => %{\"type\" => \"string\", \"pattern\" => \"^(http|https)$\"},\n        \"host\" => %{\"type\" => \"string\"},\n        \"port\" => %{\"type\" => \"number\"},\n        \"uris\" => %{\n          \"type\" => \"array\",\n          \"items\" => %{\n            \"type\" => \"string\"\n          }\n        },\n        \"strip_uri\" => %{\"type\" => \"boolean\"},\n        \"pool_count\" => %{\"type\" => \"number\"},\n        \"pool_size\" => %{\"type\" => \"number\"},\n        \"max_idle_time\" => %{\"type\" => \"number\"},\n        \"required_scopes\" => %{\n          \"type\" => \"object\",\n          \"patternProperties\" => %{\n            \"(GET|POST|PUT|HEAD|OPTIONS|PATCH|DELETE|\\\\*)\" => %{\n              \"type\" => \"array\",\n              \"items\" => %{\n                \"type\" => \"string\",\n                \"pattern\" => \".+\"\n              },\n              \"minItems\" => 1\n            }\n          },\n          \"additionalProperties\" => false\n        }\n      },\n      \"required\" => [\"node_name\", \"scheme\", \"host\", \"port\", \"uris\"]\n    }\n    |> Schema.resolve()\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/gateway_pipeline.ex",
    "content": "defmodule BorutaGateway.GatewayPipeline do\n  @moduledoc false\n\n  use Plug.Router\n\n  plug(RemoteIp)\n  plug(Plug.RequestId)\n  plug(BorutaGateway.Plug.Metrics)\n  plug(BorutaGateway.Plug.AssignUpstream)\n\n  plug(Plug.Telemetry,\n    event_prefix: [:boruta_gateway, :endpoint]\n  )\n\n  plug(:match)\n  plug(BorutaGateway.Plug.Authorize)\n\n  plug(:dispatch)\n  match(_, to: BorutaGateway.Plug.Handler, init_opts: [])\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/logger.ex",
    "content": "defmodule BorutaGateway.Logger do\n  @moduledoc false\n\n  require Logger\n\n  def start do\n    handlers = [\n      {\n        :boruta_gateway_server,\n        [:boruta_gateway, :endpoint, :stop],\n        &__MODULE__.boruta_gateway_server_handler/4\n      },\n      {\n        :boruta_gateway_requests,\n        [:boruta_gateway, :request, :done],\n        &__MODULE__.boruta_gateway_request_handler/4\n      }\n    ]\n\n    for {handler_id, event_name, fun} <- handlers do\n      :telemetry.attach(handler_id, event_name, fun, :ok)\n    end\n  end\n\n  def boruta_gateway_server_handler(_, %{duration: duration}, %{conn: conn} = metadata, _) do\n    case log_level(metadata[:options][:log], conn) do\n      false ->\n        :ok\n\n      level ->\n        Logger.log(\n          level,\n          fn ->\n            %{method: method, request_path: path, status: status, state: state} = conn\n            status = Integer.to_string(status)\n\n            [\n              \"boruta_gateway\",\n              ?\\s,\n              method,\n              ?\\s,\n              path,\n              \" - \",\n              connection_type(state),\n              ?\\s,\n              status,\n              \" in \",\n              duration(duration)\n            ]\n          end,\n          type: :request\n        )\n    end\n  end\n\n  def boruta_gateway_request_handler(\n        _,\n        _measurements,\n        %{request_time: request_time, conn: conn},\n        _\n      ) do\n    %{method: method, request_path: path, status: status_code} = conn\n    node_name = conn.assigns[:node_name]\n    status_code = Integer.to_string(status_code)\n    remote_ip = :inet.ntoa(conn.remote_ip)\n\n    status = business_status(conn)\n\n    log_line = [\n      \"boruta_gateway\",\n      ?\\s,\n      \"upstream\",\n      ?\\s,\n      \"request\",\n      \" - \",\n      status,\n      log_attribute(\"node_name\", node_name),\n      log_attribute(\"remote_ip\", remote_ip),\n      log_attribute(\"method\", method),\n      log_attribute(\"path\", path),\n      log_attribute(\"status_code\", status_code),\n      log_attribute(\"request_time\", [to_string(request_time), \"µs\"])\n    ]\n\n    log_line =\n      log_line\n      |> put_access_token(conn)\n      |> put_upstream_attributes(conn, request_time)\n      |> put_upstream_error(conn)\n\n    Logger.log(\n      :info,\n      fn -> log_line end,\n      type: :business\n    )\n  end\n\n  defp business_status(conn) do\n    case conn.assigns[:upstream] do\n      nil -> \"failure\"\n      _upstream -> \"success\"\n    end\n  end\n\n  defp put_access_token(log_line, conn) do\n    case conn.assigns[:token] do\n      nil ->\n        log_line\n\n      token ->\n        log_line ++\n          [\n            log_attribute(\"access_token\", token.value)\n          ]\n    end\n  end\n\n  defp put_upstream_attributes(log_line, conn, request_time) do\n    case conn.assigns[:upstream] do\n      nil ->\n        log_line\n\n      upstream ->\n        upstream_time = conn.assigns[:upstream_time]\n\n        log_line ++\n          [\n            log_attribute(\"upstream_scheme\", upstream.scheme),\n            log_attribute(\"upstream_host\", upstream.host),\n            log_attribute(\"upstream_port\", upstream.port),\n            log_attribute(\"upstream_time\", upstream_time && [to_string(upstream_time), \"µs\"]),\n            log_attribute(\n              \"gateway_time\",\n              upstream_time && [to_string(request_time - upstream_time), \"µs\"]\n            )\n          ]\n    end\n  end\n\n  defp put_upstream_error(log_line, conn) do\n    case conn.assigns[:upstream_error] do\n      nil ->\n        log_line\n\n      error ->\n        log_line ++ [log_attribute(\"upstream_error\", ~s[\"#{inspect(error)}\"])]\n    end\n  end\n\n  defp log_attribute(_key, nil), do: \"\"\n  defp log_attribute(key, attribute), do: \" #{key}=#{attribute}\"\n\n  # From Phoenix.Logger\n  defp log_level(nil, _conn), do: :info\n  defp log_level(level, _conn) when is_atom(level), do: level\n\n  defp log_level({mod, fun, args}, conn) when is_atom(mod) and is_atom(fun) and is_list(args) do\n    apply(mod, fun, [conn | args])\n  end\n\n  defp connection_type(:set_chunked), do: \"chunked\"\n  defp connection_type(_), do: \"sent\"\n\n  defp duration(nil), do: [\"0\", \"µs\"]\n\n  defp duration(duration) do\n    duration = System.convert_time_unit(duration, :native, :microsecond)\n\n    if duration > 1000 do\n      [duration |> div(1000) |> Integer.to_string(), \"ms\"]\n    else\n      [Integer.to_string(duration), \"µs\"]\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/microgateway_pipeline.ex",
    "content": "defmodule BorutaGateway.MicrogatewayPipeline do\n  @moduledoc false\n\n  use Plug.Router\n\n  plug(RemoteIp)\n  plug(Plug.RequestId)\n  plug(BorutaGateway.Plug.Metrics)\n  plug(BorutaGateway.Plug.AssignSidecarUpstream)\n\n  plug(Plug.Telemetry,\n    event_prefix: [:boruta_gateway, :endpoint]\n  )\n\n  plug(:match)\n  plug(BorutaGateway.Plug.Authorize)\n\n  plug(:dispatch)\n  match(_, to: BorutaGateway.Plug.Handler, init_opts: [])\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/plugs/assign_sidecar_upstream.ex",
    "content": "defmodule BorutaGateway.Plug.AssignSidecarUpstream do\n  @moduledoc false\n\n  import Plug.Conn\n\n  alias BorutaGateway.ConfigurationLoader\n  alias BorutaGateway.Upstreams\n  alias BorutaGateway.Upstreams.Upstream\n\n  require Logger\n\n  def init(options), do: options\n\n  def call(\n        %Plug.Conn{\n          path_info: path_info\n        } = conn,\n        _options\n      ) do\n    conn = assign(conn, :node_name, ConfigurationLoader.node_name())\n\n    case Upstreams.sidecar_match(path_info) do\n      %Upstream{} = upstream ->\n        assign(conn, :upstream, upstream)\n\n      nil ->\n        conn\n        |> send_resp(404, \"No upstream has been found corresponding to the given request.\")\n        |> halt()\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/plugs/assign_upstream.ex",
    "content": "defmodule BorutaGateway.Plug.AssignUpstream do\n  @moduledoc false\n\n  import Plug.Conn\n\n  alias BorutaGateway.Upstreams\n  alias BorutaGateway.Upstreams.Upstream\n\n  require Logger\n\n  def init(options), do: options\n\n  def call(\n        %Plug.Conn{\n          path_info: path_info\n        } = conn,\n        _options\n      ) do\n    conn = assign(conn, :node_name, \"global\")\n\n    case Upstreams.match(path_info) do\n      %Upstream{} = upstream ->\n        assign(conn, :upstream, upstream)\n\n      nil ->\n        conn\n        |> send_resp(404, \"No upstream has been found corresponding to the given request.\")\n        |> halt()\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/plugs/authorize.ex",
    "content": "defmodule BorutaGateway.Plug.Authorize do\n  @moduledoc false\n\n  import Plug.Conn\n\n  alias Boruta.Oauth.Authorization\n  alias Boruta.Oauth.Scope\n  alias Boruta.Oauth.Token\n  alias BorutaGateway.Upstreams.Upstream\n\n  @default_error_content_type \"application/json\"\n  @default_forbidden_response Jason.encode!(%{\n                                error: \"FORBIDDEN\",\n                                message: \"You are forbidden to access this resource.\"\n                              })\n  @default_unauthorized_response Jason.encode!(%{\n                                   error: \"UNAUTHORIZED\",\n                                   message: \"You are unauthorized to access this resource.\"\n                                 })\n\n  def init(options), do: options\n\n  def call(\n        %Plug.Conn{\n          method: method,\n          assigns: %{\n            upstream: %Upstream{authorize: true, required_scopes: required_scopes} = upstream\n          }\n        } = conn,\n        _options\n      ) do\n    with [authorization_header] <- get_req_header(conn, \"authorization\"),\n         [_header, value] <- Regex.run(~r/[B|b]earer (.+)/, authorization_header),\n         {:ok, %Token{scope: scope} = token} <- Authorization.AccessToken.authorize(value: value),\n         {:ok, _} <- validate_scopes(scope, required_scopes, method) do\n      assign(conn, :token, token)\n    else\n      {:error, \"required scopes are not present.\"} ->\n        conn\n        |> put_resp_content_type(upstream.error_content_type || @default_error_content_type)\n        |> send_resp(:forbidden, upstream.forbidden_response || @default_forbidden_response)\n        |> halt()\n\n      _error ->\n        conn\n        |> put_resp_content_type(upstream.error_content_type || @default_error_content_type)\n        |> send_resp(\n          :unauthorized,\n          upstream.unauthorized_response || @default_unauthorized_response\n        )\n        |> halt()\n    end\n  end\n\n  def call(\n        %Plug.Conn{\n          assigns: %{\n            upstream: %Upstream{authorize: false}\n          }\n        } = conn,\n        _options\n      ),\n      do: conn\n\n  defp validate_scopes(_scope, required_scopes, _method) when required_scopes == %{},\n    do: {:ok, []}\n\n  defp validate_scopes(scope, required_scopes, method) do\n    scopes = Scope.split(scope)\n    default_scopes = Map.get(required_scopes, \"*\", [:not_authorized])\n\n    case Enum.empty?(Map.get(required_scopes, method, default_scopes) -- scopes) do\n      true -> {:ok, scopes}\n      false -> {:error, \"required scopes are not present.\"}\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/plugs/handler.ex",
    "content": "defmodule BorutaGateway.Plug.Handler do\n  @moduledoc false\n\n  import Plug.Conn\n\n  alias BorutaGateway.Upstreams.Client\n\n  require Logger\n\n  def init(options), do: options\n\n  def call(\n        %Plug.Conn{\n          assigns: %{upstream: upstream}\n        } = conn,\n        _options\n      ) do\n    start = System.system_time(:microsecond)\n    case Client.request(upstream, conn) do\n      {:ok, %Finch.Response{status: status, headers: headers, body: body}} ->\n        now = System.system_time(:microsecond)\n        request_time = now - start\n        conn =\n          Enum.reduce(headers, conn, fn\n            {\"connection\", _value}, conn -> conn\n            {\"strict-transport-security\", _value}, conn -> conn\n            {\"host\", _value}, conn -> put_resp_header(conn, \"host\", conn.host)\n            {key, value}, conn -> put_resp_header(conn, String.downcase(key), value)\n          end)\n\n        conn\n        |> assign(:upstream_time, request_time)\n        |> send_resp(status, body)\n        |> halt()\n\n      {:error, error} ->\n        now = System.system_time(:microsecond)\n        request_time = now - start\n\n        conn\n        |> assign(:upstream_time, request_time)\n        |> assign(:upstream_error, error)\n        |> send_resp(500, inspect(error))\n        |> halt()\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/plugs/metrics.ex",
    "content": "defmodule BorutaGateway.Plug.Metrics do\n  @moduledoc false\n\n  @behaviour Plug\n\n  import Plug.Conn\n\n  def init(_options), do: []\n\n  def call(\n        %Plug.Conn{} = conn,\n        _options\n      ) do\n    start = System.system_time(:microsecond)\n\n    register_before_send(conn, fn conn ->\n      now = System.system_time(:microsecond)\n      request_time = now - start\n\n      :telemetry.execute(\n        [:boruta_gateway, :request, :done],\n        %{},\n        %{\n          request_time: request_time,\n          conn: conn\n        }\n      )\n\n      conn\n    end)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/release.ex",
    "content": "defmodule BorutaGateway.Release do\n  @moduledoc false\n  @apps [:boruta_auth, :boruta_gateway]\n\n  def migrate do\n    for repo <- repos() do\n      repo.__adapter__.storage_up(repo.config)\n\n      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))\n    end\n  end\n\n  def load_configuration do\n    Application.ensure_all_started(:boruta_gateway)\n\n    configuration_path = Application.get_env(:boruta_gateway, :configuration_path)\n\n    BorutaGateway.ConfigurationLoader.from_file!(configuration_path)\n  end\n\n  def rollback(repo, version) do\n    repo.__adapter__.storage_up(repo.config)\n\n    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))\n  end\n\n  def setup do\n    migrate()\n  end\n\n  defp repos do\n    Enum.flat_map(@apps, fn app ->\n      Application.load(app)\n      Application.fetch_env!(app, :ecto_repos)\n    end)\n    |> Enum.uniq()\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/repo.ex",
    "content": "defmodule BorutaGateway.Repo do\n  @moduledoc false\n  use Ecto.Repo,\n    otp_app: :boruta_gateway,\n    adapter: Ecto.Adapters.Postgres\n\n  def listen(event_name) do\n    with {:ok, pid} <- Postgrex.Notifications.start_link(__MODULE__.config()),\n         {:ok, ref} <- Postgrex.Notifications.listen(pid, event_name) do\n      {:ok, pid, ref}\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/router.ex",
    "content": "defmodule BorutaGateway.Router do\n  @moduledoc false\n\n  use Plug.Router\n\n  plug :match\n  plug :dispatch\n\n  forward \"/\", to: BorutaGateway.GatewayPipeline\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/server.ex",
    "content": "defmodule BorutaGateway.Server do\n  @moduledoc false\n\n  use Supervisor\n\n  def start_link(args) do\n    Supervisor.start_link(__MODULE__, args)\n  end\n\n  @impl Supervisor\n  def init(args) do\n    children = [\n      {Plug.Cowboy, scheme: :http, plug: args[:router], options: [\n        port: args[:port],\n        ip: {0, 0, 0, 0},\n        transport_options: [\n          num_acceptors: 64\n        ]\n      ]}\n    ]\n\n    Supervisor.init(children, strategy: :one_for_one)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/sidecar_router.ex",
    "content": "defmodule BorutaGateway.SidecarRouter do\n  @moduledoc false\n\n  use Plug.Router\n\n  plug :match\n  plug :dispatch\n\n  forward \"/\", to: BorutaGateway.MicrogatewayPipeline\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/upstreams/client/supervisor.ex",
    "content": "defmodule BorutaGateway.Upstreams.ClientSupervisor do\n  @moduledoc false\n\n  use DynamicSupervisor\n\n  alias BorutaGateway.Upstreams.Client\n  alias BorutaGateway.Upstreams.Upstream\n\n  def start_link(_init_arg) do\n    DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)\n  end\n\n  @impl DynamicSupervisor\n  def init([]) do\n    DynamicSupervisor.init(\n      strategy: :one_for_one,\n      extra_arguments: []\n    )\n  end\n\n  def start_child(upstream) do\n    DynamicSupervisor.start_child(__MODULE__, {Client, upstream})\n  end\n\n  @spec client_for_upstream(upstream :: Upstream.t()) :: {:ok, pid()} | {:error, reason :: any()}\n  def client_for_upstream(upstream) do\n    client_name = Client.name(upstream)\n\n    case Process.whereis(client_name) do\n      nil ->\n        start_child(upstream)\n\n      pid ->\n        {:ok, pid}\n    end\n  end\n\n  def kill(nil), do: {:error, :not_started}\n  def kill(client) do\n    Process.exit(client, :shutdown)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/upstreams/client.ex",
    "content": "defmodule BorutaGateway.Upstreams.Client do\n  @moduledoc \"\"\"\n  Upstream scoped HTTP client\n  \"\"\"\n\n  use GenServer\n\n  import Plug.Conn\n\n  alias Boruta.Oauth\n  alias BorutaGateway.Upstreams.Upstream\n\n  defmodule Token do\n    @moduledoc false\n\n    use Joken.Config\n\n    def token_config, do: %{}\n  end\n\n  def child_spec(upstream) do\n    %{\n      id: __MODULE__,\n      start: {__MODULE__, :start_link, [upstream]},\n      type: :worker,\n      restart: :transient\n    }\n  end\n\n  def name(%Upstream{id: upstream_id}) when is_binary(upstream_id) do\n    \"gateway_client_#{upstream_id}\" |> String.replace(\"-\", \"\") |> String.to_atom()\n  end\n\n  def finch_name(%Upstream{id: upstream_id}) when is_binary(upstream_id) do\n    \"finch_gateway_client_#{upstream_id}\" |> String.replace(\"-\", \"\") |> String.to_atom()\n  end\n\n  def start_link(upstream) do\n    GenServer.start_link(__MODULE__, upstream, name: name(upstream))\n  end\n\n  @impl GenServer\n  def init(upstream) do\n    name = finch_name(upstream)\n\n    {:ok, _pid} =\n      Finch.start_link(\n        name: name,\n        pools: %{\n          :default => [size: upstream.pool_size, count: upstream.pool_count]\n        },\n        conn_max_idle_time: upstream.max_idle_time\n      )\n\n    {:ok, %{upstream: upstream, http_client: name}}\n  end\n\n  def upstream(client) do\n    GenServer.call(client, {:get, :upstream})\n  end\n\n  def http_client(client) do\n    GenServer.call(client, {:get, :http_client})\n  end\n\n  def request(%Upstream{http_client: http_client} = upstream, conn) do\n    http_client = http_client(http_client)\n\n    Finch.build(\n      transform_method(conn),\n      transform_url(upstream, conn),\n      transform_headers(upstream, conn),\n      transform_body(conn)\n    )\n    |> Finch.request(http_client)\n  end\n\n  @impl GenServer\n  def handle_call({:get, :upstream}, _from, %{upstream: upstream} = state) do\n    {:reply, upstream, state}\n  end\n\n  def handle_call({:get, :http_client}, _from, %{http_client: http_client} = state) do\n    {:reply, http_client, state}\n  end\n\n  defp transform_method(%Plug.Conn{method: method}) do\n    method |> String.downcase() |> String.to_atom()\n  end\n\n  defp transform_headers(\n         upstream,\n         %Plug.Conn{req_headers: req_headers} = conn\n       ) do\n    token = conn.assigns[:token] || %Oauth.Token{type: \"access_token\"}\n\n    payload = %{\n      \"scope\" => token.scope,\n      \"sub\" => token.sub,\n      \"value\" => token.value,\n      \"exp\" => token.expires_at,\n      \"client_id\" => token.client && token.client.id,\n      \"iat\" => token.inserted_at && DateTime.to_unix(token.inserted_at)\n    }\n\n    token =\n      with %Joken.Signer{} = signer <- signer(upstream),\n           {:ok, token, _payload} <- Token.encode_and_sign(payload, signer) do\n        token\n      else\n        _ -> nil\n      end\n\n    req_headers\n    |> Enum.reject(fn\n      {\"x-forwarded-authorization\", _value} -> true\n      {\"connection\", _value} -> true\n      {\"content-length\", _value} -> true\n      {\"expect\", _value} -> true\n      {\"host\", _value} -> true\n      {\"keep-alive\", _value} -> true\n      {\"transfer-encoding\", _value} -> true\n      {\"upgrade\", _value} -> true\n      _rest -> false\n    end)\n    |> List.insert_at(0, {\"x-forwarded-authorization\", \"bearer #{token}\"})\n  end\n\n  def signer(\n         %Upstream{\n           forwarded_token_signature_alg: signature_alg,\n           forwarded_token_secret: secret,\n           forwarded_token_private_key: private_key\n         } = upstream\n       ) do\n    case signature_alg && signature_type(upstream) do\n      :symmetric ->\n        Joken.Signer.create(signature_alg, secret)\n\n      :asymmetric ->\n        Joken.Signer.create(signature_alg, %{\"pem\" => private_key})\n\n      nil ->\n        nil\n    end\n  end\n\n  defp signature_type(%Upstream{forwarded_token_signature_alg: signature_alg}) do\n    case signature_alg && String.match?(signature_alg, ~r/HS/) do\n      true -> :symmetric\n      false -> :asymmetric\n      nil -> nil\n    end\n  end\n\n  defp transform_body(conn) do\n    case read_body(conn) do\n      {:ok, body, _conn} -> body\n      _ -> \"\"\n    end\n  end\n\n  defp transform_url(\n         %Upstream{scheme: scheme, host: host, port: port, uris: uris, strip_uri: strip_uri},\n         %Plug.Conn{request_path: request_path} = conn\n       ) do\n    path =\n      case strip_uri do\n        true ->\n          matching_uri = Enum.find(uris, fn uri -> String.starts_with?(request_path, uri) end)\n          case matching_uri == \"/\" do\n            true -> request_path\n            false -> String.replace_prefix(request_path, matching_uri, \"\")\n          end\n\n        false ->\n          request_path\n      end\n\n    conn = fetch_query_params(conn)\n    query = URI.encode_query(conn.query_params)\n\n    uri = %URI{authority: host, host: host, path: path, port: port, query: query, scheme: scheme}\n    URI.to_string(uri)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/upstreams/store.ex",
    "content": "defmodule BorutaGateway.Upstreams.Store do\n  @moduledoc false\n\n  require Logger\n\n  use GenServer\n\n  alias BorutaGateway.ConfigurationLoader\n  alias BorutaGateway.Repo\n  alias BorutaGateway.Upstreams.ClientSupervisor\n  alias BorutaGateway.Upstreams.Upstream\n\n  def start_link do\n    GenServer.start_link(__MODULE__, [], name: __MODULE__)\n  end\n\n  @impl GenServer\n  def init(_args) do\n    subscribe()\n    hydrate()\n    {:ok, %{hydrated: false, upstreams: %{}, listener: nil}}\n  end\n\n  @impl GenServer\n  def terminate(reason, state) do\n    Logger.error(inspect(reason))\n\n    listener = state[:listener]\n\n    if listener do\n      Process.exit(listener, :normal)\n    end\n\n    :normal\n  end\n\n  def hydrate do\n    GenServer.cast(__MODULE__, :hydrate)\n  end\n\n  def subscribe do\n    GenServer.cast(__MODULE__, :subscribe)\n  end\n\n  @spec match(path_info :: list(String.t())) :: upstream :: Upstream.t() | nil\n  def match(path_info) do\n    GenServer.call(__MODULE__, {:match, path_info})\n  end\n\n  @spec sidecar_match(path_info :: list(String.t())) :: upstream :: Upstream.t() | nil\n  def sidecar_match(path_info) do\n    GenServer.call(__MODULE__, {:sidecar_match, path_info})\n  end\n\n  def all do\n    GenServer.call(__MODULE__, :all)\n  end\n\n  @impl GenServer\n  def handle_call(:all, _from, %{upstreams: upstreams} = state) do\n    {:reply, upstreams, state}\n  end\n\n  def handle_call({:match, path_info}, _from, %{upstreams: upstreams} = state) do\n    upstream =\n      with {_prefix_info, upstream} <-\n             Enum.find(upstreams[\"global\"] || [], fn {prefix_info, _upstream} ->\n               path_info = Enum.take(path_info, length(prefix_info))\n\n               Enum.empty?(prefix_info -- path_info)\n             end) do\n        upstream\n      end\n\n    {:reply, upstream, state}\n  end\n\n  def handle_call({:sidecar_match, path_info}, _from, %{upstreams: upstreams} = state) do\n    node_name = ConfigurationLoader.node_name()\n\n    upstream =\n      with {_prefix_info, upstream} <-\n             Enum.find(upstreams[node_name] || [], fn {prefix_info, _upstream} ->\n               path_info = Enum.take(path_info, length(prefix_info))\n\n               Enum.empty?(prefix_info -- path_info)\n             end) do\n        upstream\n      end\n\n    {:reply, upstream, state}\n  end\n\n  @impl GenServer\n  def handle_cast(:hydrate, state) do\n    upstreams =\n      Repo.all(Upstream)\n      |> Enum.map(fn upstream ->\n        Upstream.with_http_client(upstream)\n      end)\n      |> structure()\n\n    {:noreply, %{state | hydrated: true, upstreams: upstreams}}\n  rescue\n    error ->\n      Logger.error(inspect(error))\n      {:stop, error, state}\n  end\n\n  @impl GenServer\n  def handle_cast(:subscribe, state) do\n    case Repo.listen(\"upstreams_changed\") do\n      {:ok, pid, _ref} -> {:noreply, %{state | listener: pid}}\n      error -> {:stop, error, state}\n    end\n  end\n\n  @impl GenServer\n  def handle_info(\n        {:notification, _pid, _ref, \"upstreams_changed\", payload},\n        %{upstreams: upstreams} = state\n      ) do\n    upstreams =\n      Enum.reduce(upstreams, [], fn {_node_name, upstreams}, acc -> acc ++ (upstreams || []) end)\n      |> update_upstreams(Jason.decode!(payload))\n\n    state = %{state | upstreams: upstreams}\n\n    {:noreply, state}\n  end\n\n  defp update_upstreams(upstreams, %{\"operation\" => \"INSERT\", \"record\" => record}) do\n    new =\n      struct(\n        Upstream,\n        Enum.map(record, fn {key, value} -> {String.to_atom(key), value} end)\n      )\n      |> Upstream.with_http_client()\n\n    upstreams\n    |> Enum.map(fn {_uri, upstream} -> upstream end)\n    |> List.insert_at(0, new)\n    |> structure()\n  end\n\n  defp update_upstreams(upstreams, %{\"operation\" => \"UPDATE\", \"record\" => record}) do\n    updated =\n      struct(\n        Upstream,\n        Enum.map(record, fn {key, value} -> {String.to_atom(key), value} end)\n      )\n\n    updated_id = updated.id\n\n    upstreams\n    |> Enum.map(fn {_uri, upstream} -> upstream end)\n    |> Enum.map(fn\n      %{id: ^updated_id, http_client: http_client} ->\n        Upstream.with_http_client(%{updated | http_client: http_client})\n\n      upstream ->\n        upstream\n    end)\n    |> structure()\n  end\n\n  defp update_upstreams(upstreams, %{\"operation\" => \"DELETE\", \"record\" => %{\"id\" => id}}) do\n    upstreams\n    |> Enum.map(fn {_uri, upstream} -> upstream end)\n    |> Enum.reject(fn\n      %{id: ^id, http_client: http_client} ->\n        # TODO manage failure\n        true = ClientSupervisor.kill(http_client)\n        true\n\n      _ ->\n        false\n    end)\n    |> structure()\n  end\n\n  defp structure(upstreams) do\n    Enum.reduce(upstreams, [], fn upstream, acc ->\n      (acc ++\n         Enum.map(upstream.uris, fn prefix ->\n           prefix_info = String.split(prefix, \"/\", trim: true)\n\n           {prefix_info, upstream}\n         end))\n      |> Enum.sort_by(fn {path_info, _upstream} -> length(path_info) end, :desc)\n    end)\n    |> Enum.group_by(fn {_path_info, %Upstream{node_name: node_name}} -> node_name end)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/upstreams/upstream.ex",
    "content": "defmodule BorutaGateway.Upstreams.Upstream do\n  @moduledoc false\n\n  @required_scopes_schema %{\n    \"type\" => \"object\",\n    \"patternProperties\" => %{\n      \"(GET|POST|PUT|HEAD|OPTIONS|PATCH|DELETE|\\\\*)\" => %{\n        \"type\" => \"array\",\n        \"items\" => %{\n          \"type\" => \"string\",\n          \"pattern\" => \".+\"\n        },\n        \"minItems\" => 1\n      }\n    },\n    \"additionalProperties\" => false\n  }\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  import Boruta.Config,\n    only: [\n      token_generator: 0\n    ]\n\n  alias BorutaGateway.Upstreams.ClientSupervisor\n\n  @type t :: %__MODULE__{\n          node_name: String.t(),\n          scheme: String.t(),\n          host: String.t(),\n          port: integer(),\n          uris: list(String.t()),\n          required_scopes: map(),\n          strip_uri: boolean(),\n          authorize: boolean(),\n          error_content_type: String.t() | nil,\n          forbidden_response: String.t() | nil,\n          unauthorized_response: String.t() | nil,\n          inserted_at: DateTime.t(),\n          updated_at: DateTime.t()\n        }\n\n  @primary_key {:id, :binary_id, autogenerate: true}\n  @foreign_key_type :binary_id\n  schema \"upstreams\" do\n    field(:node_name, :string, default: \"global\")\n    field(:scheme, :string)\n    field(:host, :string)\n    field(:port, :integer)\n    field(:uris, {:array, :string}, default: [])\n    field(:required_scopes, :map, default: %{})\n    field(:strip_uri, :boolean, default: false)\n    field(:authorize, :boolean, default: false)\n    field(:pool_size, :integer, default: 10)\n    field(:pool_count, :integer, default: 1)\n    field(:max_idle_time, :integer, default: 10)\n    field(:error_content_type, :string)\n    field(:forbidden_response, :string)\n    field(:unauthorized_response, :string)\n    field(:forwarded_token_signature_alg, :string)\n    field(:forwarded_token_secret, :string)\n    field(:forwarded_token_public_key, :string)\n    field(:forwarded_token_private_key, :string)\n\n    field(:http_client, :any, virtual: true)\n\n    timestamps()\n  end\n\n  def with_http_client(%__MODULE__{http_client: nil} = upstream) do\n    # TODO manage failure\n    {:ok, http_client} = ClientSupervisor.client_for_upstream(upstream)\n\n    %{upstream | http_client: http_client}\n  end\n\n  def with_http_client(%__MODULE__{http_client: http_client} = upstream)\n      when is_pid(http_client) do\n    ClientSupervisor.kill(http_client)\n    # TODO manage failure\n    {:ok, http_client} =\n      Enum.reduce_while(1..100, http_client, fn _i, http_client ->\n        :timer.sleep(10)\n\n        case Process.alive?(http_client) do\n          true ->\n            {:cont, http_client}\n\n          false ->\n            {:halt, ClientSupervisor.client_for_upstream(upstream)}\n        end\n      end)\n\n    %{upstream | http_client: http_client}\n  end\n\n  @doc false\n  def changeset(upstream, attrs) do\n    upstream\n    |> cast(attrs, [\n      :node_name,\n      :scheme,\n      :host,\n      :port,\n      :uris,\n      :strip_uri,\n      :authorize,\n      :required_scopes,\n      :pool_size,\n      :pool_count,\n      :max_idle_time,\n      :error_content_type,\n      :forbidden_response,\n      :unauthorized_response,\n      :forwarded_token_signature_alg,\n      :forwarded_token_secret\n    ])\n    |> validate_required([:scheme, :host, :port])\n    |> validate_inclusion(:scheme, [\"http\", \"https\"])\n    |> validate_inclusion(:pool_size, 1..100)\n    |> validate_inclusion(:pool_count, 1..10)\n    |> unique_constraint([:node_name, :host, :port, :uris])\n    |> maybe_put_forwarded_token_secret()\n    |> maybe_generate_key_pair()\n    |> validate_uris()\n    |> validate_required_scopes_format()\n  end\n\n  defp validate_uris(\n         %Ecto.Changeset{\n           changes: %{uris: uris}\n         } = changeset\n       ) do\n    case Enum.any?(uris, fn uri -> is_nil(uri) || uri == \"\" end) do\n      true -> add_error(changeset, :uris, \"may not be blank\")\n      false -> changeset\n    end\n  end\n\n  defp validate_uris(changeset), do: changeset\n\n  defp validate_required_scopes_format(\n         %Ecto.Changeset{\n           changes: %{required_scopes: required_scopes}\n         } = changeset\n       ) do\n    case ExJsonSchema.Validator.validate(@required_scopes_schema, required_scopes) do\n      :ok ->\n        changeset\n\n      {:error, errors} ->\n        Enum.reduce(errors, changeset, fn {message, path}, changeset ->\n          add_error(changeset, :required_scopes, \"#{message} at #{path}\")\n        end)\n    end\n  end\n\n  defp validate_required_scopes_format(changeset), do: changeset\n\n  defp maybe_put_forwarded_token_secret(%Ecto.Changeset{data: data, changes: changes} = changeset) do\n    signature_algorithm = get_field(changeset, :forwarded_token_signature_alg)\n\n    if signature_algorithm && String.match?(signature_algorithm, ~r/HS/) do\n      case fetch_field(changeset, :forwarded_token_secret) do\n        {_, nil} ->\n          put_change(\n            changeset,\n            :forwarded_token_secret,\n            token_generator().secret(struct(data, changes))\n          )\n\n        {_, _secret} ->\n          changeset\n\n        :error ->\n          put_change(\n            changeset,\n            :forwarded_token_secret,\n            token_generator().secret(struct(data, changes))\n          )\n      end\n    else\n      changeset\n    end\n  end\n\n  defp maybe_generate_key_pair(changeset) do\n    signature_algorithm = get_field(changeset, :forwarded_token_signature_alg)\n\n    if signature_algorithm && String.match?(signature_algorithm, ~r/RS/) do\n      case fetch_field(changeset, :forwarded_token_private_key) do\n        {_, \"\" <> _private_key} ->\n          changeset\n\n        _ ->\n          private_key = JOSE.JWK.generate_key({:rsa, 1024, 65_537})\n          public_key = JOSE.JWK.to_public(private_key)\n\n          {_type, public_pem} = JOSE.JWK.to_pem(public_key)\n          {_type, private_pem} = JOSE.JWK.to_pem(private_key)\n\n          changeset\n          |> put_change(:forwarded_token_public_key, public_pem)\n          |> put_change(:forwarded_token_private_key, private_pem)\n      end\n    else\n      changeset\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway/upstreams.ex",
    "content": "defmodule BorutaGateway.Upstreams do\n  @moduledoc \"\"\"\n  The Upstreams context.\n  \"\"\"\n\n  import Ecto.Query, warn: false\n  alias BorutaGateway.Repo\n\n  alias BorutaGateway.Upstreams.Store\n  alias BorutaGateway.Upstreams.Upstream\n\n  def match(path) do\n    Store.match(path)\n  end\n\n  def sidecar_match(path) do\n    Store.sidecar_match(path)\n  end\n\n  @doc \"\"\"\n  Returns the list of upstreams.\n\n  ## Examples\n\n      iex> list_upstreams()\n      [%Upstream{}, ...]\n\n  \"\"\"\n  def list_upstreams do\n    Upstream\n    |> Repo.all()\n    |> Enum.group_by(fn %Upstream{node_name: node_name} -> node_name end)\n  end\n\n  @doc \"\"\"\n  Gets a single upstream.\n\n  Raises `Ecto.NoResultsError` if the Upstream does not exist.\n\n  ## Examples\n\n      iex> get_upstream!(123)\n      %Upstream{}\n\n      iex> get_upstream!(456)\n      ** (Ecto.NoResultsError)\n\n  \"\"\"\n  def get_upstream!(id), do: Repo.get!(Upstream, id)\n\n  @doc \"\"\"\n  Creates a upstream.\n\n  ## Examples\n\n      iex> create_upstream(%{field: value})\n      {:ok, %Upstream{}}\n\n      iex> create_upstream(%{field: bad_value})\n      {:error, %Ecto.Changeset{}}\n\n  \"\"\"\n  def create_upstream(attrs \\\\ %{}) do\n    %Upstream{}\n    |> Upstream.changeset(attrs)\n    |> Repo.insert()\n  end\n\n  @doc \"\"\"\n  Updates a upstream.\n\n  ## Examples\n\n      iex> update_upstream(upstream, %{field: new_value})\n      {:ok, %Upstream{}}\n\n      iex> update_upstream(upstream, %{field: bad_value})\n      {:error, %Ecto.Changeset{}}\n\n  \"\"\"\n  def update_upstream(%Upstream{} = upstream, attrs) do\n    upstream\n    |> Upstream.changeset(attrs)\n    |> Repo.update()\n  end\n\n  @doc \"\"\"\n  Deletes a upstream.\n\n  ## Examples\n\n      iex> delete_upstream(upstream)\n      {:ok, %Upstream{}}\n\n      iex> delete_upstream(upstream)\n      {:error, %Ecto.Changeset{}}\n\n  \"\"\"\n  def delete_upstream(%Upstream{} = upstream) do\n    Repo.delete(upstream)\n  end\n\n  @doc \"\"\"\n  Returns an `%Ecto.Changeset{}` for tracking upstream changes.\n\n  ## Examples\n\n      iex> change_upstream(upstream)\n      %Ecto.Changeset{source: %Upstream{}}\n\n  \"\"\"\n  def change_upstream(%Upstream{} = upstream) do\n    Upstream.changeset(upstream, %{})\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/boruta_gateway.ex",
    "content": "defmodule BorutaGateway do\n  @moduledoc false\nend\n"
  },
  {
    "path": "apps/boruta_gateway/lib/mix/tasks/server.ex",
    "content": "defmodule Mix.Tasks.BorutaGateway.Server do\n  @moduledoc false\n\n  use Mix.Task\n\n  def run(_args) do\n    Application.put_env(:boruta_gateway, :server, true, persistent: true)\n\n    Mix.Tasks.Run.run([\"--no-halt\"])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/mix.exs",
    "content": "defmodule BorutaGateway.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :boruta_gateway,\n      version: \"0.1.0\",\n      build_path: \"../../_build\",\n      config_path: \"../../config/config.exs\",\n      deps_path: \"../../deps\",\n      lockfile: \"../../mix.lock\",\n      elixir: \"~> 1.5\",\n      elixirc_paths: elixirc_paths(Mix.env()),\n      start_permanent: Mix.env() == :prod,\n      aliases: aliases(),\n      deps: deps()\n    ]\n  end\n\n  # Configuration for the OTP application.\n  #\n  # Type `mix help compile.app` for more information.\n  def application do\n    [\n      mod: {BorutaGateway.Application, []},\n      extra_applications: [:logger, :runtime_tools, :syntax_tools]\n    ]\n  end\n\n  # Specifies which paths to compile per environment.\n  defp elixirc_paths(:test), do: [\"lib\", \"test/support\"]\n  defp elixirc_paths(_), do: [\"lib\"]\n\n  # Specifies your project dependencies.\n  #\n  # Type `mix help deps` for examples and options.\n  defp deps do\n    [\n      {:boruta_auth, in_umbrella: true},\n      {:ecto_sql, \"~> 3.0\"},\n      {:ex_json_schema, \"~> 0.9\"},\n      {:finch, \"~> 0.10\"},\n      {:jason, \"~> 1.0\"},\n      {:plug_cowboy, \"~> 2.0\"},\n      {:postgrex, \">= 0.0.0\"},\n      {:remote_ip, \"~> 1.1\"},\n      {:telemetry, \"~> 0.4\"},\n      {:yaml_elixir, \"~> 2.9\"}\n    ]\n  end\n\n  # Aliases are shortcuts or tasks specific to the current project.\n  # For example, to create, migrate and run the seeds file at once:\n  #\n  #     $ mix ecto.setup\n  #\n  # See the documentation for `Mix` for more info on aliases.\n  defp aliases do\n    [\n      \"ecto.setup\": [\"ecto.create\", \"ecto.migrate\", \"run priv/repo/seeds.exs\"],\n      \"ecto.reset\": [\"ecto.drop\", \"ecto.setup\"],\n      test: [\"ecto.create --quiet\", \"ecto.migrate\", \"test\"]\n    ]\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20200219201345_create_upstreams.exs",
    "content": "defmodule Boruta.Repo.Migrations.CreateUpstreams do\n  use Ecto.Migration\n\n  def change do\n    create table(:upstreams, primary_key: false) do\n      add :id, :binary_id, primary_key: true\n      add :scheme, :string\n      add :host, :string\n      add :port, :integer\n      add :uris, {:array, :string}, default: []\n      add :strip_uri, :boolean, default: false\n      add :authorize, :boolean, default: false\n      add :required_scopes, {:array, :string}, default: []\n\n      timestamps()\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20200326185929_upstreams_notify.exs",
    "content": "defmodule Boruta.Repo.Migrations.UpstreamsNotify do\n  use Ecto.Migration\n\n  def change do\n    execute \"\"\"\n      CREATE OR REPLACE FUNCTION notify_upstreams_changes()\n      RETURNS trigger AS $$\n      DECLARE\n        rec RECORD;\n      BEGIN\n        CASE TG_OP\n        WHEN 'INSERT', 'UPDATE' THEN\n           rec := NEW;\n        WHEN 'DELETE' THEN\n           rec := OLD;\n        END CASE;\n\n        PERFORM pg_notify(\n          'upstreams_changed',\n          json_build_object(\n            'operation', TG_OP,\n            'record', row_to_json(rec)\n          )::text\n        );\n\n        RETURN NEW;\n      END;\n      $$ LANGUAGE plpgsql;\n    \"\"\"\n\n    execute \"\"\"\n      CREATE TRIGGER upstreams_changed\n      AFTER INSERT OR UPDATE OR DELETE\n      ON upstreams\n      FOR EACH ROW\n      EXECUTE PROCEDURE notify_upstreams_changes()\n    \"\"\"\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20210111144958_change_upstreams_required_scopes_type.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.ChangeUpstreamsRequiredScopesType do\n  use Ecto.Migration\n\n  def change do\n    alter table(:upstreams) do\n      remove :required_scopes\n      add :required_scopes, :jsonb, default: \"{}\"\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20220319220305_add_pool_size_to_upstreams.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.AddPoolSizeToUpstreams do\n  use Ecto.Migration\n\n  def change do\n    alter table(:upstreams) do\n      add(:pool_size, :integer, default: 10)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20220728122802_add_max_idle_time_to_upstreams.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.AddMaxIdleTimeToUpstreams do\n  use Ecto.Migration\n\n  def change do\n    alter table(:upstreams) do\n      add :max_idle_time, :integer, default: 10, null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20220729040405_add_pool_count_to_upstreams.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.AddPoolCountToUpstreams do\n  use Ecto.Migration\n\n  def change do\n    alter table(:upstreams) do\n      add :pool_count, :integer, default: 1, null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20220810082956_add_forbidden_response_to_upstreams.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.AddForbiddenResponseToUpstreams do\n  use Ecto.Migration\n\n  def change do\n    alter table(:upstreams) do\n      add :forbidden_response, :text\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20220810084238_add_error_content_type_to_upstreams.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.AddErrorContentTypeToUpstreams do\n  use Ecto.Migration\n\n  def change do\n    alter table(:upstreams) do\n      add :error_content_type, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20220810084450_add_unauthorized_response_to_upstreams.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.AddUnauthorizedResponseToUpstreams do\n  use Ecto.Migration\n\n  def change do\n    alter table(:upstreams) do\n      add :unauthorized_response, :text\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20221024100810_add_forwarded_token_signature_algorithm_to_upstreams.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.AddForwardedTokenSignatureAlgorithmToUpstreams do\n  use Ecto.Migration\n\n  def change do\n    alter table(:upstreams) do\n      add :forwarded_token_signature_alg, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20221024122642_add_forwarded_token_secret_to_upstreams.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.AddForwardedTokenSecretToUpstreams do\n  use Ecto.Migration\n\n  def change do\n    alter table(:upstreams) do\n      add :forwarded_token_secret, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20221024132312_add_forwarded_token_key_pair_to_upstreams.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.AddForwardedTokenKeyPairToUpstreams do\n  use Ecto.Migration\n\n  def change do\n    alter table(:upstreams) do\n      add :forwarded_token_public_key, :text\n      add :forwarded_token_private_key, :text\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20230413190522_upstreams_unique_indices.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.UpstreamsUniqueIndices do\n  use Ecto.Migration\n\n  def change do\n    create index(:upstreams, [:host, :port, :uris], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20230421135202_add_node_name_to_upstreams.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.AddNodeNameToUpstreams do\n  use Ecto.Migration\n\n  def change do\n    alter table(:upstreams) do\n      add :node_name, :string, default: \"global\"\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/migrations/20230422083455_update_upstreams_unique_constraint.exs",
    "content": "defmodule BorutaGateway.Repo.Migrations.UpdateUpstreamsUniqueConstraint do\n  use Ecto.Migration\n\n  def change do\n    drop index(:upstreams, [:host, :port, :uris], unique: true)\n    create index(:upstreams, [:node_name, :host, :port, :uris], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/priv/repo/seeds.exs",
    "content": ""
  },
  {
    "path": "apps/boruta_gateway/priv/test/configuration_files/authorized.yml",
    "content": "---\nconfiguration:\n  gateway:\n    - scheme: \"http\"\n      host: \"httpbin.patatoid.fr\"\n      port: 80\n      uris: [\"/httpbin\"]\n      strip_uri: true\n      authorize: true\n      required_scopes:\n        GET: [\"test\"]\n"
  },
  {
    "path": "apps/boruta_gateway/priv/test/configuration_files/authorized_introspect.yml",
    "content": "---\nconfiguration:\n  gateway:\n    - scheme: \"http\"\n      host: \"httpbin.patatoid.fr\"\n      port: 80\n      uris: [\"/httpbin\"]\n      strip_uri: true\n      authorize: true\n      required_scopes:\n        GET: [\"test\"]\n      forwarded_token_signature_alg: \"HS256\"\n"
  },
  {
    "path": "apps/boruta_gateway/priv/test/configuration_files/bad_configuration.yml",
    "content": "---\nbad_configuration:\n  gateway:\n    - scheme: \"http\"\n      host: \"httpbin.patatoid.fr\"\n      port: 80\n      uris: [\"/httpbin\"]\n      strip_uri: true\n      authorize: true\n      required_scopes:\n        GET: [\"test\"]\n"
  },
  {
    "path": "apps/boruta_gateway/priv/test/configuration_files/bad_gateway_configuration.yml",
    "content": "---\nversion: \"1.0\"\nconfiguration:\n  gateway:\n    - authorize: true\n      error_content_type: \"test\"\n      forbidden_response: \"test\"\n      unauthorized_response: \"test\"\n      forwarded_token_secret: \"test\"\n      forwarded_token_signature_alg: \"HS384\"\n      max_idle_time: 10\n      pool_count: 1\n      pool_size: 10\n      required_scopes:\n        GET: [\"test\"]\n      strip_uri: true\n"
  },
  {
    "path": "apps/boruta_gateway/priv/test/configuration_files/bad_microgateway_configuration.yml",
    "content": "---\nversion: \"1.0\"\nconfiguration:\n  gateway:\n    - authorize: true\n      error_content_type: \"test\"\n      forbidden_response: \"test\"\n      unauthorized_response: \"test\"\n      forwarded_token_secret: \"test\"\n      forwarded_token_signature_alg: \"HS384\"\n      host: \"httpbin.patatoid.fr\"\n      port: 80\n      uris: [\"/httpbin\"]\n      max_idle_time: 10\n      pool_count: 1\n      pool_size: 10\n      required_scopes:\n        GET: [\"test\"]\n      scheme: \"http\"\n      strip_uri: true\n  microgateway:\n    - authorize: true\n      error_content_type: \"test\"\n      forbidden_response: \"test\"\n      unauthorized_response: \"test\"\n      forwarded_token_secret: \"test\"\n      forwarded_token_signature_alg: \"HS384\"\n      max_idle_time: 10\n      pool_count: 1\n      pool_size: 10\n      required_scopes:\n        GET: [\"test\"]\n      strip_uri: true\n"
  },
  {
    "path": "apps/boruta_gateway/priv/test/configuration_files/forbidden.yml",
    "content": "---\n\nconfiguration:\n  gateway:\n    - scheme: \"http\"\n      host: \"should.not.be.called\"\n      port: 80\n      uris: [\"/forbidden\"]\n      authorize: true\n      required_scopes:\n        GET: [\"required\"]\n      error_content_type: \"text\"\n      forbidden_response: \"boom\"\n"
  },
  {
    "path": "apps/boruta_gateway/priv/test/configuration_files/full_configuration.yml",
    "content": "---\nconfiguration:\n  node_name: \"full-configuration\"\n  gateway:\n    - authorize: true\n      error_content_type: \"test\"\n      forbidden_response: \"test\"\n      unauthorized_response: \"test\"\n      forwarded_token_secret: \"test\"\n      forwarded_token_signature_alg: \"HS384\"\n      host: \"httpbin.patatoid.fr\"\n      port: 80\n      uris: [\"/httpbin\"]\n      max_idle_time: 10\n      pool_count: 1\n      pool_size: 10\n      required_scopes:\n        GET: [\"test\"]\n      scheme: \"http\"\n      strip_uri: true\n  microgateway:\n    - authorize: true\n      error_content_type: \"test\"\n      forbidden_response: \"test\"\n      unauthorized_response: \"test\"\n      forwarded_token_secret: \"test\"\n      forwarded_token_signature_alg: \"HS384\"\n      host: \"httpbin.patatoid.fr\"\n      port: 80\n      uris: [\"/httpbin\"]\n      max_idle_time: 10\n      pool_count: 1\n      pool_size: 10\n      required_scopes:\n        GET: [\"test\"]\n      scheme: \"http\"\n      strip_uri: true\n"
  },
  {
    "path": "apps/boruta_gateway/priv/test/configuration_files/not_found.yml",
    "content": "---\nconfiguration:\n  gateway:\n    - scheme: \"http\"\n      host: \"should.not.be.called\"\n      port: 80\n      uris:\n        - \"/upstream\"\n"
  },
  {
    "path": "apps/boruta_gateway/priv/test/configuration_files/unauthorized.yml",
    "content": "---\nconfiguration:\n  gateway:\n    - scheme: \"http\"\n      host: \"should.not.be.called\"\n      port: 80\n      uris: [\"/unauthorized\"]\n      authorize: true\n      error_content_type: \"text\"\n      unauthorized_response: \"boom\"\n"
  },
  {
    "path": "apps/boruta_gateway/test/boruta_gateway/configuration_loader_test.exs",
    "content": "defmodule BorutaGateway.ConfigurationLoaderTest do\n  use BorutaGateway.DataCase\n\n  alias BorutaGateway.ConfigurationLoader\n  alias BorutaGateway.Repo\n  alias BorutaGateway.Upstreams.Upstream\n\n  test \"returns an error with a bad configuration file\" do\n    assert Repo.all(Upstream) |> Enum.empty?()\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_gateway)\n      |> Path.join(\"/test/configuration_files/bad_configuration.yml\")\n\n    assert_raise MatchError, fn ->\n      ConfigurationLoader.from_file!(configuration_file_path)\n    end\n  end\n\n  test \"returns an error with a bad gateway configuration file\" do\n    assert Repo.all(Upstream) |> Enum.empty?()\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_gateway)\n      |> Path.join(\"/test/configuration_files/bad_gateway_configuration.yml\")\n\n    assert_raise RuntimeError,\n                 ~s|[{\"Required properties scheme, host, port, uris were not present.\", \"#\"}]|,\n                 fn ->\n                   ConfigurationLoader.from_file!(configuration_file_path)\n                 end\n  end\n\n  test \"returns an error with a bad microgateway configuration file\" do\n    assert Repo.all(Upstream) |> Enum.empty?()\n\n    configuration_file_path =\n      :code.priv_dir(:boruta_gateway)\n      |> Path.join(\"/test/configuration_files/bad_microgateway_configuration.yml\")\n\n    assert_raise RuntimeError,\n                 ~s|[{\"Required properties scheme, host, port, uris were not present.\", \"#\"}]|,\n                 fn ->\n                   ConfigurationLoader.from_file!(configuration_file_path)\n                 end\n  end\n\n  test \"loads a file\" do\n    assert Repo.all(Upstream) |> Enum.empty?()\n\n    Application.delete_env(ConfigurationLoader, :node_name)\n    configuration_file_path =\n      :code.priv_dir(:boruta_gateway)\n      |> Path.join(\"/test/configuration_files/full_configuration.yml\")\n\n    ConfigurationLoader.from_file!(configuration_file_path)\n\n    assert [\n             %Upstream{\n               scheme: \"http\",\n               host: \"httpbin.patatoid.fr\",\n               port: 80,\n               uris: [\"/httpbin\"],\n               required_scopes: %{\"GET\" => [\"test\"]},\n               strip_uri: true,\n               authorize: true,\n               pool_size: 10,\n               pool_count: 1,\n               max_idle_time: 10,\n               error_content_type: \"test\",\n               forbidden_response: \"test\",\n               unauthorized_response: \"test\",\n               forwarded_token_signature_alg: \"HS384\",\n               forwarded_token_secret: \"test\",\n               forwarded_token_public_key: nil,\n               forwarded_token_private_key: nil\n             },\n             %Upstream{\n               node_name: \"full-configuration\",\n               scheme: \"http\",\n               host: \"httpbin.patatoid.fr\",\n               port: 80,\n               uris: [\"/httpbin\"],\n               required_scopes: %{\"GET\" => [\"test\"]},\n               strip_uri: true,\n               authorize: true,\n               pool_size: 10,\n               pool_count: 1,\n               max_idle_time: 10,\n               error_content_type: \"test\",\n               forbidden_response: \"test\",\n               unauthorized_response: \"test\",\n               forwarded_token_signature_alg: \"HS384\",\n               forwarded_token_secret: \"test\",\n               forwarded_token_public_key: nil,\n               forwarded_token_private_key: nil,\n             }\n           ] = Repo.all(Upstream)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/test/boruta_gateway/integration/requests_test.exs",
    "content": "defmodule BorutaGateway.RequestsIntegrationTest do\n  use ExUnit.Case\n  use Plug.Test\n  use BorutaGateway.DataCase\n\n  alias Boruta.AccessTokensAdapter\n  alias Boruta.ClientsAdapter\n  alias Boruta.Ecto.Admin\n  alias BorutaGateway.ConfigurationLoader\n  alias BorutaGateway.Repo\n  alias BorutaGateway.RequestsIntegrationTest.HttpClient\n  alias BorutaGateway.Upstreams\n  alias BorutaGateway.Upstreams.Client\n  alias BorutaGateway.Upstreams.Upstream\n  alias Ecto.Adapters.SQL.Sandbox\n\n  setup_all do\n    Finch.start_link(name: HttpClient)\n\n    :ok\n  end\n\n  @tag :skip\n  describe \"requests\" do\n    setup do\n      {:ok, %Boruta.Ecto.Client{id: client_id}} = Admin.create_client(%{})\n\n      {:ok, access_token} =\n        AccessTokensAdapter.create(\n          %{\n            client: ClientsAdapter.get_client(client_id),\n            scope: \"test\"\n          },\n          []\n        )\n\n      {:ok, access_token: access_token}\n    end\n\n    test \"returns a 404 when no upstream found\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          Upstreams.create_upstream(%{\n            scheme: \"http\",\n            host: \"should.not.be.called\",\n            port: 80,\n            uris: [\"/upstream\"]\n          })\n\n          Process.sleep(100)\n\n          request = Finch.build(:get, \"http://localhost:7777/no_upstream\", [], \"\")\n\n          assert {:ok, %Finch.Response{body: body, status: 404}} =\n                   Finch.request(request, HttpClient)\n\n          assert body == \"No upstream has been found corresponding to the given request.\"\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    test \"returns a 404 when no upstream persisted\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          request = Finch.build(:get, \"http://localhost:7777/no_upstream\", [], \"\")\n\n          assert {:ok, %Finch.Response{body: body, status: 404}} =\n                   Finch.request(request, HttpClient)\n\n          assert body == \"No upstream has been found corresponding to the given request.\"\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    test \"returns a 401 when unauthorized\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          {:ok, upstream} =\n            Upstreams.create_upstream(%{\n              scheme: \"http\",\n              host: \"should.not.be.called\",\n              port: 80,\n              uris: [\"/unauthorized\"],\n              authorize: true,\n              error_content_type: \"text\",\n              unauthorized_response: \"boom\"\n            })\n\n          Process.sleep(100)\n\n          request = Finch.build(:get, \"http://localhost:7777/unauthorized\", [], \"\")\n\n          assert {:ok, %Finch.Response{body: body, headers: headers, status: 401}} =\n                   Finch.request(request, HttpClient)\n\n          assert body == upstream.unauthorized_response\n\n          assert Enum.any?(headers, fn\n                   {\"content-type\", content_type} ->\n                     upstream.error_content_type\n                     |> Regex.compile!()\n                     |> Regex.match?(content_type)\n\n                   _ ->\n                     false\n                 end)\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    test \"returns a 403 when forbidden\", %{access_token: access_token} do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          {:ok, upstream} =\n            Upstreams.create_upstream(%{\n              scheme: \"http\",\n              host: \"should.not.be.called\",\n              port: 80,\n              uris: [\"/forbidden\"],\n              authorize: true,\n              required_scopes: %{\"GET\" => [\"required\"]},\n              error_content_type: \"text\",\n              forbidden_response: \"boom\"\n            })\n\n          Process.sleep(100)\n\n          request =\n            Finch.build(\n              :get,\n              \"http://localhost:7777/forbidden\",\n              [{\"authorization\", \"Bearer #{access_token.value}\"}],\n              \"\"\n            )\n\n          assert {:ok, %Finch.Response{body: body, headers: headers, status: 403}} =\n                   Finch.request(request, HttpClient)\n\n          assert body == upstream.forbidden_response\n\n          assert Enum.any?(headers, fn\n                   {\"content-type\", content_type} ->\n                     upstream.error_content_type\n                     |> Regex.compile!()\n                     |> Regex.match?(content_type)\n\n                   _ ->\n                     false\n                 end)\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    @tag :skip\n    test \"returns response when authorized\", %{access_token: access_token} do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          Upstreams.create_upstream(%{\n            scheme: \"http\",\n            host: \"httpbin.patatoid.fr\",\n            port: 80,\n            uris: [\"/httpbin\"],\n            strip_uri: true,\n            authorize: true,\n            required_scopes: %{\"GET\" => [\"test\"]}\n          })\n\n          Process.sleep(100)\n\n          request =\n            Finch.build(\n              :get,\n              \"http://localhost:7777/httpbin/status/418\",\n              [{\"authorization\", \"Bearer #{access_token.value}\"}],\n              \"\"\n            )\n\n          assert {:ok, %Finch.Response{body: body, status: 418}} =\n                   Finch.request(request, HttpClient)\n\n          assert body =~ ~r/teapot/\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    @tag :skip\n    test \"returns response root uri stripped\", %{access_token: access_token} do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          Upstreams.create_upstream(%{\n            scheme: \"http\",\n            host: \"httpbin.patatoid.fr\",\n            port: 80,\n            uris: [\"/\"],\n            strip_uri: true,\n            authorize: true,\n            required_scopes: %{\"GET\" => [\"test\"]}\n          })\n\n          Process.sleep(100)\n\n          request =\n            Finch.build(\n              :get,\n              \"http://localhost:7777/status/418\",\n              [{\"authorization\", \"Bearer #{access_token.value}\"}],\n              \"\"\n            )\n\n          assert {:ok, %Finch.Response{body: body, status: 418}} =\n                   Finch.request(request, HttpClient)\n\n          assert body =~ ~r/teapot/\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    @tag :skip\n    test \"returns authorization header with introspected token when authorized\", %{\n      access_token: access_token\n    } do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          {:ok, upstream} =\n            Upstreams.create_upstream(%{\n              scheme: \"http\",\n              host: \"httpbin.patatoid.fr\",\n              port: 80,\n              uris: [\"/httpbin\"],\n              strip_uri: true,\n              authorize: true,\n              required_scopes: %{\"GET\" => [\"test\"]},\n              forwarded_token_signature_alg: \"HS256\"\n            })\n\n          Process.sleep(100)\n\n          request =\n            Finch.build(\n              :get,\n              \"http://localhost:7777/httpbin/anything\",\n              [{\"authorization\", \"bearer #{access_token.value}\"}],\n              \"\"\n            )\n\n          assert {:ok, %Finch.Response{body: body, status: 200}} =\n                   Finch.request(request, HttpClient)\n\n          assert %{\n                   \"headers\" => %{\n                     \"Authorization\" => forwarded_authorization,\n                     \"X-Forwarded-Authorization\" => authorization\n                   }\n                 } = Jason.decode!(body)\n\n          assert [_authorization_header, token] = Regex.run(~r/bearer (.+)/, authorization)\n          signer = Client.signer(upstream)\n          assert {:ok, claims} = Client.Token.verify(token, signer)\n          assert claims[\"client_id\"] == access_token.client.id\n          assert claims[\"value\"] == access_token.value\n\n          assert forwarded_authorization == \"bearer #{access_token.value}\"\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n  end\n\n  @tag :skip\n  describe \"requests (from configuration file)\" do\n    setup do\n      {:ok, %Boruta.Ecto.Client{id: client_id}} = Admin.create_client(%{})\n\n      {:ok, access_token} =\n        AccessTokensAdapter.create(\n          %{\n            client: ClientsAdapter.get_client(client_id),\n            scope: \"test\"\n          },\n          []\n        )\n\n      {:ok, access_token: access_token}\n    end\n\n    test \"returns a 404 when no upstream found\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          configuration_file_path =\n            :code.priv_dir(:boruta_gateway)\n            |> Path.join(\"/test/configuration_files/not_found.yml\")\n\n          ConfigurationLoader.from_file!(configuration_file_path)\n\n          Process.sleep(100)\n\n          request = Finch.build(:get, \"http://localhost:7777/no_upstream\", [], \"\")\n\n          assert {:ok, %Finch.Response{body: body, status: 404}} =\n                   Finch.request(request, HttpClient)\n\n          assert body == \"No upstream has been found corresponding to the given request.\"\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    test \"returns a 401 when unauthorized\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          configuration_file_path =\n            :code.priv_dir(:boruta_gateway)\n            |> Path.join(\"/test/configuration_files/unauthorized.yml\")\n\n          ConfigurationLoader.from_file!(configuration_file_path)\n\n          Process.sleep(100)\n\n          request = Finch.build(:get, \"http://localhost:7777/unauthorized\", [], \"\")\n\n          assert {:ok, %Finch.Response{body: body, headers: headers, status: 401}} =\n                   Finch.request(request, HttpClient)\n\n          assert body == \"boom\"\n\n          assert Enum.any?(headers, fn\n                   {\"content-type\", content_type} ->\n                     Regex.match?(~r/text/, content_type)\n\n                   _ ->\n                     false\n                 end)\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    test \"returns a 403 when forbidden\", %{access_token: access_token} do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          configuration_file_path =\n            :code.priv_dir(:boruta_gateway)\n            |> Path.join(\"/test/configuration_files/forbidden.yml\")\n\n          ConfigurationLoader.from_file!(configuration_file_path)\n\n          Process.sleep(100)\n\n          request =\n            Finch.build(\n              :get,\n              \"http://localhost:7777/forbidden\",\n              [{\"authorization\", \"Bearer #{access_token.value}\"}],\n              \"\"\n            )\n\n          assert {:ok, %Finch.Response{body: body, headers: headers, status: 403}} =\n                   Finch.request(request, HttpClient)\n\n          assert body == \"boom\"\n\n          assert Enum.any?(headers, fn\n                   {\"content-type\", content_type} ->\n                     Regex.match?(~r/text/, content_type)\n\n                   _ ->\n                     false\n                 end)\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    @tag :skip\n    test \"returns response when authorized\", %{access_token: access_token} do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          configuration_file_path =\n            :code.priv_dir(:boruta_gateway)\n            |> Path.join(\"/test/configuration_files/authorized.yml\")\n\n          ConfigurationLoader.from_file!(configuration_file_path)\n\n          Process.sleep(100)\n\n          request =\n            Finch.build(\n              :get,\n              \"http://localhost:7777/httpbin/status/418\",\n              [{\"authorization\", \"Bearer #{access_token.value}\"}],\n              \"\"\n            )\n\n          assert {:ok, %Finch.Response{body: body, status: 418}} =\n                   Finch.request(request, HttpClient)\n\n          assert body =~ ~r/teapot/\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    @tag :skip\n    test \"returns authorization header with introspected token when authorized\", %{\n      access_token: access_token\n    } do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          configuration_file_path =\n            :code.priv_dir(:boruta_gateway)\n            |> Path.join(\"/test/configuration_files/authorized_introspect.yml\")\n\n          ConfigurationLoader.from_file!(configuration_file_path)\n\n          Process.sleep(100)\n\n          request =\n            Finch.build(\n              :get,\n              \"http://localhost:7777/httpbin/anything\",\n              [{\"authorization\", \"bearer #{access_token.value}\"}],\n              \"\"\n            )\n\n          assert {:ok, %Finch.Response{body: body, status: 200}} =\n                   Finch.request(request, HttpClient)\n\n          assert %{\n                   \"headers\" => %{\n                     \"Authorization\" => forwarded_authorization,\n                     \"X-Forwarded-Authorization\" => authorization\n                   }\n                 } = Jason.decode!(body)\n\n          assert [_authorization_header, token] = Regex.run(~r/bearer (.+)/, authorization)\n\n          upstream = Repo.all(Upstream) |> List.first()\n          signer = Client.signer(upstream)\n          assert {:ok, claims} = Client.Token.verify(token, signer)\n          assert claims[\"client_id\"] == access_token.client.id\n          assert claims[\"value\"] == access_token.value\n\n          assert forwarded_authorization == \"bearer #{access_token.value}\"\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n  end\n\n  @tag :skip\n  describe \"sidecar requests\" do\n    setup do\n      {:ok, %Boruta.Ecto.Client{id: client_id}} = Admin.create_client(%{})\n\n      {:ok, access_token} =\n        AccessTokensAdapter.create(\n          %{\n            client: ClientsAdapter.get_client(client_id),\n            scope: \"test\"\n          },\n          []\n        )\n\n      {:ok, access_token: access_token}\n    end\n\n    test \"returns a 404 when no upstream found\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          Upstreams.create_upstream(%{\n            node_name: Atom.to_string(node()),\n            scheme: \"http\",\n            host: \"should.not.be.called\",\n            port: 80,\n            uris: [\"/upstream\"]\n          })\n\n          Process.sleep(100)\n\n          request = Finch.build(:get, \"http://localhost:7778/no_upstream\", [], \"\")\n\n          assert {:ok, %Finch.Response{body: body, status: 404}} =\n                   Finch.request(request, HttpClient)\n\n          assert body == \"No upstream has been found corresponding to the given request.\"\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    test \"returns a 401 when unauthorized\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          {:ok, upstream} =\n            Upstreams.create_upstream(%{\n              node_name: Atom.to_string(node()),\n              scheme: \"http\",\n              host: \"should.not.be.called\",\n              port: 80,\n              uris: [\"/unauthorized\"],\n              authorize: true,\n              error_content_type: \"text\",\n              unauthorized_response: \"boom\"\n            })\n\n          Process.sleep(100)\n\n          request = Finch.build(:get, \"http://localhost:7778/unauthorized\", [], \"\")\n\n          assert {:ok, %Finch.Response{body: body, headers: headers, status: 401}} =\n                   Finch.request(request, HttpClient)\n\n          assert body == upstream.unauthorized_response\n\n          assert Enum.any?(headers, fn\n                   {\"content-type\", content_type} ->\n                     upstream.error_content_type\n                     |> Regex.compile!()\n                     |> Regex.match?(content_type)\n\n                   _ ->\n                     false\n                 end)\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    test \"returns a 403 when forbidden\", %{access_token: access_token} do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          {:ok, upstream} =\n            Upstreams.create_upstream(%{\n              node_name: Atom.to_string(node()),\n              scheme: \"http\",\n              host: \"should.not.be.called\",\n              port: 80,\n              uris: [\"/forbidden\"],\n              authorize: true,\n              required_scopes: %{\"GET\" => [\"required\"]},\n              error_content_type: \"text\",\n              forbidden_response: \"boom\"\n            })\n\n          Process.sleep(100)\n\n          request =\n            Finch.build(\n              :get,\n              \"http://localhost:7778/forbidden\",\n              [{\"authorization\", \"Bearer #{access_token.value}\"}],\n              \"\"\n            )\n\n          assert {:ok, %Finch.Response{body: body, headers: headers, status: 403}} =\n                   Finch.request(request, HttpClient)\n\n          assert body == upstream.forbidden_response\n\n          assert Enum.any?(headers, fn\n                   {\"content-type\", content_type} ->\n                     upstream.error_content_type\n                     |> Regex.compile!()\n                     |> Regex.match?(content_type)\n\n                   _ ->\n                     false\n                 end)\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    @tag :skip\n    test \"returns response when authorized\", %{access_token: access_token} do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          Upstreams.create_upstream(%{\n            node_name: Atom.to_string(node()),\n            scheme: \"http\",\n            host: \"httpbin.patatoid.fr\",\n            port: 80,\n            uris: [\"/httpbin\"],\n            strip_uri: true,\n            authorize: true,\n            required_scopes: %{\"GET\" => [\"test\"]}\n          })\n\n          Process.sleep(100)\n\n          request =\n            Finch.build(\n              :get,\n              \"http://localhost:7778/httpbin/status/418\",\n              [{\"authorization\", \"Bearer #{access_token.value}\"}],\n              \"\"\n            )\n\n          assert {:ok, %Finch.Response{body: body, status: 418}} =\n                   Finch.request(request, HttpClient)\n\n          assert body =~ ~r/teapot/\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    @tag :skip\n    test \"returns authorization header with introspected token when authorized\", %{\n      access_token: access_token\n    } do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          {:ok, upstream} =\n            Upstreams.create_upstream(%{\n              node_name: Atom.to_string(node()),\n              scheme: \"http\",\n              host: \"httpbin.patatoid.fr\",\n              port: 80,\n              uris: [\"/httpbin\"],\n              strip_uri: true,\n              authorize: true,\n              required_scopes: %{\"GET\" => [\"test\"]},\n              forwarded_token_signature_alg: \"HS256\"\n            })\n\n          Process.sleep(100)\n\n          request =\n            Finch.build(\n              :get,\n              \"http://localhost:7778/httpbin/anything\",\n              [{\"authorization\", \"bearer #{access_token.value}\"}],\n              \"\"\n            )\n\n          assert {:ok, %Finch.Response{body: body, status: 200}} =\n                   Finch.request(request, HttpClient)\n\n          assert %{\n                   \"headers\" => %{\n                     \"Authorization\" => forwarded_authorization,\n                     \"X-Forwarded-Authorization\" => authorization\n                   }\n                 } = Jason.decode!(body)\n\n          assert [_authorization_header, token] = Regex.run(~r/bearer (.+)/, authorization)\n          signer = Client.signer(upstream)\n          assert {:ok, claims} = Client.Token.verify(token, signer)\n          assert claims[\"client_id\"] == access_token.client.id\n          assert claims[\"value\"] == access_token.value\n\n          assert forwarded_authorization == \"bearer #{access_token.value}\"\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/test/boruta_gateway/upstreams/client_test.exs",
    "content": "defmodule BorutaGateway.Upstreams.ClientTest do\n  use ExUnit.Case\n  use Plug.Test\n\n  alias BorutaGateway.Repo\n  alias BorutaGateway.Upstreams\n  alias BorutaGateway.Upstreams.Client\n  alias BorutaGateway.Upstreams.ClientSupervisor\n  alias BorutaGateway.Upstreams.Upstream\n  alias Ecto.Adapters.SQL.Sandbox\n\n  describe \"ClientSupervisor.start_link/1\" do\n    test \"application starts supervisor\" do\n      assert {:error, {:already_started, _pid}} = ClientSupervisor.start_link([])\n    end\n  end\n\n  describe \"ClientSupervisor.client_for_upstream/1\" do\n    setup do\n      {:ok, upstream: %Upstream{id: SecureRandom.uuid()}}\n    end\n\n    test \"starts a client\", %{upstream: upstream} do\n      {:ok, client} = ClientSupervisor.client_for_upstream(upstream)\n\n      assert Process.alive?(client)\n    end\n\n    test \"starts a client with an upstream\", %{upstream: upstream} do\n      {:ok, client} = ClientSupervisor.client_for_upstream(upstream)\n\n      assert Client.upstream(client) == upstream\n    end\n\n    test \"starts a client with a Finch instance\", %{upstream: upstream} do\n      {:ok, client} = ClientSupervisor.client_for_upstream(upstream)\n\n      assert client |> Client.http_client() |> Process.whereis() |> Process.alive?()\n    end\n  end\n\n  # TODO change for an internal server\n  describe \"external http calls\" do\n    @tag :skip\n    test \"should request an external url (httpbin.patatoid.fr/status) given a Plug.Conn\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          {:ok, upstream} = Upstreams.create_upstream(%{scheme: \"http\", host: \"httpbin.patatoid.fr\", port: 80})\n          :timer.sleep(100)\n\n          conn = conn(\"GET\", \"/status/418\")\n\n          {:ok,\n           %{\n             body: body,\n             status: status\n           }} = Client.request(Upstream.with_http_client(upstream), conn)\n\n          assert status == 418\n          assert body =~ ~r/teapot/\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    @tag :skip\n    test \"should request an external url (httpbin.patatoid.fr/headers) given a Plug.Conn\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          {:ok, upstream} = Upstreams.create_upstream(%{scheme: \"http\", host: \"httpbin.patatoid.fr\", port: 80})\n          :timer.sleep(100)\n\n          conn =\n            conn(\"GET\", \"/headers\")\n            |> put_req_header(\"authorization\", \"Bearer test\")\n\n          {:ok,\n           %{\n             body: body,\n             status: status\n           }} = Client.request(Upstream.with_http_client(upstream), conn)\n\n          assert status == 200\n\n          req_headers = Jason.decode!(body)[\"headers\"]\n\n          assert Enum.any?(req_headers, fn\n                   {\"Authorization\", \"Bearer test\"} -> true\n                   _ -> false\n                 end)\n\n          assert Enum.any?(req_headers, fn\n                   {\"Host\", \"httpbin.patatoid.fr\"} -> true\n                   _ -> false\n                 end)\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/test/boruta_gateway/upstreams/store_test.exs",
    "content": "defmodule BorutaGateway.Upstreams.StoreTest do\n  use ExUnit.Case\n  use BorutaGateway.DataCase\n\n  alias BorutaGateway.ConfigurationLoader\n  alias BorutaGateway.Upstreams.Client\n  alias BorutaGateway.Upstreams.Store\n  alias BorutaGateway.Upstreams.Upstream\n  alias Ecto.Adapters.SQL.Sandbox\n\n  @tag :skip\n  test \"stores all inserted upstreams from repo\" do\n    Sandbox.unboxed_run(Repo, fn ->\n      try do\n        {:ok, a} = Repo.insert(%Upstream{host: \"test1.host\", port: 1111, uris: [\"/path1\"]})\n        {:ok, b} = Repo.insert(%Upstream{host: \"test2.host\", port: 2222, uris: [\"/path2\"]})\n        Store |> Process.whereis() |> Process.exit(:normal)\n        :timer.sleep(100)\n\n        upstreams = Store.all()\n\n        assert Enum.any?(upstreams, fn\n                 {[\"path1\"], %{id: id, http_client: http_client}} ->\n                   id == a.id && Process.alive?(http_client)\n\n                 _ ->\n                   false\n               end)\n\n        assert Enum.any?(upstreams, fn\n                 {[\"path2\"], %{id: id, http_client: http_client}} ->\n                   id == b.id && Process.alive?(http_client)\n\n                 _ ->\n                   false\n               end)\n      after\n        Repo.delete_all(Upstream)\n      end\n    end)\n  end\n\n  test \"stores all updated upstreams from repo\" do\n    Sandbox.unboxed_run(Repo, fn ->\n      try do\n        {:ok, a} = Repo.insert(%Upstream{host: \"test1.host\", port: 1111, uris: [\"/path\"]})\n\n        a = Ecto.Changeset.change(a, host: \"updated.host\")\n        {:ok, _a} = Repo.update(a)\n        :timer.sleep(200)\n\n        upstreams = Store.all()\n\n        assert Enum.any?(upstreams[\"global\"], fn\n                 {[\"path\"], %{host: \"updated.host\", http_client: http_client} = upstream} ->\n                   assert Client.upstream(http_client).host == upstream.host\n\n                 _ ->\n                   false\n               end)\n      after\n        Repo.delete_all(Upstream)\n      end\n    end)\n  end\n\n  test \"do not stores all deleted upstreams from repo\" do\n    Sandbox.unboxed_run(Repo, fn ->\n      try do\n        {:ok, a} = Repo.insert(%Upstream{host: \"test1.host\", port: 1111, uris: [\"/path\"]})\n        :timer.sleep(100)\n        upstreams = Store.all()\n\n        assert {_path, %Upstream{http_client: http_client}} =\n                 Enum.find(upstreams[\"global\"], fn\n                   {[\"path\"], %{id: id}} -> id == a.id\n                   _ -> false\n                 end)\n\n        assert Process.alive?(http_client)\n\n        Repo.delete(a)\n        :timer.sleep(100)\n\n        upstreams = Store.all()\n\n        assert Enum.all?(upstreams, fn\n                 {[\"path\"], %{id: id}} -> id != a.id\n                 _ -> false\n               end)\n\n        refute Process.alive?(http_client)\n      after\n        Repo.delete_all(Upstream)\n      end\n    end)\n  end\n\n  describe \"match/2\" do\n    test \"return matching upstream\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          {:ok, a} =\n            Repo.insert(%Upstream{host: \"test1.host\", port: 1111, uris: [\"/matching/uri\"]})\n\n          :timer.sleep(100)\n\n          %Upstream{id: id} = Store.match([\"matching\", \"uri\"])\n          assert id == a.id\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n  end\n\n  describe \"sidecar_match/2\" do\n    test \"return sidecar matching upstream\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          {:ok, a} =\n            Repo.insert(%Upstream{\n              node_name: ConfigurationLoader.node_name(),\n              host: \"test1.host\",\n              port: 1111,\n              uris: [\"/matching/uri\"]\n            })\n\n          :timer.sleep(100)\n\n          %Upstream{id: id} = Store.sidecar_match([\"matching\", \"uri\"])\n          assert id == a.id\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    test \"return sidecar matching upstream from static configuration\" do\n      Application.delete_env(ConfigurationLoader, :node_name)\n      configuration_file_path =\n        :code.priv_dir(:boruta_gateway)\n        |> Path.join(\"/test/configuration_files/full_configuration.yml\")\n      Application.put_env(:boruta_gateway, :configuration_path, configuration_file_path)\n      :timer.sleep(100)\n\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          {:ok, a} =\n            Repo.insert(%Upstream{\n              node_name: \"full-configuration\",\n              host: \"test1.host\",\n              port: 1111,\n              uris: [\"/matching/uri\"]\n            })\n\n          :timer.sleep(100)\n\n          %Upstream{id: id} = Store.sidecar_match([\"matching\", \"uri\"])\n          assert id == a.id\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/test/boruta_gateway/upstreams_test.exs",
    "content": "defmodule BorutaGateway.UpstreamsTest do\n  use BorutaGateway.DataCase\n\n  alias BorutaGateway.Upstreams\n  alias Ecto.Adapters.SQL.Sandbox\n\n  describe \"upstreams\" do\n    alias BorutaGateway.Upstreams.Upstream\n\n    @valid_attrs %{\n      scheme: \"https\",\n      host: \"test.host\",\n      port: 777,\n      uris: [\"/valid\"],\n      required_scopes: %{\"GET\" => [\"scope\"]}\n    }\n    @update_attrs %{host: \"update.host\"}\n    @invalid_attrs %{port: nil, required_scopes: %{\"BAD\" => \"bad_format\"}}\n\n    def upstream_fixture(attrs \\\\ %{}) do\n      {:ok, upstream} =\n        attrs\n        |> Enum.into(@valid_attrs)\n        |> Upstreams.create_upstream()\n\n      upstream\n    end\n\n    test \"list_upstreams/0 returns all upstreams\" do\n      upstream = upstream_fixture()\n      assert Upstreams.list_upstreams() == %{\"global\" => [upstream]}\n    end\n\n    test \"get_upstream!/1 returns the upstream with given id\" do\n      upstream = upstream_fixture()\n      assert Upstreams.get_upstream!(upstream.id) == upstream\n    end\n\n    test \"match/1 returns the upstream matching given path\" do\n      Sandbox.unboxed_run(Repo, fn ->\n        try do\n          upstream = upstream_fixture()\n          :timer.sleep(100)\n\n          %Upstream{id: id} = Upstreams.match([\"valid\"])\n          assert id == upstream.id\n        after\n          Repo.delete_all(Upstream)\n        end\n      end)\n    end\n\n    test \"create_upstream/1 with valid data creates a upstream\" do\n      assert {:ok, %Upstream{}} = Upstreams.create_upstream(@valid_attrs)\n    end\n\n    test \"create_upstream/1 generates a secret with HS* algorithms\" do\n      assert {:ok, %Upstream{forwarded_token_secret: forwarded_token_secret}} =\n               Upstreams.create_upstream(\n                 Map.put(\n                   @valid_attrs,\n                   :forwarded_token_signature_alg,\n                   \"HS256\"\n                 )\n               )\n\n      assert forwarded_token_secret\n    end\n\n    test \"create_upstream/1 generates a secret with RS* algorithms\" do\n      assert {:ok,\n              %Upstream{\n                forwarded_token_private_key: forwarded_token_private_key,\n                forwarded_token_public_key: forwarded_token_public_key\n              }} =\n               Upstreams.create_upstream(\n                 Map.put(\n                   @valid_attrs,\n                   :forwarded_token_signature_alg,\n                   \"RS256\"\n                 )\n               )\n\n      assert forwarded_token_private_key\n      assert forwarded_token_public_key\n    end\n\n    test \"create_upstream/1 with invalid data returns error changeset\" do\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [\n                  required_scopes: {\"Schema does not allow additional properties. at #/BAD\", []},\n                  scheme: {\"can't be blank\", [validation: :required]},\n                  host: {\"can't be blank\", [validation: :required]},\n                  port: {\"can't be blank\", [validation: :required]}\n                ]\n              }} = Upstreams.create_upstream(@invalid_attrs)\n    end\n\n    test \"create_upstream/1 with unique constraint returns error changeset\" do\n      Upstreams.create_upstream(@valid_attrs)\n\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [\n                  node_name:\n                    {\"has already been taken\",\n                     [constraint: :unique, constraint_name: \"upstreams_node_name_host_port_uris_index\"]}\n                ]\n              }} = Upstreams.create_upstream(@valid_attrs)\n    end\n\n    test \"update_upstream/2 with valid data updates the upstream\" do\n      upstream = upstream_fixture()\n      assert {:ok, %Upstream{}} = Upstreams.update_upstream(upstream, @update_attrs)\n    end\n\n    test \"update_upstream/2 with invalid data returns error changeset\" do\n      upstream = upstream_fixture()\n      assert {:error, %Ecto.Changeset{}} = Upstreams.update_upstream(upstream, @invalid_attrs)\n      assert upstream == Upstreams.get_upstream!(upstream.id)\n    end\n\n    test \"delete_upstream/1 deletes the upstream\" do\n      upstream = upstream_fixture()\n      assert {:ok, %Upstream{}} = Upstreams.delete_upstream(upstream)\n      assert_raise Ecto.NoResultsError, fn -> Upstreams.get_upstream!(upstream.id) end\n    end\n\n    test \"change_upstream/1 returns a upstream changeset\" do\n      upstream = upstream_fixture()\n      assert %Ecto.Changeset{} = Upstreams.change_upstream(upstream)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/test/support/data_case.ex",
    "content": "defmodule BorutaGateway.DataCase do\n  @moduledoc \"\"\"\n  This module defines the setup for tests requiring\n  access to the application's data layer.\n\n  You may define functions here to be used as helpers in\n  your tests.\n\n  Finally, if the test case interacts with the database,\n  we enable the SQL sandbox, so changes done to the database\n  are reverted at the end of every test. If you are using\n  PostgreSQL, you can even run database tests asynchronously\n  by setting `use nil.DataCase, async: true`, although\n  this option is not recommendded for other databases.\n  \"\"\"\n\n  use ExUnit.CaseTemplate\n\n  alias Ecto.Adapters.SQL.Sandbox\n\n  using do\n    quote do\n      alias BorutaGateway.Repo\n\n      import Ecto\n      import Ecto.Changeset\n      import Ecto.Query\n      import BorutaGateway.DataCase\n    end\n  end\n\n  setup tags do\n    :ok = Sandbox.checkout(BorutaGateway.Repo)\n    :ok = Sandbox.checkout(BorutaAuth.Repo)\n\n    unless tags[:async] do\n      Sandbox.mode(BorutaGateway.Repo, {:shared, self()})\n      Sandbox.mode(BorutaAuth.Repo, {:shared, self()})\n    end\n\n    :ok\n  end\n\n  @doc \"\"\"\n  A helper that transforms changeset errors into a map of messages.\n\n      assert {:error, changeset} = Accounts.create_user(%{password: \"short\"})\n      assert \"password is too short\" in errors_on(changeset).password\n      assert %{password: [\"password is too short\"]} = errors_on(changeset)\n\n  \"\"\"\n  def errors_on(changeset) do\n    Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->\n      Regex.replace(~r\"%{(\\w+)}\", message, fn _, key ->\n        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()\n      end)\n    end)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_gateway/test/test_helper.exs",
    "content": "ExUnit.start()\n"
  },
  {
    "path": "apps/boruta_identity/.formatter.exs",
    "content": "[\n  import_deps: [:ecto, :phoenix],\n  inputs: [\"*.{ex,exs}\", \"priv/*/seeds.exs\", \"{config,lib,test}/**/*.{ex,exs}\"],\n  subdirectories: [\"priv/*/migrations\"]\n]\n"
  },
  {
    "path": "apps/boruta_identity/.gitignore",
    "content": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up here.\n/cover/\n\n# The directory Mix downloads your dependencies sources to.\n/deps/\n\n# Where 3rd-party dependencies like ExDoc output generated docs.\n/doc/\n\n# Ignore .fetch files in case you like to edit your project deps locally.\n/.fetch\n\n# If the VM crashes, it generates a dump, let's ignore it too.\nerl_crash.dump\n\n# Also ignore archive artifacts (built via \"mix archive.build\").\n*.ez\n\n# Ignore package tarball (built via \"mix hex.build\").\nboruta_identity-*.tar\n\n# If NPM crashes, it generates a log, let's ignore it too.\nnpm-debug.log\n\n# The directory NPM downloads your dependencies sources to.\n/assets/node_modules/\n\n# Since we are building assets from assets/,\n# we ignore priv/static. You may want to comment\n# this depending on your deployment strategy.\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/.browserslistrc",
    "content": "> 1%\nlast 2 versions\nnot dead\nnot ie 11\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/.gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/Dockerfile",
    "content": "FROM node:21.4 AS builder\n\nCOPY . /app\n\nWORKDIR /app\n\nRUN npm ci\nRUN npm run build\n\nRUN npm install -g serve\n\nCMD [\"serve\", \"-s\", \"--listen\",  \"tcp://0.0.0.0:3000\",  \"dist\"]\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/README.md",
    "content": "# boruta-wallet\n\n## Project setup\n```\nnpm install\n```\n\n### Compiles and hot-reloads for development\n```\nnpm run serve\n```\n\n### Compiles and minifies for production\n```\nnpm run build\n```\n\n### Run your unit tests\n```\nnpm run test:unit\n```\n\n### Customize configuration\nSee [Configuration Reference](https://cli.vuejs.org/config/).\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/package.json",
    "content": "{\n  \"name\": \"boruta-wallet\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vite\",\n    \"build\": \"vite build --emptyOutDir\",\n    \"build:watch\": \"vite build --watch --emptyOutDir\",\n    \"deploy\": \"vite build\"\n  },\n  \"dependencies\": {\n    \"@sd-jwt/decode\": \"^0.9.2\",\n    \"axios\": \"^1.15.0\",\n    \"boruta-client\": \"github:malach-it/boruta-client\",\n    \"qr-scanner\": \"^1.4.2\",\n    \"register-service-worker\": \"^1.7.2\",\n    \"rollup\": \"^4.59.0\",\n    \"vue\": \"^3.2.13\",\n    \"vue-router\": \"^4.0.3\",\n    \"vuex\": \"^4.0.0\"\n  },\n  \"devDependencies\": {\n    \"@types/chai\": \"^4.2.15\",\n    \"@types/mocha\": \"^8.2.1\",\n    \"@vitejs/plugin-vue\": \"^5.2.3\",\n    \"@vue/test-utils\": \"^2.0.0-0\",\n    \"chai\": \"^4.2.0\",\n    \"sass\": \"^1.32.7\",\n    \"sass-loader\": \"^12.0.0\",\n    \"typescript\": \"~4.5.5\",\n    \"vite\": \"^6.4.2\",\n    \"vite-plugin-node-polyfills\": \"^0.23.0\",\n    \"vite-plugin-pwa\": \"^0.21.1\",\n    \"vite-plugin-singlefile\": \"^2.2.0\"\n  }\n}\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <link rel=\"manifest\" href=\"<%= BASE_URL %>manifest.json\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css\" integrity=\"sha512-KXol4x3sVoO+8ZsWPFI/r5KBVB/ssCGB5tsv2nVOKwLg33wTFP3fmnXa47FdSVIshVTgsYk/1734xSk9aFIa4A==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\" />\n    <title><%= htmlWebpackPlugin.options.title %></title>\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/public/robots.txt",
    "content": "User-agent: *\nDisallow:\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/App.vue",
    "content": "<template>\n  <div class=\"main header\">\n    <router-link to=\"/\">\n      <img src=\"./assets/accounts/wallet/images/logo.png\" />\n    </router-link>\n  </div>\n  <router-view/>\n</template>\n\n<style lang=\"scss\">\nhtml, body {\n  width: 100%;\n  overflow-x: hidden;\n}\n.main.header {\n  border-bottom: 1px solid #eee;\n  padding: 1em;\n  background: white;\n  display: flex;\n  justify-content: center;\n  img {\n    width: 4em;\n  }\n}\n#app {\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  background: #f8f8f8;\n  min-height: 100vh;\n}\n\nnav {\n  text-align: center;\n  padding: 30px;\n\n  a {\n    font-weight: bold;\n    color: black;\n\n    &:hover {\n      color: #555;\n    }\n    &.router-link-exact-active {\n      color: #f5ba00;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/components/Consent.vue",
    "content": "<template>\n  <div class=\"consent\" v-if=\"eventKey\">\n    <div class=\"ui center aligned segment\">\n      <h2>{{ message }}</h2>\n      <div class=\"ui fluid two buttons\">\n        <button class=\"ui orange button\" @click=\"$emit('abort', type)\">Abort</button>\n        <button class=\"ui green button\" @click=\"$emit('consent', eventKey)\">Proceed</button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name: 'Consent',\n  props: {\n    eventKey: String,\n    message: String,\n    type: String\n  },\n});\n</script>\n\n<style lang=\"scss\">\n.consent {\n  position: fixed;\n  z-index: 1000;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  background: rgba(0, 0, 0, 0.7);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n</style>\n\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/components/Credentials.vue",
    "content": "<template>\n  <div class=\"credentials\">\n    <div class=\"ui container\">\n      <div class=\"ui cards\">\n        <div class=\"card\" v-for=\"credential in credentials\">\n          <div class=\"content\">\n            <div class=\"header\">\n              {{ credential.credentialId }}\n            </div>\n            <div class=\"meta\">\n              {{ credential.format }}\n            </div>\n            <div class=\"description\">\n              <div class=\"ui list\">\n                <div class=\"item\" v-for=\"claim in credential.claims\">\n                  <div class=\"content\">\n                    <a class=\"header\">{{ claim.key }}</a>\n                    <div class=\"description\">{{ claim.value }}</div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n          <div class=\"extra content\">\n            <div class=\"ui fluid buttons\">\n              <button class=\"ui basic red button\" @click=\"$emit('deleteCredential', credential)\">\n              {{ deleteLabel || 'Delete' }}\n              </button>\n              <button class=\"ui basic blue button\" @click=\"showCredential(credential)\">Show</button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div class=\"modal-wrapper\" v-if=\"credential\">\n      <div class=\"ui modal visible active\">\n        <div class=\"header\">\n          {{ credential.credential_configuration_id }} credential JWT\n        </div>\n        <div class=\"content\">\n          <div class=\"description\">\n            <p class=\"ui segment\"><pre>{{ credential.credential }}</pre></p>\n          </div>\n        </div>\n        <div class=\"actions\">\n          <button class=\"ui positive right button\" @click=\"hideCredential()\">\n            Done\n          </button>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { defineComponent } from 'vue'\nimport { decodeSdJwt } from '@sd-jwt/decode'\nimport Consent from './Consent.vue'\n\nexport default defineComponent({\n  name: 'CredentialsView',\n  props: ['credentials', 'deleteLabel'],\n  components: { Consent },\n  data () {\n    return {\n      formattedCredentials: [],\n      credential: null\n    }\n  },\n  methods: {\n    showCredential (credential) {\n      this.credential = credential\n    },\n    hideCredential (credential) {\n      this.credential = null\n    }\n  }\n})\n</script>\n\n<style scoped lang=\"scss\">\n.ui.cards {\n  justify-content: center;\n}\n.card .item {\n  overflow: hidden\n\n}\npre {\n  white-space: pre-wrap;\n  word-wrap: break-word;\n  overflow: hidden;\n  overflow-y: scroll;\n  max-height: 60vh;\n}\n.modal-wrapper {\n  z-index: 1000;\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  display: flex;\n  align-items: flex-start;\n  justify-content: center;\n  padding: 1em;\n}\n</style>\n\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/components/HelloWorld.vue",
    "content": "<template>\n  <div class=\"hello\">\n    <h1>{{ msg }}</h1>\n    <p>\n      For a guide and recipes on how to configure / customize this project,<br>\n      check out the\n      <a href=\"https://cli.vuejs.org\" target=\"_blank\" rel=\"noopener\">vue-cli documentation</a>.\n    </p>\n    <h3>Installed CLI Plugins</h3>\n    <ul>\n      <li><a href=\"https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa\" target=\"_blank\" rel=\"noopener\">pwa</a></li>\n      <li><a href=\"https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router\" target=\"_blank\" rel=\"noopener\">router</a></li>\n      <li><a href=\"https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex\" target=\"_blank\" rel=\"noopener\">vuex</a></li>\n      <li><a href=\"https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-mocha\" target=\"_blank\" rel=\"noopener\">unit-mocha</a></li>\n      <li><a href=\"https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript\" target=\"_blank\" rel=\"noopener\">typescript</a></li>\n    </ul>\n    <h3>Essential Links</h3>\n    <ul>\n      <li><a href=\"https://vuejs.org\" target=\"_blank\" rel=\"noopener\">Core Docs</a></li>\n      <li><a href=\"https://forum.vuejs.org\" target=\"_blank\" rel=\"noopener\">Forum</a></li>\n      <li><a href=\"https://chat.vuejs.org\" target=\"_blank\" rel=\"noopener\">Community Chat</a></li>\n      <li><a href=\"https://twitter.com/vuejs\" target=\"_blank\" rel=\"noopener\">Twitter</a></li>\n      <li><a href=\"https://news.vuejs.org\" target=\"_blank\" rel=\"noopener\">News</a></li>\n    </ul>\n    <h3>Ecosystem</h3>\n    <ul>\n      <li><a href=\"https://router.vuejs.org\" target=\"_blank\" rel=\"noopener\">vue-router</a></li>\n      <li><a href=\"https://vuex.vuejs.org\" target=\"_blank\" rel=\"noopener\">vuex</a></li>\n      <li><a href=\"https://github.com/vuejs/vue-devtools#vue-devtools\" target=\"_blank\" rel=\"noopener\">vue-devtools</a></li>\n      <li><a href=\"https://vue-loader.vuejs.org\" target=\"_blank\" rel=\"noopener\">vue-loader</a></li>\n      <li><a href=\"https://github.com/vuejs/awesome-vue\" target=\"_blank\" rel=\"noopener\">awesome-vue</a></li>\n    </ul>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name: 'HelloWorld',\n  props: {\n    msg: String,\n  },\n});\n</script>\n\n<!-- Add \"scoped\" attribute to limit CSS to this component only -->\n<style scoped lang=\"scss\">\nh3 {\n  margin: 40px 0 0;\n}\nul {\n  list-style-type: none;\n  padding: 0;\n}\nli {\n  display: inline-block;\n  margin: 0 10px;\n}\na {\n  color: #42b983;\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/components/KeySelect.vue",
    "content": "<template>\n  <div class=\"select-key\">\n    <div class=\"ui center aligned key-select segment\">\n      <Consent\n        message=\"You are about to remove a cryptographic key\"\n        :event-key=\"removeKeyConsentEventKey\"\n        @abort=\"abortKeyConsent\"\n        @consent=\"removeKeyConsent\"\n      />\n      <h2>Select a key</h2>\n      <div class=\"ui error message\" v-if=\"error\">{{ error }}</div>\n      <div class=\"ui cards\" v-else>\n        <div class=\"card\" v-for=\"[identifier, did] in keys\">\n          <div class=\"content\">\n            <div class=\"header\">\n              {{ identifier }}\n            </div>\n            <div class=\"description\" v-if=\"selectedKeys.includes(identifier)\">\n              <p class=\"ui warning message\"><em>key confirmed</em></p>\n            </div>\n          </div>\n          <div class=\"extra content\">\n            <div class=\"ui two buttons\">\n              <button class=\"ui basic blue button\" @click=\"$emit('selected', identifier, did)\">Use this key</button>\n              <button class=\"ui basic red button\" @click=\"deleteKey(identifier)\">Delete</button>\n            </div>\n          </div>\n        </div>\n        <div class=\"card\" v-if=\"!requestedKey\">\n          <div class=\"content\">\n            <div class=\"header\">\n              <div class=\"ui form\">\n                <input type=\"text\" v-model=\"newIdentifier\" placeholder=\"Key identifier\"/>\n              </div>\n            </div>\n          </div>\n          <div class=\"extra content\">\n            <button :disabled=\"!newIdentifier\" class=\"ui fluid basic blue button\" @click=\"$emit('selected', newIdentifier)\">Add a new key</button>\n          </div>\n        </div>\n      </div>\n      <hr />\n      <button class=\"ui fluid orange button\" @click=\"$emit('abort', type)\">Abort</button>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { defineComponent } from 'vue'\nimport { BorutaOauth, KeyStore, CustomEventHandler } from 'boruta-client'\nimport { storage } from '../store'\n\nimport Consent from './Consent.vue'\n\nconst eventHandler = new CustomEventHandler()\n\nexport default defineComponent({\n  name: 'KeySelect',\n  components: { Consent },\n  data () {\n    const keyStore = new KeyStore(eventHandler, storage)\n\n    return {\n      keys: [],\n      newIdentifier: null,\n      removeKeyConsentEventKey: null,\n      requestedKey: null,\n      selectedKeys: [],\n      error: null,\n      keyStore\n    }\n  },\n  async mounted () {\n    await this.keyStore.listKeyIdentifiers().then(keys => {\n      this.keys = keys.map(key => [key])\n      keys.forEach(identifier => {\n        eventHandler.listen('remove_key-request', identifier, () => {\n          this.removeKeyConsentEventKey = identifier\n        })\n      })\n    })\n\n    await Promise.all(this.keys.map(async ([identifier]) => {\n      return [identifier, await this.keyStore.extractDid(identifier)]\n    })).then(keys => {\n      this.keys = keys\n      const key = keys.find(([identifier, did]) => {\n        console.log(did)\n        try {\n          return this.$route.query.client_id == did ||\n            JSON.parse(this.$route.query.credential_offer).client_id == did\n        } catch (_error) {\n          return false\n        }\n      })\n\n      if (key) {\n        this.requestedKey = key[0]\n      }\n    })\n\n    if (this.requestedKey) {\n      const keySelections = localStorage.getItem('keySelection')\n      if (keySelections) {\n        keySelections.split('|').forEach(keySelection => {\n          const keySelectedAt = keySelection.split('~')[0]\n          const selectedKey = keySelection.split('~')[1]\n          if (parseInt(keySelectedAt) + 60000 > Date.now()) {\n            this.selectedKeys.push(selectedKey)\n          } else {\n            localStorage.setItem('keySelection', keySelections.replace('|' + keySelection, ''))\n          }\n        })\n      } else {\n        this.keys = []\n        this.error = 'Cannot confirm requested key.'\n      }\n    }\n  },\n  methods: {\n    deleteKey (identifier) {\n      this.keyStore.removeKey(identifier).then(keys => {\n        this.keys = keys.map(key => [key])\n      })\n    },\n    removeKeyConsent (eventKey) {\n      eventHandler.dispatch('remove_key-approval', eventKey)\n      this.removeKeyConsentEventKey = null\n    },\n    abortKeyConsent () {\n      this.removeKeyConsentEventKey = null\n    }\n  }\n})\n</script>\n\n<style scoped lang=\"scss\">\n.ui.cards {\n  display: flex;\n  justify-content: center;\n}\n\n.select-key {\n  position: fixed;\n  z-index: 1000;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  background: rgba(0, 0, 0, 0.7);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.key-select.segment {\n  max-width: 80%;\n}\n</style>\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/main.ts",
    "content": "import { createApp } from 'vue'\nimport App from './App.vue'\nimport './registerServiceWorker'\nimport router from './router'\nimport store from './store'\n\ncreateApp(App).use(store).use(router).mount('#app')\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/registerServiceWorker.ts",
    "content": "/* eslint-disable no-console */\n\nimport { register } from 'register-service-worker'\n\nregister(`${window.env.BORUTA_OAUTH_BASE_URL}/accounts/wallet/sw.js`, {\n  ready () {\n    console.log(\n      'App is being served from cache by a service worker.\\n' +\n        'For more details, visit https://goo.gl/AFskqB'\n    )\n  },\n  registered () {\n    console.log('Service worker has been registered.')\n  },\n  cached () {\n    console.log('Content has been cached for offline use.')\n  },\n  updatefound () {\n    console.log('New content is downloading.')\n  },\n  updated () {\n    console.log('New content is available; please refresh.')\n  },\n  offline () {\n    console.log('No internet connection found. App is running in offline mode.')\n  },\n  error (error) {\n    console.error('Error during service worker registration:', error)\n  }\n})\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/router/index.ts",
    "content": "import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'\nimport HomeView from '../views/HomeView.vue'\nimport Oid4vcCallbackView from '../views/Oid4vcCallbackView.vue'\n\nconst routes: Array<RouteRecordRaw> = [\n  {\n    path: '/credentials',\n    name: 'home',\n    component: HomeView\n  },\n  {\n    path: '/',\n    name: 'oid4vc-callback',\n    component: Oid4vcCallbackView\n  },\n  {\n    path: '/callback',\n    name: 'callback',\n    component: HomeView\n  }\n]\n\nconst router = createRouter({\n  history: createWebHistory('/accounts/wallet'),\n  routes\n})\n\nexport default router\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/shims-vue.d.ts",
    "content": "/* eslint-disable */\ndeclare module '*.vue' {\n  import type { DefineComponent } from 'vue'\n  const component: DefineComponent<{}, {}, any>\n  export default component\n}\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/store/index.ts",
    "content": "import { createStore } from 'vuex'\nimport { CredentialsStore, BrowserStorage, BrowserEventHandler } from 'boruta-client'\n\nexport const storage = new BrowserStorage(window)\nconst eventHandler = new BrowserEventHandler(window)\nconst credentialsStore = new CredentialsStore(eventHandler, storage)\n\nconst store = createStore({\n  state: {\n    credentials: []\n  },\n  getters: {\n    credentials ({ credentials }) {\n      return credentials\n    }\n  },\n  mutations: {\n    async refreshCredentials(state) {\n      state.credentials = await credentialsStore.credentials()\n    },\n    deleteCredential(state, credential) {\n      credentialsStore.deleteCredential(credential.credential).then(credentials => {\n        state.credentials = credentials\n      })\n    }\n  },\n  actions: {\n  },\n  modules: {\n  }\n})\n\nstore.commit('refreshCredentials')\n\nexport default store\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/views/HomeView.vue",
    "content": "<template>\n  <div class=\"home\">\n    <Consent\n      message=\"You are about to remove a credential from your wallet\"\n      :event-key=\"deleteConsentEventKey\"\n      @abort=\"abortDelete\"\n      @consent=\"deleteConsent\"\n    />\n    <Credentials :credentials=\"credentials\" @deleteCredential=\"deleteCredential\" />\n    <div class=\"reader-overlay\" :class=\"{ 'hidden': !scanning }\" @click=\"hide()\">\n      <video ref=\"reader\" id=\"reader\"></video>\n    </div>\n    <div>\n      <button class=\"ui massive violet scan button\" @click=\"scan()\" v-show=\"!code\"><i class=\"ui qrcode icon\"></i> Scan a QR Code</button>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue'\nimport { mapGetters } from 'vuex'\nimport QrScanner from 'qr-scanner'\nimport Credentials from '../components/Credentials.vue'\nimport Consent from '../components/Consent.vue'\n\nexport default defineComponent({\n  name: 'HomeView',\n  components: { Credentials, Consent },\n  data () {\n    return {\n      qrScanner: null,\n      code: '',\n      scanning: false,\n      deleteConsentEventKey: null\n    }\n  },\n  mounted () {\n    this.qrScanner = new QrScanner(this.$refs.reader, result => {\n      const url = new URL(result)\n      this.qrScanner?.stop()\n      this.scanning = false\n      this.$router.push({\n        name: 'oid4vc-callback',\n        query: Object.fromEntries(url.searchParams)\n      })\n    })\n  },\n  computed: {\n    params () {\n      return this.$route.query\n    },\n    ...mapGetters(['credentials'])\n  },\n  methods: {\n    scan () {\n      this.scanning = true\n      this.qrScanner?.start()\n    },\n    hide () {\n      this.scanning = false\n      this.qrScanner?.stop()\n    },\n    deleteCredential (credential) {\n      window.addEventListener('delete_credential-request~' + credential.credential, () => {\n        this.deleteConsentEventKey = credential.credential\n      })\n      this.$store.commit('deleteCredential', credential)\n    },\n    deleteConsent (eventKey) {\n      window.dispatchEvent(new Event('delete_credential-approval~' + eventKey))\n      this.deleteConsentEventKey = null\n    },\n    abortDelete (eventKey) {\n      this.deleteConsentEventKey = null\n    }\n  }\n})\n</script>\n\n<style lang=\"scss\">\n  .home {\n    padding-top: 4em;\n    padding-bottom: 8em;\n    .button.scan {\n      position: fixed;\n      bottom: 1em;\n      right: 1em;\n    }\n    .reader-overlay {\n      z-index: 500;\n      position: fixed;\n      top: 0;\n      right: 0;\n      bottom: 0;\n      left: 0;\n      background: rgba(0, 0, 0, 0.9);\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      &.hidden {\n        display: none;\n      }\n      #reader {\n        border-radius: 1em;\n        max-height: 80%;\n        max-width: 80%;\n        border: 7px solid white;\n      }\n      .close {\n        position: fixed;\n        top: 1em;\n        right: 1em;\n        color: white;\n        cursor: pointer;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/views/Oid4vcCallbackView.vue",
    "content": "<template>\n  <div class=\"ui verifiable-presentations container\">\n    <h1 v-if=\"mode == 'oid4vp'\">Verifiable presentation</h1>\n    <h1 v-if=\"mode == 'siopv2'\">Key presentation</h1>\n      <Consent\n        message=\"You are about to add a new cryptographic key\"\n        :event-key=\"generateKeyConsentEventKey\"\n        @abort=\"abortKeyConsent\"\n        @consent=\"generateKeyConsent\"\n      />\n    <div class=\"ui segment\" v-if=\"error\">\n      <div class=\"ui placeholder segment\">\n        <div class=\"ui header\">\n          {{ error }}\n        </div>\n      </div>\n      <router-link to=\"/\" class=\"ui large fluid blue button\">Back</router-link>\n    </div>\n    <KeySelect v-if=\"keyConsentEventKey && !error\" @selected=\"selectKey\" @abort=\"keyConsentEventKey = null\"/>\n    <div class=\"ui segment\" v-if=\"!success && presentation_submission && !credentials.length\">\n      <div class=\"ui placeholder segment\">\n        <div class=\"ui header\">\n          No credential match the presentation\n        </div>\n      </div>\n      <router-link to=\"/\" class=\"ui large fluid blue button\">Back</router-link>\n    </div>\n    <div class=\"ui segment\" v-if=\"success\">\n      <div class=\"ui placeholder segment\">\n        <div class=\"ui header\">\n          {{ success }}\n        </div>\n      </div>\n      <router-link to=\"/\" class=\"ui large fluid blue button\">Back</router-link>\n    </div>\n    <div v-if=\"credentials.length\">\n      <div v-for=\"input_descriptor of presentation_definition.input_descriptors\">\n        <div v-for=\"field of input_descriptor.constraints.fields\">\n          <p :key=\"field.path\" v-if=\"field.id\" class=\"ui purpose segment\">\n            <strong>{{ field.id }}</strong> {{ field.purpose }}\n          </p>\n        </div>\n      </div>\n      <Credentials :credentials=\"credentials\" delete-label=\"Unselect\" @deleteCredential=\"deleteCredential\" />\n      <div class=\"ui segment\">\n        <form :action=\"redirect_uri\" method=\"POST\">\n          <input type=\"hidden\" name=\"vp_token\" :value=\"vp_token\" />\n          <input type=\"hidden\" name=\"presentation_submission\" :value=\"presentation_submission\" />\n          <button class=\"ui violet large fluid button\" type=\"submit\">Present your credential to {{ host }}</button>\n        </form>\n      </div>\n    </div>\n    <div class=\"ui segment\" v-if=\"id_token\">\n      <form method=\"POST\" :action=\"redirect_uri\" class=\"ui form large segment\">\n        <input type=\"hidden\" name=\"id_token\" :value=\"id_token\" />\n        <input type=\"hidden\" name=\"metadata_policy\" :value=\"metadata_policy\">\n        <button class=\"ui fluid blue button\" type=\"submit\">Present your cryptographic key</button>\n      </form>\n    </div>\n    <div class=\"issuance\" v-if=\"mode == 'oid4vci'\">\n      <VerifiableCredentialsIssuanceView />\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue'\nimport { BorutaOauth, KeyStore, CustomEventHandler } from 'boruta-client'\nimport { storage } from '../store'\nimport VerifiableCredentialsIssuanceView from './VerifiableCredentialsIssuanceView.vue'\n\nimport Consent from '../components/Consent.vue'\nimport Credentials from '../components/Credentials.vue'\nimport KeySelect from '../components/KeySelect.vue'\n\nconst eventHandler = new CustomEventHandler(window)\nconst oauth = new BorutaOauth({\n  host: window.env.BORUTA_OAUTH_BASE_URL,\n  jwksPath: window.env.BORUTA_OAUTH_BASE_URL + '/openid/jwks',\n  storage,\n  eventHandler\n})\n\n\nexport default defineComponent({\n  name: 'Oid4vcCallbackView',\n  components: { Consent, Credentials, KeySelect, VerifiableCredentialsIssuanceView },\n  data () {\n    return {\n      mode: null,\n      client: null,\n      host: null,\n      error: null,\n      success: null,\n      presentation: null,\n      credentials: [],\n      id_token: null,\n      metadata_policy: \"{}\",\n      redirect_uri: null,\n      vp_token: null,\n      requestedKey: null,\n      presentation_definition: null,\n      presentation_submission: null,\n      keyConsentEventKey: null,\n      generateKeyConsentEventKey: null,\n      keyIdentifier: null\n    }\n  },\n  async mounted () {\n    this.parseLocation()\n  },\n  methods: {\n    async parseLocation () {\n      this.mode = null\n\n      if (this.$route.query.error) {\n        this.mode = 'oid4vc_error'\n        this.error = this.$route.query.error_description\n      }\n\n      if (this.$route.query.code) {\n        this.mode = 'presentation_success'\n        this.success = 'Your credential has successfully been presented.'\n      }\n\n      if (this.$route.query.response_type == 'vp_token') {\n        this.mode = 'oid4vp'\n        const client = new oauth.VerifiablePresentations({\n          clientId: window.env.BORUTA_OAUTH_BASE_URL + '/accounts/wallet',\n          redirectUri: window.env.BORUTA_OAUTH_BASE_URL + '/accounts/wallet/verifiable-presentation'\n        })\n\n\n        this.client = client\n        client.parseVerifiablePresentationAuthorization(window.location).then((presentation) => {\n          this.presentation = presentation\n\n          eventHandler.listen('extract_key-request', this.presentation.id, () => {\n            this.keyConsentEventKey = this.presentation.id\n          })\n          eventHandler.listen('generate_key-request', '', () => {\n            this.generateKeyConsentEventKey = this.presentation.id\n          })\n\n          this.presentation_definition = presentation.presentation_definition\n\n          return presentation\n        }).then(this.client.generatePresentation.bind(this.client))\n          .then(({ credentials, redirect_uri, vp_token, presentation_submission }) => {\n            const keySelection = localStorage.getItem('keySelection')\n            localStorage.setItem('keySelection', keySelection + '|' + Date.now() + '~' + this.selectedKey)\n            this.redirect_uri = redirect_uri\n            this.host = new URL(redirect_uri).host\n            this.vp_token = vp_token\n            this.presentation_submission = presentation_submission\n            this.credentials = credentials\n        }).catch(response => {\n          if (response.error) {\n            this.error = response.error_description\n          } else {\n            this.success = response\n          }\n        })\n      }\n\n      if (this.$route.query.response_type == 'id_token') {\n        this.mode = 'siopv2'\n        eventHandler.listen('extract_key-request', this.$route.query.client_id, () => {\n          this.keyConsentEventKey = this.$route.query.client_id\n        })\n        eventHandler.listen('generate_key-request', '', () => {\n          this.generateKeyConsentEventKey = this.$route.query.client_id\n        })\n\n        const client = new oauth.Siopv2({ clientId: '', redirectUri: '' })\n        client.parseSiopv2Response(window.location).then(({ id_token, redirect_uri }) => {\n          const keySelection = localStorage.getItem('keySelection')\n          localStorage.setItem('keySelection', keySelection + '|' + Date.now() + '~' + this.selectedKey)\n          this.id_token = id_token\n          this.redirect_uri = redirect_uri\n        }).catch(({ error_description }) => {\n          this.error = error_description\n        })\n      }\n\n      if (this.$route.query.credential_offer) {\n        this.mode = 'oid4vci'\n      }\n\n      if (!this.mode) {\n        this.$router.push({ name: 'home' })\n      }\n    },\n    async selectKey (identifier, did) {\n      this.selectedKey = identifier\n      eventHandler.dispatch('extract_key-approval', this.keyConsentEventKey, identifier)\n      this.keyConsentEventKey = null\n    },\n    deleteCredential (credential) {\n      const credentials = this.credentials.map(e => e)\n      credentials.splice(this.credentials.indexOf(credential), 1)\n\n      this.client.generatePresentation(this.presentation, credentials)\n        .then(({ credentials, redirect_uri, vp_token, presentation_submission }) => {\n          this.credentials = credentials\n\n          this.redirect_uri = redirect_uri\n          this.host = new URL(redirect_uri).host\n          this.vp_token = vp_token\n          this.presentation_submission = presentation_submission\n          this.credentials = credentials\n        }).catch(response => {\n          if (response.error) {\n            this.error = response.error_description\n          }\n        })\n    },\n    generateKeyConsent (eventKey) {\n      eventHandler.dispatch('generate_key-approval', '')\n      this.generateKeyConsentEventKey = null\n    },\n    abortKeyConsent () {\n      this.generateKeyConsentEventKey = null\n    },\n  },\n  watch: {\n    '$route.query': {\n      handler: function () { this.parseLocation() },\n      deep: true\n    }\n  }\n})\n</script>\n\n<style scoped lang=\"scss\">\n.verifiable-presentations {\n  padding: 1.5em 0;\n  h1 {\n    padding: 1em .5em;\n    text-align: center\n  }\n  .purpose {\n    margin-bottom: 1.5em;\n  }\n}\n</style>\n\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/src/views/VerifiableCredentialsIssuanceView.vue",
    "content": "<template>\n  <div class=\"ui verifiable-credentials-issuance container\">\n    <div class=\"ui segment\" v-if=\"error\">\n      <div class=\"ui placeholder segment\">\n        <div class=\"ui header\">\n          {{ error }}\n        </div>\n      </div>\n      <router-link to=\"/\" class=\"ui large fluid blue button\">Back</router-link>\n    </div>\n    <div v-else>\n      <h1 v-if=\"credentialIssuer\">{{ credentialIssuer }} offer those credentials</h1>\n      <Consent\n        message=\"You are about to add a new cryptographic key\"\n        :event-key=\"generateKeyConsentEventKey\"\n        @abort=\"abortKeyConsent\"\n        @consent=\"generateKeyConsent\"\n      />\n      <Consent\n        message=\"You are about to insert a credential to your wallet\"\n        :event-key=\"insertConsentEventKey\"\n        @abort=\"abortInsertConsent\"\n        @consent=\"insertConsent\"\n      />\n      <KeySelect v-if=\"keyConsentEventKey && !error\" @selected=\"selectKey\" @abort=\"keyConsentEventKey = null\"/>\n      <div class=\"ui segment\" v-for=\"authorizationDetail in authorizationDetails\">\n        <h2>\n          {{ authorizationDetail.credential_configuration_id }}\n          <span class=\"ui brown label\">{{ authorizationDetail.format }}</span>\n        </h2>\n        <button :disabled=\"fetchingCredential\" class=\"ui fluid violet button\" @click=\"getCredential(authorizationDetail)\" :class=\"{ 'loading': fetchingCredential }\" v-if=\"tokenResponse\">Get credential</button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue'\nimport { BorutaOauth, KeyStore, CustomEventHandler } from 'boruta-client'\nimport { storage } from '../store'\nimport Consent from '../components/Consent.vue'\nimport KeySelect from '../components/KeySelect.vue'\n\nconst eventHandler = new CustomEventHandler()\nconst oauth = new BorutaOauth({\n  host: window.env.BORUTA_OAUTH_BASE_URL,\n  tokenPath: '/oauth/token',\n  credentialPath: '/openid/credential',\n  window,\n  storage,\n  eventHandler\n})\n\n\nexport default defineComponent({\n  name: 'VerifiableCredentialsIssuanceView',\n  components: { KeySelect, Consent },\n  data () {\n    const keyStore = new KeyStore(eventHandler, storage)\n    return {\n      keyStore,\n      client: null,\n      keys: [],\n      keyIdentifier: null,\n      newIdentifier: null,\n      credentialIssuer: null,\n      credentialId: null,\n      tokenResponse: null,\n      authorizationDetails: [],\n      keyConsentEventKey: null,\n      generateKeyConsentEventKey: null,\n      insertConsentEventKey: null,\n      fetchingCredential: false,\n      error: null\n    }\n  },\n  computed: {\n    params () {\n      return this.$route.query\n    }\n  },\n  async mounted () {\n    const client = new oauth.VerifiableCredentialsIssuance({\n      clientId: window.env.BORUTA_OAUTH_BASE_URL + '/accounts/wallet',\n      redirectUri: window.env.BORUTA_OAUTH_BASE_URL + '/accounts/wallet/preauthorized-code'\n    })\n    this.client = client\n\n    client.parsePreauthorizedCodeResponse(window.location).then(({ credential_issuer, preauthorized_code }) => {\n      oauth.host = credential_issuer\n      return client.getToken(preauthorized_code)\n    }).then((tokenResponse) => {\n      this.credentialIssuer = new URL(oauth.host).host\n\n      const { authorization_details } = tokenResponse\n      this.tokenResponse = tokenResponse\n      this.authorizationDetails = authorization_details\n    }).catch(({ error_description }) => {\n      this.error = error_description\n    })\n  },\n  methods: {\n    async selectKey (identifier) {\n      this.keyConsentEventKey = null\n      eventHandler.dispatch('extract_key-approval', this.credentialId, identifier)\n    },\n    insertConsent (eventKey) {\n      eventHandler.dispatch('insert_credential-approval', eventKey)\n      this.keyConsentEventKey = null\n    },\n    generateKeyConsent (eventKey) {\n      eventHandler.dispatch('generate_key-approval', '')\n      this.generateKeyConsentEventKey = null\n    },\n    abortKeyConsent () {\n      this.keyConsentEventKey = null\n      this.generateKeyConsentEventKey = null\n      this.fetchingCredential = false\n      this.keyIdentifier = null\n    },\n    abortInsertConsent () {\n      this.insertConsentEventKey = null\n      this.fetchingCredential = false\n    },\n    getCredential({ credential_configuration_id, format }) {\n      this.fetchingCredential = true\n\n      this.credentialId = credential_configuration_id\n      eventHandler.listen('extract_key-request', this.credentialId, () => {\n        this.keyConsentEventKey = this.credentialId\n      })\n      eventHandler.listen('generate_key-request', '', () => {\n        this.generateKeyConsentEventKey = this.credentialId\n      })\n      eventHandler.listen('insert_credential-request', this.credentialId, () => {\n        this.insertConsentEventKey = this.credentialId\n      })\n      this.client.getCredential(this.tokenResponse, credential_configuration_id, format).then((credential) => {\n        this.$store.commit('refreshCredentials')\n        this.$router.push({ name: 'home' })\n        this.resetKeySelect()\n      }).catch(({ error_description }) => {\n        this.error = error_description\n        this.resetKeySelect()\n      })\n    },\n    resetKeySelect () {\n      localStorage.removeItem('keySelection')\n    }\n  }\n})\n</script>\n\n<style scoped lang=\"scss\">\n.verifiable-credentials-issuance {\n  padding: 1.5em 0;\n  h1 {\n    padding: 1em .5em;\n    text-align: center\n  }\n}\n</style>\n\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es6\",\n    \"module\": \"esnext\",\n    \"strict\": true,\n    \"jsx\": \"preserve\",\n    \"importHelpers\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"useDefineForClassFields\": true,\n    \"sourceMap\": true,\n    \"baseUrl\": \".\",\n    \"types\": [\n      \"webpack-env\",\n      \"mocha\",\n      \"chai\"\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"src/*\"\n      ]\n    },\n    \"lib\": [\n      \"esnext\",\n      \"dom\",\n      \"dom.iterable\",\n      \"scripthost\"\n    ]\n  },\n  \"include\": [\n    \"src/**/*.ts\",\n    \"src/**/*.tsx\",\n    \"src/**/*.vue\",\n    \"tests/**/*.ts\",\n    \"tests/**/*.tsx\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "apps/boruta_identity/assets/wallet/vite.config.js",
    "content": "import path from 'path'\nimport { defineConfig } from 'vite'\nimport { viteSingleFile } from 'vite-plugin-singlefile'\nimport vue from '@vitejs/plugin-vue'\nimport { VitePWA } from 'vite-plugin-pwa'\nimport { nodePolyfills } from 'vite-plugin-node-polyfills'\n\nconst base_url = new URL(process.env.BORUTA_OAUTH_BASE_URL || 'http://localhost:4000')\n\nconst manifest = {\n  \"name\": \"boruta wallet\",\n  \"theme_color\": \"#f5ba00\",\n  \"background_color\": \"#333333\",\n  \"display\": \"standalone\",\n  \"scope\": \"/accounts/wallet\",\n  \"start_url\": \"/accounts/wallet\",\n  \"intent_filters\": {\n    \"scope_url_scheme\": base_url.protocol.slice(0, -1),\n    \"scope_url_host\": base_url.host,\n    \"scope_url_path\": \"/accounts/wallet\"\n  },\n  \"capture_links\": \"existing-client-navigate\",\n  \"url_handlers\": [\n    {\n      \"origin\": `${base_url.toString()}/accounts/wallet`\n    }\n  ],\n  \"icons\": [{\n    \"src\": \"/accounts/wallet/images/icons/logo-512x512.png\",\n    \"sizes\": \"512x512\",\n    \"type\": \"image/png\",\n    \"purpose\": \"any\"\n  }]\n}\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [\n    nodePolyfills({\n      // To exclude specific polyfills, add them to this list.\n      exclude: [\n        'fs', // Excludes the polyfill for `fs` and `node:fs`.\n      ],\n      // Whether to polyfill specific globals.\n      globals: {\n        Buffer: true, // can also be 'build', 'dev', or false\n        global: true,\n        process: true,\n      },\n      // Whether to polyfill `node:` protocol imports.\n      protocolImports: true,\n    }),\n    vue(),\n    viteSingleFile(),\n    VitePWA({\n      injectRegister: 'auto',\n      manifest\n    })\n  ],\n  publicDir: false,\n  build: {\n    outDir: path.resolve(__dirname, '../../priv/static/wallet'),\n    emptyOutDir: false,\n    lib: {\n      entry: path.resolve(__dirname, './src/main.ts'),\n      name: 'Boruta',\n      fileName: (format) => `app.${format}.js`\n    },\n    target: 'esnext',\n    assetsInlineLimit: 100000000,\n    chunkSizeWarningLimit: 100000000,\n    cssCodeSplit: false,\n    brotliSize: false\n  }\n})\n"
  },
  {
    "path": "apps/boruta_identity/config/config.exs",
    "content": "import Config\n\nconfig :boruta_identity,\n  ecto_repos: [BorutaAuth.Repo, BorutaIdentity.Repo]\n\nconfig :boruta_identity, BorutaIdentityWeb.Endpoint,\n  url: [host: \"localhost\"],\n  # url: [host: \"localhost\", path: \"/accounts\"],\n  server: false,\n  secret_key_base: \"Caq0kwgjLGwxoEVPOxUhEiZ3AG2nADaNYi+ceWh2RuAgKF6vv/FfwqM/P7cDcNrR\",\n  render_errors: [view: BorutaIdentityWeb.ErrorView, accepts: ~w(html json), layout: false],\n  pubsub_server: BorutaIdentity.PubSub,\n  live_view: [signing_salt: \"9q0RPs/i\"]\n\nconfig :logger, :console,\n  format: \"$time $metadata[$level] $message\\n\",\n  metadata: [:request_id]\n\nconfig :phoenix, :json_library, Jason\n\nconfig :boruta, Boruta.Oauth,\n  repo: BorutaAuth.Repo,\n  contexts: [\n    resource_owners: BorutaIdentity.ResourceOwners\n  ],\n  issuer: System.get_env(\"BORUTA_OAUTH_BASE_URL\", \"http://localhost:4000\")\n\nconfig :oauth2, adapter: Tesla.Adapter.Mint\n\nimport_config \"#{Mix.env()}.exs\"\n"
  },
  {
    "path": "apps/boruta_identity/config/dev.exs",
    "content": "import Config\n\nconfig :boruta_identity, BorutaIdentity.Repo,\n  username: \"postgres\",\n  password: \"postgres\",\n  database: \"boruta_dev\",\n  hostname: \"localhost\",\n  show_sensitive_data_on_connection_error: true,\n  pool_size: 5,\n  after_connect: {BorutaIdentity.Repo, :set_limit, []}\n\nconfig :boruta_identity, BorutaIdentityWeb.Endpoint,\n  http: [port: System.get_env(\"BORUTA_OAUTH_PORT\", \"4000\") |> String.to_integer(), path: \"/accounts\"],\n  debug_errors: true,\n  code_reloader: true,\n  check_origin: false,\n  server: false\n\nconfig :boruta_identity, BorutaIdentity.SMTP,\n  adapter: Swoosh.Adapters.SMTP\n\nconfig :logger, :console, format: \"[$level] $message\\n\"\n\nconfig :phoenix, :stacktrace_depth, 20\n\nconfig :phoenix, :plug_init_mode, :runtime\n"
  },
  {
    "path": "apps/boruta_identity/config/prod.exs",
    "content": "import Config\n"
  },
  {
    "path": "apps/boruta_identity/config/test.exs",
    "content": "import Config\n\n# Configure your database\n#\n# The MIX_TEST_PARTITION environment variable can be used\n# to provide built-in test partitioning in CI environment.\n# Run `mix help test` for more information.\nconfig :boruta_identity, BorutaIdentity.Repo,\n  username: \"postgres\",\n  password: \"postgres\",\n  database: \"boruta_identity_test#{System.get_env(\"MIX_TEST_PARTITION\")}\",\n  hostname: \"localhost\",\n  pool: Ecto.Adapters.SQL.Sandbox,\n  after_connect: {BorutaIdentity.Repo, :set_limit, []}\n\nconfig :boruta_identity, BorutaIdentityWeb.Endpoint,\n  http: [port: 4002],\n  server: false\n\nconfig :boruta_identity, BorutaIdentity.SMTP,\n  adapter: Swoosh.Adapters.Test\n\nconfig :boruta_identity, BorutaIdentity.LdapRepo,\n  adapter: BorutaIdentity.LdapRepoMock\n\nconfig :logger, level: :warn\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/backends/federated.ex",
    "content": "defmodule BorutaIdentity.Accounts.Federated do\n  @moduledoc false\n  @behaviour BorutaIdentity.FederatedAccounts\n\n  import Ecto.Query\n\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Repo\n\n  @features [\n    :destroyable\n  ]\n\n  def features, do: @features\n\n  @account_type \"federated\"\n\n  def account_type, do: @account_type\n\n  @impl BorutaIdentity.FederatedAccounts\n  def domain_user!(federated_server_name, access_token, backend) do\n    federated_server =\n      Enum.find(backend.federated_servers, fn %{\"name\" => name} ->\n        name == federated_server_name\n      end)\n\n    base_url = URI.parse(federated_server[\"base_url\"])\n\n    userinfo_uri =\n      case URI.parse(federated_server[\"userinfo_path\"]) do\n        %URI{host: host} = uri when not is_nil(host) ->\n          uri\n\n        %URI{path: path} ->\n          %{base_url | path: path}\n      end\n      |> URI.to_string()\n\n    userinfo = get_resource!(userinfo_uri, access_token)\n\n    federated_metadata =\n      Enum.flat_map(federated_server[\"metadata_endpoints\"] || [], fn endpoint ->\n        response = get_resource!(endpoint[\"endpoint\"], access_token)\n\n        claims_from_response(endpoint, response)\n      end)\n      |> Enum.into(%{})\n\n    impl_user_params = %{\n      uid: to_string(userinfo[\"sub\"] || userinfo[\"id\"]),\n      username: userinfo[\"email\"] || \"#{userinfo[\"sub\"]}@#{federated_server[\"name\"]}\",\n      federated_metadata: %{federated_server_name => Map.merge(userinfo, federated_metadata)},\n      account_type: @account_type,\n      backend_id: backend.id\n    }\n\n    # TODO store origin federated server\n    changeset = User.implementation_changeset(impl_user_params, backend)\n    new_metadata = Ecto.Changeset.get_field(changeset, :federated_metadata)\n    new_username = Ecto.Changeset.get_field(changeset, :username)\n\n    Repo.insert!(changeset,\n      on_conflict:\n        from(u in User,\n          update: [\n            set: [\n              username: ^new_username,\n              federated_metadata: fragment(\"? || ?\", u.federated_metadata, ^new_metadata)\n            ]\n          ]\n        ),\n      returning: true,\n      conflict_target: [:backend_id, :uid]\n    )\n    |> Repo.preload([:authorized_scopes, :consents, :backend, :organizations])\n  end\n\n  def delete_user(_uid), do: :ok\n\n  defp get_resource!(url, access_token) do\n    case Finch.build(:get, url, [\n           {\"accept\", \"application/json\"},\n           {\"authorization\", \"Bearer #{access_token}\"}\n         ])\n         |> Finch.request(BorutaIdentity.Finch) do\n      {:ok, %Finch.Response{status: 200, body: body}} ->\n        Jason.decode!(body)\n\n      {:ok, %Finch.Response{status: status, body: body}} ->\n        raise \"GET #{url} failed with status #{status} - #{inspect(body)}\"\n\n      error ->\n        raise inspect(error)\n    end\n  end\n\n  defp claims_from_response(endpoint, body) do\n    endpoint[\"claims\"]\n    |> String.split(\" \")\n    |> Enum.map(fn claim ->\n      {String.replace(claim, \".\", \"-\"),\n       %{\n         \"value\" =>\n           get_in(\n             body,\n             String.split(claim, \".\")\n             |> Enum.map(fn\n               \":all\" -> Access.all()\n               claim -> claim\n             end)\n           )\n       }}\n    end)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/backends/internal/user.ex",
    "content": "defmodule BorutaIdentity.Accounts.Internal.User do\n  @moduledoc false\n\n  use Ecto.Schema\n\n  import Ecto.Changeset\n\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  @type t :: %__MODULE__{\n          email: String.t(),\n          password: String.t(),\n          hashed_password: String.t(),\n          metadata: map() | nil,\n          inserted_at: DateTime.t(),\n          updated_at: DateTime.t()\n        }\n\n  @derive {Inspect, except: [:password]}\n  @primary_key {:id, Ecto.UUID, autogenerate: true}\n  @foreign_key_type Ecto.UUID\n  schema \"internal_users\" do\n    field(:email, :string)\n    field(:password, :string, virtual: true)\n    field(:hashed_password, :string)\n    field(:metadata, :map, virtual: true)\n    field(:group, :string, virtual: true)\n\n    belongs_to(:backend, Backend)\n\n    timestamps()\n  end\n\n  @doc \"\"\"\n  A user changeset for registration.\n\n  It is important to validate the length of both email and password.\n  Otherwise databases may truncate the email without warnings, which\n  could lead to unpredictable or insecure behaviour. Long passwords may\n  also be very expensive to hash for certain algorithms.\n\n  ## Options\n\n    * `:hash_password` - Hashes the password so it can be stored securely\n      in the database and ensures the password field is cleared to prevent\n      leaks in the logs. If password hashing is not needed and clearing the\n      password field is not desired (like when using this changeset for\n      validations on a LiveView form), this option can be set to `false`.\n      Defaults to `true`.\n  \"\"\"\n  def registration_changeset(user, attrs, %{backend: backend} = opts) do\n    user\n    |> cast(attrs, [:email, :password])\n    |> validate_required([:email, :password])\n    |> change(backend_id: backend.id)\n    |> validate_email()\n    |> validate_password(opts)\n  end\n\n  def raw_registration_changeset(user, attrs, %{backend: backend}) do\n    user\n    |> cast(attrs, [:email, :hashed_password])\n    |> validate_required([:email, :hashed_password])\n    |> change(backend_id: backend.id)\n    |> validate_email()\n  end\n\n  defp validate_email(changeset) do\n    changeset\n    |> validate_format(:email, ~r/^[^\\s]+@[^\\s]+$/, message: \"must have the @ sign and no spaces\")\n    |> validate_length(:email, max: 160)\n    |> unique_constraint([:backend_id, :email], error_key: :email, message: \"has already been taken\")\n  end\n\n  defp validate_password(changeset, opts) do\n    changeset\n    |> validate_length(:password, min: 12, max: 80)\n    # |> validate_format(:password, ~r/[a-z]/, message: \"at least one lower case character\")\n    # |> validate_format(:password, ~r/[A-Z]/, message: \"at least one upper case character\")\n    # |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: \"at least one digit or punctuation character\")\n    |> maybe_hash_password(opts)\n  end\n\n  defp maybe_hash_password(changeset, %{backend: backend} = opts) do\n    hash_password? = Map.get(opts, :hash_password, true)\n    password = get_change(changeset, :password)\n\n    if hash_password? && password && changeset.valid? do\n      changeset\n      |> put_change(\n        :hashed_password,\n        apply(Backend.password_hashing_module(backend), :hash_pwd_salt, [\n          password,\n          Backend.password_hashing_opts(backend)\n        ])\n      )\n      |> delete_change(:password)\n    else\n      changeset\n    end\n  end\n\n  def update_changeset(user, attrs, opts) do\n    user\n    |> cast(attrs, [:email, :password, :group])\n    |> validate_required([:email])\n    |> validate_email()\n    |> validate_password(opts)\n  end\n\n  @doc \"\"\"\n  A user changeset for changing the password.\n\n  ## Options\n\n    * `:hash_password` - Hashes the password so it can be stored securely\n      in the database and ensures the password field is cleared to prevent\n      leaks in the logs. If password hashing is not needed and clearing the\n      password field is not desired (like when using this changeset for\n      validations on a LiveView form), this option can be set to `false`.\n      Defaults to `true`.\n  \"\"\"\n  def password_changeset(user, attrs, opts) do\n    user\n    |> cast(attrs, [:password])\n    |> validate_confirmation(:password, message: \"does not match password\")\n    |> validate_password(opts)\n  end\n\n  def valid_password?(backend, %__MODULE__{hashed_password: hashed_password}, password)\n      when is_binary(hashed_password) and byte_size(password) > 0 do\n    apply(\n      Backend.password_hashing_module(backend),\n      :verify_pass,\n      [password, hashed_password]\n    )\n  rescue\n    _ -> false\n  end\n\n  def valid_password?(backend, _, _) do\n    apply(\n      Backend.password_hashing_module(backend),\n      :no_user_verify,\n      []\n    )\n    false\n  rescue\n    _ -> false\n  end\n\n  @doc \"\"\"\n  Validates the current password otherwise adds an error to the changeset.\n  \"\"\"\n  def validate_current_password(backend, changeset, password) do\n    if valid_password?(backend, changeset.data, password) do\n      changeset\n    else\n      add_error(changeset, :current_password, \"is not valid\")\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/backends/internal.ex",
    "content": "defmodule BorutaIdentity.Accounts.Internal do\n  @moduledoc \"\"\"\n  Internal database `Accounts` implementation.\n  \"\"\"\n\n  @behaviour BorutaIdentity.Admin\n  @behaviour BorutaIdentity.Accounts.Registrations\n  @behaviour BorutaIdentity.Accounts.ResetPasswords\n  @behaviour BorutaIdentity.Accounts.Sessions\n  @behaviour BorutaIdentity.Accounts.Settings\n\n  import Ecto.Query, only: [from: 2]\n\n  alias BorutaIdentity.Accounts.Internal\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.UserToken\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.Repo\n\n  @features [\n    :authenticable,\n    :totpable,\n    :webauthnable,\n    :registrable,\n    :user_editable,\n    :confirmable,\n    :reset_password,\n    :consentable,\n    :destroyable\n  ]\n\n  def features, do: @features\n\n  @account_type \"internal\"\n\n  def account_type, do: @account_type\n\n  @impl BorutaIdentity.Accounts.Registrations\n  def register(backend, registration_params) do\n    with {:ok, user} <-\n           Internal.User.registration_changeset(\n             %Internal.User{\n               group: registration_params[:group],\n               metadata: registration_params[:metadata]\n             },\n             registration_params,\n             %{\n               backend: backend\n             }\n           )\n           |> Repo.insert() do\n      {:ok, domain_user!(user, backend)}\n    end\n  end\n\n  @impl BorutaIdentity.Accounts.Sessions\n  def get_user(backend, %{email: email}) when is_binary(email) do\n    user = Repo.get_by!(Internal.User, email: email, backend_id: backend.id)\n\n    {:ok, user}\n  rescue\n    Ecto.NoResultsError ->\n      {:error, \"User not found.\"}\n  end\n\n  def get_user(_authentication_params), do: {:error, \"Cannot find an user without an email.\"}\n\n  @impl BorutaIdentity.Accounts.Sessions\n  def domain_user!(\n        %Internal.User{id: id, email: email, metadata: metadata, group: group},\n        %Backend{\n          id: backend_id\n        } = backend,\n        repo \\\\ Repo\n      ) do\n    impl_user_params = %{\n      uid: id,\n      username: email,\n      group: group,\n      backend_id: backend_id,\n      account_type: @account_type\n    }\n\n    {replace, impl_user_params} =\n      case metadata do\n        %{} = metadata ->\n          {[:username, :metadata, :group], Map.put(impl_user_params, :metadata, metadata)}\n\n        _ ->\n          {[:username, :group], impl_user_params}\n      end\n\n    User.implementation_changeset(impl_user_params, backend)\n    |> repo.insert!(\n      on_conflict: {:replace, replace},\n      returning: true,\n      conflict_target: [:backend_id, :uid]\n    )\n    |> Repo.preload([:authorized_scopes, :consents, :backend, :organizations])\n  end\n\n  # BorutaIdentity.Accounts.Sessions, BorutaIdentity.Accounts.Settings\n  @impl true\n  def check_user_against(backend, user, authentication_params) do\n    check_user_password(backend, user, authentication_params[:password])\n  end\n\n  defp check_user_password(backend, user, password) do\n    case Internal.User.valid_password?(backend, user, password) do\n      true -> {:ok, user}\n      false -> {:error, \"Invalid user password.\"}\n    end\n  end\n\n  @impl BorutaIdentity.Accounts.ResetPasswords\n  def reset_password(backend, reset_password_params) do\n    with {:ok, user} <-\n           get_user_by_reset_password_token(reset_password_params.reset_password_token),\n         {:ok, %{user: user}} <- reset_user_password_multi(backend, user, reset_password_params) do\n      {:ok, user}\n    else\n      {:error, :user, changeset, _} -> {:error, changeset}\n      {:error, _reason} = error -> error\n    end\n  end\n\n  @impl BorutaIdentity.Accounts.Settings\n  def update_user(backend, user, params) do\n    Repo.transaction(fn repo ->\n      case %{user | metadata: params[:metadata], group: params[:group]}\n             |> Internal.User.update_changeset(params, %{backend: backend})\n             |> repo.update() do\n        {:ok, user} ->\n          domain_user!(user, backend, repo)\n        {:error, error} ->\n          Repo.rollback(error)\n      end\n    end)\n  end\n\n  @impl BorutaIdentity.Accounts.Settings\n  def delete_user(uid) do\n    case Repo.delete_all(from(u in Internal.User, where: u.id == ^uid)) do\n      {1, nil} -> :ok\n      _ -> {:error, \"User could not be deleted.\"}\n    end\n  end\n\n  @impl BorutaIdentity.Admin\n  def create_user(backend, params) do\n    Repo.transaction(fn repo ->\n      case Internal.User.registration_changeset(\n               %Internal.User{\n                 group: params[:group],\n                 metadata: params[:metadata]\n               },\n               %{\n                 email: params[:username],\n                 password: params[:password]\n               },\n               %{backend: backend}\n             )\n             |> repo.insert() do\n\n        {:ok, user} ->\n          domain_user!(user, backend, repo)\n        {:error, error} ->\n          Repo.rollback(error)\n      end\n    end)\n  end\n\n  @impl BorutaIdentity.Admin\n  def create_raw_user(backend, params) do\n    # TODO database transaction\n    with {:ok, user} <-\n           Internal.User.raw_registration_changeset(\n             %Internal.User{},\n             %{\n               email: params[:username],\n               hashed_password: params[:hashed_password]\n             },\n             %{backend: backend}\n           )\n           |> Repo.insert() do\n      {:ok, domain_user!(user, backend)}\n    end\n  end\n\n  defp get_user_by_reset_password_token(token) do\n    with {:ok, query} <- UserToken.verify_email_token_query(token, \"reset_password\"),\n         %User{} = user <- Repo.one(query),\n         %Internal.User{} = user <- Repo.get(Internal.User, user.uid) do\n      {:ok, user}\n    else\n      _ -> {:error, \"Given reset password token is invalid.\"}\n    end\n  end\n\n  defp reset_user_password_multi(backend, user, reset_password_params) do\n    Ecto.Multi.new()\n    |> Ecto.Multi.update(\n      :user,\n      Internal.User.password_changeset(user, reset_password_params, %{backend: backend})\n    )\n    |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))\n    |> Repo.transaction()\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/backends/ldap/user.ex",
    "content": "defmodule BorutaIdentity.Accounts.Ldap.User do\n  @moduledoc false\n\n  defstruct uid: nil, dn: nil, username: nil, backend: nil, metadata: nil\n\n  @type t :: %__MODULE__{\n    uid: String.t() | nil,\n    dn: String.t() | nil,\n    username: String.t() | nil,\n    backend: BorutaIdentity.IdentityProviders.Backend.t() | nil,\n    metadata: map() | nil\n  }\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/backends/ldap.ex",
    "content": "defmodule BorutaIdentity.Accounts.LdapError do\n  @enforce_keys [:message]\n  defexception [:message]\n\n  @type t :: %__MODULE__{\n          message: String.t()\n        }\n\n  def exception(message) when is_binary(message) do\n    %__MODULE__{message: message}\n  end\n\n  def message(exception) do\n    exception.message\n  end\nend\n\ndefmodule BorutaIdentity.Accounts.Ldap do\n  @moduledoc false\n\n  @behaviour NimblePool\n\n  alias BorutaIdentity.Accounts.Ldap\n  alias BorutaIdentity.Accounts.LdapError\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.UserToken\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.LdapRepo\n  alias BorutaIdentity.Repo\n\n  @behaviour BorutaIdentity.Accounts.ResetPasswords\n  @behaviour BorutaIdentity.Accounts.Sessions\n  @behaviour BorutaIdentity.Accounts.Settings\n  @behaviour BorutaIdentity.Admin\n\n  @features [\n    :authenticable,\n    :totpable,\n    :webauthnable,\n    :consentable,\n    :user_editable,\n    :reset_password,\n    :confirmable\n  ]\n\n  @account_type \"ldap\"\n\n  def account_type, do: \"ldap\"\n\n  @ldap_timeout 10_000\n\n  def features, do: @features\n\n  @impl BorutaIdentity.Accounts.Sessions\n  def get_user(backend, %{email: email}) do\n    lazy_start(backend)\n\n    NimblePool.checkout!(\n      pool_name(backend),\n      :checkout,\n      fn _from, ldap ->\n        {fetch_user_from_ldap(ldap, backend, email), ldap}\n      end,\n      @ldap_timeout\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.Sessions\n  def domain_user!(\n        %Ldap.User{uid: uid, username: username, metadata: metadata},\n        %Backend{id: backend_id} = backend\n      ) do\n    impl_user_params = %{\n      uid: uid,\n      username: username,\n      backend_id: backend_id,\n      account_type: @account_type\n    }\n\n    {replace, impl_user_params} =\n      case metadata do\n        %{} = metadata ->\n          {[:username, :metadata], Map.put(impl_user_params, :metadata, metadata)}\n\n        _ ->\n          {[:username], impl_user_params}\n      end\n\n    User.implementation_changeset(impl_user_params, backend)\n    |> Repo.insert!(\n      on_conflict: {:replace, replace},\n      returning: true,\n      conflict_target: [:backend_id, :uid]\n    )\n    |> Repo.preload([:authorized_scopes, :consents, :backend, :organizations])\n  end\n\n  @impl BorutaIdentity.Accounts.Sessions\n  def check_user_against(backend, ldap_user, authentication_params) do\n    lazy_start(backend)\n\n    NimblePool.checkout!(\n      pool_name(backend),\n      :checkout,\n      fn _from, ldap ->\n        case LdapRepo.simple_bind(ldap, ldap_user.dn, authentication_params[:password]) do\n          :ok ->\n            {{:ok, ldap_user}, ldap}\n\n          _error ->\n            {{:error, \"Authentication failure.\"}, ldap}\n        end\n      end,\n      @ldap_timeout\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.Settings\n  def update_user(backend, user, params) do\n    lazy_start(backend)\n\n    NimblePool.checkout!(\n      pool_name(backend),\n      :checkout,\n      fn _from, ldap ->\n        case update_user_in_ldap(ldap, backend, user, params) do\n          {:ok, user} ->\n            {{:ok, domain_user!(%{user | metadata: params[:metadata]}, backend)}, ldap}\n\n          {:error, error, user} ->\n            # NOTE keep user synchronized from LDAP\n            domain_user!(user, backend)\n\n            {{:error, error}, ldap}\n        end\n      end\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.Settings\n  def delete_user(_id) do\n    {:error, \"LDAP backends does not support user deletion.\"}\n  end\n\n  @impl BorutaIdentity.Accounts.ResetPasswords\n  def reset_password(backend, reset_password_params) do\n    lazy_start(backend)\n\n    NimblePool.checkout!(\n      pool_name(backend),\n      :checkout,\n      fn _from, ldap ->\n        with {:ok, user} <-\n               get_user_by_reset_password_token(\n                 ldap,\n                 backend,\n                 reset_password_params.reset_password_token\n               ),\n             {:ok, _user} <- reset_password_in_ldap(ldap, backend, user, reset_password_params) do\n          {{:ok, user}, ldap}\n        else\n          {:error, error} ->\n            {{:error, error}, ldap}\n        end\n      end\n    )\n  end\n\n  @impl BorutaIdentity.Admin\n  def create_user(_backend, _params) do\n    raise LdapError, \"LDAP backends does not support user creation.\"\n  end\n\n  @impl BorutaIdentity.Admin\n  def create_raw_user(_backend, _params) do\n    raise LdapError, \"LDAP backends does not support user creation.\"\n  end\n\n  @spec pool_name(backend :: Backend.t()) :: pool_name :: atom()\n  def pool_name(backend) do\n    signature =\n      backend\n      |> Map.from_struct()\n      |> Enum.map_join(fn\n        {key, value} ->\n          case Atom.to_string(key) do\n            \"ldap_\" <> _rest -> to_string(value)\n            _ -> nil\n          end\n      end)\n\n    signature = :crypto.hash(:sha256, signature)\n\n    signature\n    |> Base.encode16()\n    |> String.to_atom()\n  end\n\n  def start_link(backend) do\n    NimblePool.start_link(\n      pool_size: backend.ldap_pool_size,\n      worker: {__MODULE__, %{backend: backend}},\n      name: pool_name(backend)\n    )\n  end\n\n  @impl NimblePool\n  def init_worker(%{backend: backend}) do\n    # TODO add ldap port and ssl configurations\n    {:ok, ldap} = LdapRepo.open(backend.ldap_host)\n\n    {:ok, ldap, %{backend: backend}}\n  end\n\n  @impl NimblePool\n  def handle_checkout(:checkout, _from, ldap, pool_state) do\n    {:ok, ldap, ldap, pool_state}\n  end\n\n  @impl NimblePool\n  def handle_checkin(ldap, _from, ldap, pool_state) do\n    {:remove, :closed, pool_state}\n  end\n\n  @impl NimblePool\n  def terminate_worker(_reason, ldap, pool_state) do\n    LdapRepo.close(ldap)\n\n    {:ok, pool_state}\n  rescue\n    _ ->\n      {:ok, pool_state}\n  end\n\n  defp fetch_user_from_ldap(ldap, backend, email) do\n    user_rdn_attribute = backend.ldap_user_rdn_attribute\n\n    with {:ok, {dn, user_properties}} <- LdapRepo.search(ldap, backend, email),\n         uid when is_binary(uid) <-\n           Map.get(\n             user_properties,\n             \"uid\",\n             {:error, \"Could not get uid attribute\"}\n           ),\n         username when is_binary(username) <-\n           Map.get(\n             user_properties,\n             user_rdn_attribute,\n             {:error, \"Could not get #{user_rdn_attribute} attribute\"}\n           ) do\n      {:ok,\n       %Ldap.User{\n         uid: to_string(uid),\n         dn: to_string(dn),\n         username: to_string(username),\n         backend: backend\n       }}\n    else\n      {:error, error} when is_binary(error) ->\n        {:error, error}\n\n      {:error, error} ->\n        {:error, inspect(error)}\n    end\n  end\n\n  defp update_user_in_ldap(\n         ldap,\n         %Backend{\n           ldap_master_dn: ldap_master_dn,\n           ldap_master_password: ldap_master_password\n         } = backend,\n         user,\n         %{email: email} = params\n       ) do\n    with :ok <- LdapRepo.simple_bind(ldap, ldap_master_dn, ldap_master_password),\n         :ok <-\n           LdapRepo.modify(ldap, backend, user, email) do\n      user = %{user | username: to_string(email)}\n      update_user_in_ldap(ldap, backend, user, Map.delete(params, :email))\n    else\n      {:error, error} ->\n        {:error, error, user}\n    end\n  end\n\n  defp update_user_in_ldap(\n         ldap,\n         backend,\n         user,\n         %{password: password, current_password: current_password} = params\n       )\n       when byte_size(password) > 0 do\n    case LdapRepo.modify_password(ldap, user, password, current_password) do\n      :ok ->\n        update_user_in_ldap(ldap, backend, user, Map.delete(params, :password))\n\n      {:error, error} ->\n        {:error, error, user}\n    end\n  end\n\n  defp update_user_in_ldap(_ldap, _backend, user, _params), do: {:ok, user}\n\n  defp get_user_by_reset_password_token(ldap, backend, token) do\n    with {:ok, query} <- UserToken.verify_email_token_query(token, \"reset_password\"),\n         %User{username: username} <- Repo.one(query),\n         {:ok, user} <- fetch_user_from_ldap(ldap, backend, username) do\n      {:ok, user}\n    else\n      _ -> {:error, \"Given reset password token is invalid.\"}\n    end\n  end\n\n  defp reset_password_in_ldap(\n         ldap,\n         %Backend{\n           ldap_master_dn: ldap_master_dn,\n           ldap_master_password: ldap_master_password\n         },\n         user,\n         %{password: password} = reset_password_params\n       )\n       when byte_size(password) > 0 do\n    with :ok <- check_password_confirmation(reset_password_params),\n         :ok <- LdapRepo.simple_bind(ldap, ldap_master_dn, ldap_master_password),\n         :ok <- LdapRepo.modify_password(ldap, user, reset_password_params.password) do\n      {:ok, user}\n    end\n  end\n\n  defp reset_password_in_ldap(_ldap, _backend, _user, _reset_password_params),\n    do: {:error, \"Password cannot be empty.\"}\n\n  defp check_password_confirmation(%{\n         password: password,\n         password_confirmation: password_confirmation\n       })\n       when password == password_confirmation do\n    :ok\n  end\n\n  defp check_password_confirmation(_reset_password_params),\n    do: {:error, \"Password and password confirmation do not match.\"}\n\n  defp lazy_start(backend) do\n    case start_link(backend) do\n      {:ok, pid} -> {:ok, pid}\n      {:error, {:already_started, pid}} -> {:ok, pid}\n      error -> error\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/choose_sessions.ex",
    "content": "defmodule BorutaIdentity.Accounts.ChooseSessionApplication do\n  @moduledoc \"\"\"\n  TODO ConsentApplication documentation\n  \"\"\"\n\n  @callback choose_session_initialized(\n              context :: any(),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) :: any()\n\n  @callback choose_session_not_required(context :: any()) :: any()\nend\n\ndefmodule BorutaIdentity.Accounts.ChooseSessions do\n  @moduledoc false\n\n  import BorutaIdentity.Accounts.Utils, only: [defwithclientidp: 2]\n\n  alias BorutaIdentity.IdentityProviders\n\n  @spec initialize_choose_session(context :: any(), client_id :: String.t(), module :: atom()) ::\n          callback_result :: any()\n  defwithclientidp initialize_choose_session(context, client_id, module) do\n    case client_idp.choose_session do\n      true ->\n        module.choose_session_initialized(context, new_choose_session_template(client_idp))\n      false ->\n        module.choose_session_not_required(context)\n    end\n  end\n\n  defp new_choose_session_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(identity_provider.id, :choose_session)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/confirmations.ex",
    "content": "defmodule BorutaIdentity.Accounts.ConfirmationError do\n  @enforce_keys [:message]\n  defexception [:message]\n\n  @type t :: %__MODULE__{\n          message: String.t()\n        }\n\n  def exception(message) when is_binary(message) do\n    %__MODULE__{message: message}\n  end\n\n  def message(exception) do\n    exception.message\n  end\nend\n\ndefmodule BorutaIdentity.Accounts.ConfirmationApplication do\n  @moduledoc \"\"\"\n  TODO ConfirmationApplication documentation\n  \"\"\"\n\n  @callback confirmation_instructions_initialized(\n              context :: any(),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) :: any()\n\n  @callback confirmation_instructions_delivered(context :: any()) ::\n              any()\n\n  @callback user_confirmed(context :: any(), user :: BorutaIdentity.Accounts.User.t()) ::\n              any()\n\n  @callback user_confirmation_failure(\n              context :: any(),\n              error :: BorutaIdentity.Accounts.ConfirmationError.t()\n            ) ::\n              any()\nend\n\ndefmodule BorutaIdentity.Accounts.Confirmations do\n  @moduledoc false\n\n  import BorutaIdentity.Accounts.Utils, only: [defwithclientidp: 2]\n\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.ConfirmationError\n  alias BorutaIdentity.Accounts.Deliveries\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.UserToken\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.Repo\n\n  @type confirmation_instructions_params :: %{\n          email: String.t()\n        }\n  @type confirmation_url_fun :: (token :: String.t() -> confirmation_url :: String.t())\n\n  @spec initialize_confirmation_instructions(\n          context :: any(),\n          client_id :: String.t(),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp initialize_confirmation_instructions(context, client_id, module) do\n    module.confirmation_instructions_initialized(\n      context,\n      new_confirmation_instructions_template(client_idp)\n    )\n  end\n\n  @spec send_confirmation_instructions(\n          context :: any(),\n          client_id :: String.t(),\n          confirmation_instructions_params :: confirmation_instructions_params(),\n          confirmation_url_fun :: confirmation_url_fun(),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp send_confirmation_instructions(\n                     context,\n                     client_id,\n                     confirmation_instructions_params,\n                     confirmation_url_fun,\n                     module\n                   ) do\n    with %User{} = user <-\n           Accounts.get_user_by_email(\n             client_idp.backend,\n             confirmation_instructions_params[:email]\n           ) do\n      Deliveries.deliver_user_confirmation_instructions(client_idp.backend, user, confirmation_url_fun)\n    end\n\n    # NOTE return a success either confirmation instructions email sent or not\n    module.confirmation_instructions_delivered(context)\n  end\n\n  # NOTE If the token matches, the user account is marked as confirmed and the token is deleted.\n  @spec confirm_user(\n          context :: any(),\n          client_id :: String.t(),\n          token :: String.t(),\n          module :: atom()\n        ) :: callback_result :: any()\n  def confirm_user(context, _client_id, token, module) do\n    case confirm_user(token) do\n      {:ok, user} ->\n        module.user_confirmed(context, user)\n\n      {:error, _reason} ->\n        module.user_confirmation_failure(context, %ConfirmationError{\n          message: \"Account confirmation token is invalid or it has expired.\"\n        })\n    end\n  end\n\n  defp new_confirmation_instructions_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(\n      identity_provider.id,\n      :new_confirmation_instructions\n    )\n  end\n\n  defp confirm_user(token) do\n    with {:ok, query} <- UserToken.verify_email_token_query(token, \"confirm\"),\n         %User{confirmed_at: nil} = user <- Repo.one(query),\n         {:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do\n      {:ok, user}\n    else\n      _ ->\n        {:error, \"Account confirmation token is invalid or it has expired.\"}\n    end\n  end\n\n  defp confirm_user_multi(user) do\n    Ecto.Multi.new()\n    |> Ecto.Multi.update(:user, User.confirm_changeset(user))\n    |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, [\"confirm\"]))\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/consents.ex",
    "content": "defmodule BorutaIdentity.Accounts.ConsentApplication do\n  @moduledoc \"\"\"\n  TODO ConsentApplication documentation\n  \"\"\"\n\n  @callback consent_initialized(\n              context :: any(),\n              client :: Boruta.Oauth.Client.t(),\n              scopes :: list(Boruta.Oauth.Scope.t()),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) :: any()\n\n  @callback consent_not_required(context :: any()) :: any()\n\n  @callback consented(context :: any(), scope :: String.t()) :: any()\n\n  @callback consent_failed(context :: any(), changeset :: Ecto.Changeset.t()) :: any()\nend\n\ndefmodule BorutaIdentity.Accounts.Consents do\n  @moduledoc false\n\n  import BorutaIdentity.Accounts.Utils, only: [defwithclientidp: 2]\n  import Ecto.Query, only: [from: 2]\n\n  alias Boruta.Ecto.Admin\n  alias Boruta.Ecto.Clients\n  alias Boruta.Oauth.Scope\n  alias BorutaIdentity.Accounts.Consent\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.Repo\n\n  @spec initialize_consent(\n          context :: any(),\n          client_id :: String.t(),\n          user :: User.t(),\n          scope :: String.t(),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp initialize_consent(\n                    context,\n                    client_id,\n                    user,\n                    scope,\n                    module\n                  ) do\n    client = Clients.get_client(client_id)\n    scopes = Scope.split(scope)\n\n    case {client_idp.consentable, consented?(user, client_id, scopes)} do\n      {true, false} ->\n        scopes = Admin.get_scopes_by_names(scopes)\n\n        module.consent_initialized(context, client, scopes, new_consent_template(client_idp))\n      _ ->\n        module.consent_not_required(context)\n    end\n  end\n\n  @type consent_params :: %{\n          client_id: String.t(),\n          scopes: list(String.t())\n        }\n\n  @spec consent(\n          context :: any(),\n          client_id :: String.t(),\n          user :: User.t(),\n          params :: consent_params(),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp consent(context, client_id, user, params, module) do\n    _client_idp = client_idp\n\n    case user\n         |> User.consent_changeset(%{consents: [params]})\n         |> Repo.update() do\n      {:ok, _user} ->\n        module.consented(context, params[:scopes])\n\n      {:error, changeset} ->\n        module.consent_failed(context, changeset)\n    end\n  end\n\n  @spec consented?(user :: User.t(), client_id :: String.t(), scopes :: list(String.t())) :: boolean()\n  def consented?(%User{}, _client_id, []), do: true\n\n  def consented?(%User{id: user_id}, client_id, scopes) do\n    case Repo.one(from c in Consent, where: c.user_id == ^user_id and c.client_id == ^client_id) do\n      nil -> false\n      consent -> Enum.empty?(scopes -- consent.scopes)\n    end\n  end\n\n  def consented?(_, _, _), do: false\n\n  defp new_consent_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(identity_provider.id, :new_consent)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/deliveries/email_template.ex",
    "content": "defmodule BorutaIdentity.Accounts.EmailTemplate do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  @type t :: %__MODULE__{\n          id: String.t() | nil,\n          type: String.t(),\n          default: boolean(),\n          txt_content: String.t(),\n          html_content: String.t(),\n          inserted_at: DateTime.t() | nil,\n          updated_at: DateTime.t() | nil\n        }\n\n  @template_types [\n    :confirmation_instructions,\n    :reset_password_instructions,\n    :tx_code\n  ]\n\n  @type template_type :: :confirmation_instructions | :reset_password_instructions\n\n  @default_templates %{\n    txt_confirmation_instructions:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/emails/confirmation_instructions.txt.mustache\")\n      |> File.read!(),\n    html_confirmation_instructions:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/emails/confirmation_instructions.html.mustache\")\n      |> File.read!(),\n    txt_reset_password_instructions:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/emails/reset_password_instructions.txt.mustache\")\n      |> File.read!(),\n    html_reset_password_instructions:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/emails/reset_password_instructions.html.mustache\")\n      |> File.read!(),\n    txt_tx_code:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/emails/tx_code.txt.mustache\")\n      |> File.read!(),\n    html_tx_code:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/emails/tx_code.html.mustache\")\n      |> File.read!()\n  }\n\n  @foreign_key_type :binary_id\n  @primary_key {:id, :binary_id, autogenerate: true}\n  schema \"email_templates\" do\n    field(:txt_content, :string, default: \"\")\n    field(:html_content, :string, default: \"\")\n    field(:type, :string)\n\n    field(:default, :boolean, virtual: true, default: false)\n\n    belongs_to(:backend, Backend)\n\n    timestamps()\n  end\n\n  def template_types, do: @template_types\n\n  @spec default_txt_content(type :: template_type()) :: template_content :: String.t()\n  def default_txt_content(type) when type in @template_types, do: @default_templates[:\"txt_#{type}\"]\n\n  @spec default_html_content(type :: template_type()) :: template_content :: String.t()\n  def default_html_content(type) when type in @template_types, do: @default_templates[:\"html_#{type}\"]\n\n  @spec default_template(type :: template_type()) :: %__MODULE__{} | nil\n  def default_template(type) when type in @template_types do\n    %__MODULE__{\n      default: true,\n      type: Atom.to_string(type),\n      txt_content: default_txt_content(type),\n      html_content: default_html_content(type)\n    }\n  end\n\n  def default_template(_type), do: nil\n\n  @doc false\n  def changeset(template, attrs) do\n    template\n    |> cast(attrs, [:type, :txt_content, :html_content, :backend_id])\n    |> validate_required([:type, :backend_id, :txt_content, :html_content])\n    |> validate_inclusion(:type, Enum.map(@template_types, &Atom.to_string/1))\n    |> foreign_key_constraint(:backend_id)\n    |> put_default_txt()\n    |> put_default_html()\n  end\n\n  @doc false\n  def assoc_changeset(template, attrs) do\n    template\n    |> cast(attrs, [:type, :txt_content, :html_content])\n    |> validate_required([:type, :txt_content, :html_content])\n    |> validate_inclusion(:type, Enum.map(@template_types, &Atom.to_string/1))\n    |> put_default_txt()\n    |> put_default_html()\n  end\n\n  defp put_default_txt(changeset) do\n    case fetch_change(changeset, :txt_content) do\n      {:ok, content} when not is_nil(content) ->\n        changeset\n\n      _ ->\n        change(\n          changeset,\n          txt_content: default_txt_content(changeset |> fetch_field!(:type) |> String.to_atom())\n        )\n    end\n  end\n\n  defp put_default_html(changeset) do\n    case fetch_change(changeset, :html_content) do\n      {:ok, content} when not is_nil(content) ->\n        changeset\n\n      _ ->\n        change(\n          changeset,\n          html_content: default_html_content(changeset |> fetch_field!(:type) |> String.to_atom())\n        )\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/deliveries/user_notifier.ex",
    "content": "defmodule BorutaIdentity.Accounts.UserNotifier do\n  @moduledoc false\n\n  require Logger\n\n  import Swoosh.Email\n\n  alias BorutaIdentity.Accounts.EmailTemplate\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  def smtp_adapter do\n    Application.get_env(:boruta_identity, BorutaIdentity.SMTP)[:adapter]\n  end\n\n  # TODO hide Swoosh from the rest of the world\n  @spec deliver(email :: %Swoosh.Email{}, backend :: Backend.t()) ::\n          {:ok, email :: %Swoosh.Email{}} | {:error, reason :: String.t()}\n  def deliver(email, backend) do\n    config = [\n      relay: backend.smtp_relay,\n      username: backend.smtp_username,\n      password: backend.smtp_password,\n      ssl: backend.smtp_ssl,\n      tls: String.to_atom(backend.smtp_tls),\n      auth: :always,\n      port: backend.smtp_port,\n      # dkim: [\n      #   s: \"default\", d: \"domain.com\",\n      #   private_key: {:pem_plain, File.read!(\"priv/keys/domain.private\")}\n      # ],\n      retries: 2,\n      no_mx_lookups: false\n    ]\n\n    with :ok <- smtp_adapter().validate_config(config),\n         {:ok, _} <- smtp_adapter().deliver(email, config) do\n      {:ok, email}\n    else\n      {:error, {_status, %{\"Errors\" => errors}}} ->\n        reason =\n          errors\n          |> Enum.map_join(\", \", fn %{\"ErrorMessage\" => message} -> message end)\n\n        {:error, reason}\n\n      {:error, reason} ->\n        {:error, inspect(reason)}\n    end\n  rescue\n    _error ->\n      {:error, \"Bad SMTP configuration.\"}\n  end\n\n  @doc \"\"\"\n  Deliver instructions to confirm account.\n  \"\"\"\n  def deliver_confirmation_instructions(backend, user, url) do\n    template = Enum.find(backend.email_templates, fn %EmailTemplate{type: type} ->\n      type == \"confirmation_instructions\"\n    end) || EmailTemplate.default_template(:confirmation_instructions)\n\n    context = %{\n      user: Map.from_struct(user),\n      url: url\n    }\n\n    text_body = Mustachex.render(template.txt_content, context)\n    html_body = Mustachex.render(template.html_content, context)\n\n    new()\n    |> from(user.backend.smtp_from)\n    |> to(user.username)\n    |> subject(\"Welcome to boruta service beta preview\")\n    |> text_body(text_body)\n    |> html_body(html_body)\n  rescue\n    _error ->\n      {:error, \"Bad SMTP configuration.\"}\n  end\n\n  @doc \"\"\"\n  Deliver instructions to reset a user password.\n  \"\"\"\n  def deliver_reset_password_instructions(backend, user, url) do\n    template = Enum.find(backend.email_templates, fn %EmailTemplate{type: type} ->\n      type == \"reset_password_instructions\"\n    end) || EmailTemplate.default_template(:reset_password_instructions)\n\n    context = %{\n      user: Map.from_struct(user),\n      url: url\n    }\n\n    text_body = Mustachex.render(template.txt_content, context)\n    html_body = Mustachex.render(template.html_content, context)\n\n    new()\n    |> from(user.backend.smtp_from)\n    |> to(user.username)\n    |> subject(\"Reset your password.\")\n    |> text_body(text_body)\n    |> html_body(html_body)\n  rescue\n    _error ->\n      {:error, \"Bad SMTP configuration.\"}\n  end\n\n  def deliver_tx_code(backend, user, tx_code) do\n    template = Enum.find(backend.email_templates, fn %EmailTemplate{type: type} ->\n      type == \"tx_code\"\n    end) || EmailTemplate.default_template(:tx_code)\n\n    context = %{\n      user: Map.from_struct(user),\n      tx_code: tx_code\n    }\n\n    text_body = Mustachex.render(template.txt_content, context)\n    html_body = Mustachex.render(template.html_content, context)\n\n    new()\n    |> from(user.backend.smtp_from)\n    |> to(user.username)\n    |> subject(\"Wallet transaction code\")\n    |> text_body(text_body)\n    |> html_body(html_body)\n  rescue\n    _error ->\n      {:error, \"Bad SMTP configuration.\"}\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/deliveries.ex",
    "content": "defmodule BorutaIdentity.Accounts.Deliveries do\n  @moduledoc false\n\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.UserNotifier\n  alias BorutaIdentity.Accounts.UserToken\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.Repo\n\n  @type callback_function :: (token :: String.t() -> String.t())\n\n  @spec deliver_user_confirmation_instructions(\n          backend :: Backend.t(),\n          user :: User.t(),\n          confirmation_url_fun :: callback_function()\n        ) ::\n          {:ok, confirmation_token :: String.t()}\n          | {:error, reason :: String.t() | Ecto.Changeset.t()}\n  def deliver_user_confirmation_instructions(backend, %User{} = user, confirmation_url_fun)\n      when is_function(confirmation_url_fun, 1) do\n    if user.confirmed_at do\n      {:error, \"User is already confirmed.\"}\n    else\n      {encoded_token, confirmation_token} = UserToken.build_email_token(user, \"confirm\")\n\n      with {:ok, _confirmation_token} <- Repo.insert(confirmation_token),\n           {:ok, _email} <-\n             UserNotifier.deliver_confirmation_instructions(\n               backend,\n               user,\n               confirmation_url_fun.(encoded_token)\n             )\n             |> UserNotifier.deliver(backend) do\n        {:ok, encoded_token}\n      end\n    end\n  end\n\n  @spec deliver_user_reset_password_instructions(\n          backend :: Backend.t(),\n          user :: User.t(),\n          reset_password_url_fun :: callback_function()\n        ) :: {:ok, email :: any()} | {:error, reason :: any()}\n  def deliver_user_reset_password_instructions(backend, %User{} = user, reset_password_url) do\n    UserNotifier.deliver_reset_password_instructions(\n      backend,\n      user,\n      reset_password_url\n    )\n    |> UserNotifier.deliver(backend)\n  end\n\n  @spec deliver_tx_code(\n          backend :: Backend.t(),\n          user :: User.t(),\n          tx_code :: String.t()\n        ) ::\n          :ok\n          | {:error, reason :: String.t() | Ecto.Changeset.t()}\n  def deliver_tx_code(backend, %User{} = user, tx_code) do\n    with {:ok, _email} <-\n           UserNotifier.deliver_tx_code(\n             backend,\n             user,\n             tx_code\n           )\n           |> UserNotifier.deliver(backend) do\n      :ok\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/registrations.ex",
    "content": "defmodule BorutaIdentity.Accounts.RegistrationError do\n  @enforce_keys [:message]\n  defexception [:user, :message, :changeset, :template]\n\n  @type t :: %__MODULE__{\n          user: BorutaIdentity.Accounts.User.t() | nil,\n          message: String.t(),\n          changeset: Ecto.Changeset.t() | nil,\n          template: BorutaIdentity.IdentityProviders.Template.t()\n        }\n\n  def exception(message) when is_binary(message) do\n    %__MODULE__{message: message}\n  end\n\n  def message(exception) do\n    exception.message\n  end\nend\n\ndefmodule BorutaIdentity.Accounts.RegistrationApplication do\n  @moduledoc \"\"\"\n  TODO RegistrationApplication documentation\n  \"\"\"\n\n  @callback registration_initialized(\n              context :: any(),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) :: any()\n\n  @callback user_registered(\n              context :: any(),\n              user :: BorutaIdentity.Accounts.User.t(),\n              session_token :: String.t()\n            ) ::\n              any()\n\n  @callback registration_failure(\n              context :: any(),\n              error :: BorutaIdentity.Accounts.RegistrationError.t()\n            ) :: any()\nend\n\ndefmodule BorutaIdentity.Accounts.Registrations do\n  @moduledoc false\n\n  import BorutaIdentity.Accounts.Utils, only: [defwithclientidp: 2]\n\n  alias BorutaIdentity.Accounts.Deliveries\n  alias BorutaIdentity.Accounts.RegistrationError\n  alias BorutaIdentity.Accounts.Sessions\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n\n  @type registration_params :: %{\n          email: String.t(),\n          password: String.t(),\n          metadata: map()\n        }\n\n  @callback register(\n              backend :: BorutaIdentity.IdentityProviders.Backend.t(),\n              registration_params :: registration_params()\n            ) ::\n              {:ok, user :: User.t()}\n              | {:error, changeset :: Ecto.Changeset.t()}\n\n  @spec initialize_registration(context :: any(), client_id :: String.t(), module :: atom()) ::\n          callback_result :: any()\n  defwithclientidp initialize_registration(context, client_id, module) do\n    module.registration_initialized(context, new_registration_template(client_idp))\n  end\n\n  @spec register(\n          context :: any(),\n          client_id :: String.t(),\n          registration_params :: registration_params(),\n          confirmation_url_fun :: (token :: String.t() -> confirmation_url :: String.t()),\n          module :: atom()\n        ) :: calback_result :: any()\n  defwithclientidp register(\n                     context,\n                     client_id,\n                     registration_params,\n                     confirmation_url_fun,\n                     module\n                   ) do\n    registration_params =\n      case registration_params[:metadata] do\n        %{} = metadata ->\n          Map.put(\n            registration_params,\n            :metadata,\n            User.user_metadata_filter(%User{}, metadata, client_idp.backend)\n          )\n\n        nil ->\n          registration_params\n      end\n\n    with {:ok, user} <- create_user(client_idp, registration_params),\n         :ok <-\n           maybe_deliver_confirmation_email(\n             client_idp.backend,\n             user,\n             confirmation_url_fun,\n             client_idp\n           ),\n         {:ok, user, session_token} <- maybe_create_session(user, client_idp) do\n      module.user_registered(context, user, session_token)\n    else\n      {:error, %Ecto.Changeset{} = changeset} ->\n        module.registration_failure(context, %RegistrationError{\n          changeset: changeset,\n          message: \"Could not create user with given params.\",\n          template: new_registration_template(client_idp)\n        })\n\n      {:error, reason} ->\n        module.registration_failure(context, %RegistrationError{\n          message: inspect(reason),\n          template: new_confirmation_instructions_template(client_idp)\n        })\n\n      {:user_not_confirmed, user, reason} ->\n        module.registration_failure(context, %RegistrationError{\n          user: user,\n          message: reason,\n          template: new_confirmation_instructions_template(client_idp)\n        })\n    end\n  end\n\n  use BorutaIdentity.PostUserCreationHook\n\n  @decorate post_user_creation_hook([])\n  defp create_user(client_idp, registration_params) do\n    client_impl = IdentityProvider.implementation(client_idp)\n\n    apply(client_impl, :register, [client_idp.backend, registration_params])\n  end\n\n  defp maybe_deliver_confirmation_email(_backend, _user, _confirmation_url_fun, %IdentityProvider{\n         confirmable: false\n       }) do\n    :ok\n  end\n\n  defp maybe_deliver_confirmation_email(backend, user, confirmation_url_fun, %IdentityProvider{\n         confirmable: true\n       }) do\n    with {:ok, _confirmation_token} <-\n           Deliveries.deliver_user_confirmation_instructions(\n             backend,\n             user,\n             confirmation_url_fun\n           ) do\n      :ok\n    end\n  end\n\n  defp maybe_create_session(user, %IdentityProvider{confirmable: true}) do\n    {:user_not_confirmed, user, \"Email confirmation is required to authenticate.\"}\n  end\n\n  defp maybe_create_session(user, %IdentityProvider{confirmable: false}) do\n    Sessions.create_user_session(user)\n  end\n\n  defp new_registration_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(identity_provider.id, :new_registration)\n  end\n\n  defp new_confirmation_instructions_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(\n      identity_provider.id,\n      :new_confirmation_instructions\n    )\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/reset_passwords.ex",
    "content": "defmodule BorutaIdentity.Accounts.ResetPasswordError do\n  @enforce_keys [:message]\n  defexception [:message, :changeset, :token, :template]\n\n  @type t :: %__MODULE__{\n          message: String.t(),\n          token: String.t(),\n          changeset: Ecto.Changeset.t() | nil,\n          template: template :: BorutaIdentity.IdentityProviders.Template.t()\n        }\n\n  def exception(message) when is_binary(message) do\n    %__MODULE__{message: message}\n  end\n\n  def message(exception) do\n    exception.message\n  end\nend\n\ndefmodule BorutaIdentity.Accounts.ResetPasswordApplication do\n  @moduledoc \"\"\"\n  TODO SessionApplication documentation\n  \"\"\"\n\n  @callback password_instructions_initialized(\n              context :: any(),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) :: any()\n\n  @callback reset_password_instructions_delivered(context :: any()) ::\n              any()\n\n  @callback password_reset_initialized(\n              context :: any(),\n              token :: String.t(),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) ::\n              any()\n\n  @callback password_reseted(context :: any(), user :: BorutaIdentity.Accounts.User.t()) ::\n              any()\n\n  @callback password_reset_failure(\n              context :: any(),\n              error :: BorutaIdentity.Accounts.ResetPasswordError.t()\n            ) ::\n              any()\nend\n\ndefmodule BorutaIdentity.Accounts.ResetPasswords do\n  @moduledoc false\n\n  import BorutaIdentity.Accounts.Utils, only: [defwithclientidp: 2]\n\n  alias BorutaIdentity.Accounts.Deliveries\n  alias BorutaIdentity.Accounts.ResetPasswordError\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.Users\n  alias BorutaIdentity.Accounts.UserToken\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.Repo\n\n  @type reset_password_url_fun :: (token :: String.t() -> reset_password_url :: String.t())\n\n  @type reset_password_instructions_params :: %{\n          email: String.t()\n        }\n\n  @type reset_password_params :: %{\n          reset_password_token: String.t(),\n          password: String.t(),\n          password_confirmation: String.t()\n        }\n\n  @callback reset_password(\n              backend :: BorutaIdentity.IdentityProviders.Backend.t(),\n              reset_password_params :: reset_password_params()\n            ) ::\n              {:ok, user :: User.t()} | {:error, reason :: String.t() | Ecto.Changeset.t()}\n\n  @spec initialize_password_instructions(\n          context :: any(),\n          client_id :: String.t(),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp initialize_password_instructions(context, client_id, module) do\n    module.password_instructions_initialized(\n      context,\n      new_reset_password_template(client_idp)\n    )\n  end\n\n  @spec send_reset_password_instructions(\n          context :: any(),\n          client_id :: String.t(),\n          reset_password_instructions_params :: reset_password_instructions_params(),\n          reset_password_url_fun :: reset_password_url_fun(),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp send_reset_password_instructions(\n                     context,\n                     client_id,\n                     reset_password_instructions_params,\n                     reset_password_url_fun,\n                     module\n                   ) do\n    with %User{} = user <-\n           Users.get_user_by_email(client_idp.backend, reset_password_instructions_params[:email]) do\n      send_reset_password_instructions(\n        client_idp.backend,\n        user,\n        reset_password_url_fun\n      )\n    end\n\n    # NOTE return a success either reset passowrd instructions email sent or not\n    module.reset_password_instructions_delivered(context)\n  end\n\n  @spec initialize_password_reset(\n          context :: any(),\n          client_id :: String.t(),\n          token :: String.t(),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp initialize_password_reset(\n                     context,\n                     client_id,\n                     token,\n                     module\n                   ) do\n    case reset_password_changeset(client_idp.backend, token) do\n      {:ok, _changeset} ->\n        module.password_reset_initialized(\n          context,\n          token,\n          edit_reset_password_template(client_idp)\n        )\n\n      {:error, reason} ->\n        module.password_reset_failure(context, %ResetPasswordError{\n          message: reason,\n          token: token,\n          template: edit_reset_password_template(client_idp)\n        })\n    end\n  end\n\n  @spec reset_password(\n          context :: any(),\n          client_id :: String.t(),\n          reset_password_params :: reset_password_params(),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp reset_password(\n                     context,\n                     client_id,\n                     reset_password_params,\n                     module\n                   ) do\n    client_impl = IdentityProvider.implementation(client_idp)\n    edit_template = edit_reset_password_template(client_idp)\n    token = reset_password_params.reset_password_token\n\n    with {:ok, user} <- get_user_by_reset_password_token(token),\n         # TODO wrap password reset and token revocation in a transaction\n         {:ok, _user} <- apply(client_impl, :reset_password, [client_idp.backend, reset_password_params]),\n         {:ok, revoke_query} <- UserToken.revoke_email_token_query(token, \"reset_password\"),\n    _deleted <- Repo.update_all(revoke_query, set: [revoked_at: DateTime.utc_now()]) do\n      module.password_reseted(context, user)\n    else\n      {:error, %Ecto.Changeset{} = changeset} ->\n        module.password_reset_failure(context, %ResetPasswordError{\n          token: reset_password_params.reset_password_token,\n          message: \"Could not update user password.\",\n          changeset: changeset,\n          template: edit_template\n        })\n\n      {:error, reason} ->\n        module.password_reset_failure(context, %ResetPasswordError{\n          template: edit_template,\n          token: reset_password_params.reset_password_token,\n          message: reason\n        })\n    end\n  end\n\n  defp send_reset_password_instructions(backend, user, reset_password_url_fun) do\n    {encoded_token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n\n    with {:ok, _user_token} <- Repo.insert(user_token),\n         {:ok, _user_token} <-\n           Deliveries.deliver_user_reset_password_instructions(\n             backend,\n             user,\n             reset_password_url_fun.(encoded_token)\n           ) do\n      :ok\n    end\n  end\n\n  defp reset_password_changeset(_backend, token) do\n    with {:ok, user} <-\n           get_user_by_reset_password_token(token) do\n      {:ok, Ecto.Changeset.change(user)}\n    end\n  end\n\n  defp get_user_by_reset_password_token(token) do\n    with {:ok, query} <- UserToken.verify_email_token_query(token, \"reset_password\"),\n         %User{} = user <- Repo.one(query) do\n      {:ok, user}\n    else\n      _ -> {:error, \"Given reset password token is invalid.\"}\n    end\n  end\n\n  defp new_reset_password_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(identity_provider.id, :new_reset_password)\n  end\n\n  defp edit_reset_password_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(identity_provider.id, :edit_reset_password)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/schemas/consent.ex",
    "content": "defmodule BorutaIdentity.Accounts.Consent do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias BorutaIdentity.Accounts.Internal.User\n\n  @type t :: %__MODULE__{\n          id: String.t(),\n          client_id: String.t(),\n          scopes: list(String.t()),\n          user: Ecto.Association.NotLoaded.t() | User.t(),\n          inserted_at: DateTime.t(),\n          updated_at: DateTime.t()\n        }\n\n  @primary_key {:id, :binary_id, autogenerate: true}\n  @foreign_key_type :binary_id\n  schema \"consents\" do\n    field(:client_id, :string)\n    field(:scopes, {:array, :string}, default: [])\n\n    belongs_to(:user, User)\n\n    timestamps()\n  end\n\n  @doc false\n  def changeset(consent, attrs) do\n    consent\n    |> cast(attrs, [:client_id, :scopes])\n    |> validate_required([:client_id])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/schemas/role.ex",
    "content": "defmodule BorutaIdentity.Accounts.Role do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias BorutaIdentity.Accounts.RoleScope\n  alias BorutaIdentity.Repo\n\n  @primary_key {:id, :binary_id, autogenerate: true}\n  @foreign_key_type :binary_id\n  schema \"roles\" do\n    field :name, :string\n    field :scopes, {:array, :map}, virtual: true\n\n    has_many :role_scopes, RoleScope, on_replace: :delete\n    timestamps()\n  end\n\n  @doc false\n  def changeset(role, attrs) do\n    role\n    |> Repo.preload(:role_scopes)\n    |> cast(attrs, [:id, :name])\n    |> unique_constraint(:id, name: :roles_pkey)\n    |> put_assoc(\n      :role_scopes,\n      (attrs[:scopes] || attrs[\"scopes\"] || [])\n      |> Enum.uniq()\n      |> Enum.map(fn\n        %{id: id} -> %RoleScope{scope_id: id}\n        %{\"id\" => id} -> %RoleScope{scope_id: id}\n        _ -> nil\n      end)\n      |> Enum.reject(&is_nil/1)\n    )\n    |> validate_required([:name])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/schemas/role_scope.ex",
    "content": "defmodule BorutaIdentity.Accounts.RoleScope do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  @primary_key {:id, :binary_id, autogenerate: true}\n  @foreign_key_type :binary_id\n  schema \"roles_scopes\" do\n    field :role_id, Ecto.UUID\n    field :scope_id, Ecto.UUID\n\n    timestamps()\n  end\n\n  @doc false\n  def changeset(role_scope, attrs) do\n    role_scope\n    |> cast(attrs, [:role_id, :scope_id])\n    |> validate_required([:role_id, :scope_id])\n    |> unique_constraint([:role_id, :scope_id], name: \"roles_scopes_role_id_scope_id_index\", error_key: :scopes, message: \"must be unique\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/schemas/user.ex",
    "content": "defmodule BorutaIdentity.Accounts.User do\n  @moduledoc false\n\n  defmodule CoseKey do\n    @moduledoc false\n\n    @behaviour Ecto.Type\n\n    def type, do: :binary\n    def cast(bin), do: {:ok, Base.decode64!(bin) |> :erlang.binary_to_term()}\n    def load(bin), do: {:ok, Base.decode64!(bin) |> :erlang.binary_to_term()}\n    def dump(bin), do: {:ok, :erlang.term_to_binary(bin) |> Base.encode64()}\n    def equal?(a, b), do: a == b\n    def embed_as(_a), do: :self\n  end\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias BorutaIdentity.Accounts.Consent\n  alias BorutaIdentity.Accounts.UserAuthorizedScope\n  alias BorutaIdentity.Accounts.UserRole\n  alias BorutaIdentity.Accounts.UserToken\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.Organizations.OrganizationUser\n  alias BorutaIdentity.Repo\n\n  @type t :: %__MODULE__{\n          id: String.t() | nil,\n          uid: String.t() | nil,\n          username: String.t() | nil,\n          password: String.t() | nil,\n          metadata: map(),\n          federated_metadata: map(),\n          totp_secret: String.t() | nil,\n          webauthn_challenge: String.t() | nil,\n          confirmed_at: DateTime.t() | nil,\n          authorized_scopes: Ecto.Association.NotLoaded.t() | list(UserAuthorizedScope.t()),\n          consents: Ecto.Association.NotLoaded.t() | list(Consent.t()),\n          backend: Ecto.Association.NotLoaded.t() | Backend.t(),\n          backend_id: String.t() | nil,\n          last_login_at: DateTime.t() | nil,\n          inserted_at: DateTime.t() | nil,\n          updated_at: DateTime.t() | nil\n        }\n\n  @metadata_schema %{\n    \"type\" => \"object\",\n    \"properties\" => %{\n      \"value\" => %{},\n      \"status\" => %{\"type\" => \"string\"},\n      \"display\" => %{\"type\" => \"array\", \"items\" => %{\"type\" => \"string\"}}\n    },\n    \"required\" => [\"value\", \"status\"]\n  }\n\n  def account_types, do: [\n    BorutaIdentity.Accounts.Federated.account_type(),\n    BorutaIdentity.Accounts.Internal.account_type(),\n    BorutaIdentity.Accounts.Ldap.account_type()\n  ]\n\n  @derive {Inspect, except: [:password]}\n  @primary_key {:id, Ecto.UUID, autogenerate: true}\n  @foreign_key_type Ecto.UUID\n  schema \"users\" do\n    # TODO add email field\n    field(:username, :string)\n    field(:uid, :string)\n    field(:group, :string)\n    field(:password, :string, virtual: true)\n    field(:confirmed_at, :utc_datetime_usec)\n    field(:last_login_at, :utc_datetime_usec)\n    field(:metadata, :map, default: %{})\n    field(:federated_metadata, :map, default: %{})\n    field(:totp_secret, :string)\n    field(:totp_registered_at, :utc_datetime_usec)\n    field(:webauthn_challenge, :string)\n    field(:webauthn_identifier, :string)\n    field(:webauthn_public_key, CoseKey)\n    field(:webauthn_registered_at, :utc_datetime_usec)\n    field(:account_type, :string)\n\n    has_many(:user_tokens, UserToken)\n    has_many(:authorized_scopes, UserAuthorizedScope)\n    has_many(:roles, UserRole)\n    has_many(:organizations, OrganizationUser)\n    has_many(:consents, Consent, on_replace: :delete)\n    belongs_to(:backend, Backend)\n\n    timestamps()\n  end\n\n  def implementation_changeset(attrs, backend) do\n    %__MODULE__{}\n    |> cast(attrs, [\n      :backend_id,\n      :uid,\n      :username,\n      :group,\n      :metadata,\n      :federated_metadata,\n      :account_type\n    ])\n    |> metadata_template_filter(backend)\n    |> validate_required([:backend_id, :uid, :username, :account_type])\n    |> validate_inclusion(:account_type, account_types())\n    |> validate_metadata()\n    |> validate_group()\n  end\n\n  def changeset(user, attrs \\\\ %{}) do\n    user\n    |> cast(attrs, [:metadata, :group])\n    |> validate_group()\n    |> validate_metadata()\n  end\n\n  def login_changeset(user) do\n    user\n    |> change(last_login_at: DateTime.utc_now())\n    |> validate_required([:backend_id])\n  end\n\n  def confirm_changeset(user) do\n    now = DateTime.utc_now()\n    change(user, confirmed_at: now)\n  end\n\n  def unconfirm_changeset(user) do\n    change(user, confirmed_at: nil)\n  end\n\n  def webauthn_challenge_changeset(user) do\n    change(user, webauthn_challenge: SecureRandom.hex())\n  end\n\n  def webauthn_public_key_changeset(user, cose_key, identifier) do\n    change(user,\n      webauthn_public_key: cose_key,\n      webauthn_registered_at: DateTime.utc_now(),\n      webauthn_identifier: identifier\n    )\n  end\n\n  def totp_changeset(user, totp_secret) do\n    change(user, totp_secret: totp_secret, totp_registered_at: DateTime.utc_now())\n  end\n\n  def consent_changeset(user, attrs) do\n    user\n    |> Repo.preload(:consents)\n    |> cast(attrs, [])\n    |> cast_assoc(:consents, with: &Consent.changeset/2)\n  end\n\n  def confirmed?(%__MODULE__{confirmed_at: nil}), do: false\n  def confirmed?(%__MODULE__{confirmed_at: _confirmed_at}), do: true\n\n  @spec metadata_filter(metadata :: map(), backend :: Backend.t()) :: metadata :: map()\n  def metadata_filter(metadata, %Backend{\n        metadata_fields: metadata_fields\n      }) do\n    Enum.filter(metadata, fn {key, _value} ->\n      attribute_names =\n        Enum.map(metadata_fields, fn %{\"attribute_name\" => attribute_name} -> attribute_name end)\n\n      Enum.member?(attribute_names, key)\n    end)\n    |> Enum.into(%{})\n  end\n\n  @spec user_metadata_filter(user :: t(), metadata :: map(), backend :: Backend.t()) ::\n          metadata :: map()\n  def user_metadata_filter(\n        %__MODULE__{metadata: user_metadata},\n        metadata,\n        %Backend{metadata_fields: metadata_fields} = backend\n      ) do\n    metadata = metadata_filter(metadata, backend)\n\n    metadata_fields\n    |> Enum.map(fn field ->\n      attribute_name = field[\"attribute_name\"]\n      user_editable = field[\"user_editable\"]\n\n      case Enum.find(metadata, fn {key, _value} ->\n             attribute_name == key\n           end) do\n        {key, _value} = field ->\n          case user_editable do\n            true -> field\n            _ -> {attribute_name, user_metadata[key]}\n          end\n\n        nil ->\n          {attribute_name, user_metadata[attribute_name]}\n      end\n    end)\n    |> Enum.reject(fn\n      {_key, nil} -> true\n      nil -> true\n      _ -> false\n    end)\n    |> Enum.map(fn\n      {key, value} when is_map(value) -> {key, value}\n      {key, value} ->\n      # TODO default display\n      {key, %{\"value\" => value, \"status\" => \"valid\", \"display\" => []}}\n    end)\n    |> Enum.into(%{})\n  end\n\n  # TODO check metadata schema\n  defp metadata_template_filter(\n         %Ecto.Changeset{changes: %{metadata: %{} = metadata}} = changeset,\n         backend\n       )\n       when not (map_size(metadata) == 0) do\n    put_change(changeset, :metadata, metadata_filter(metadata, backend))\n  end\n\n  defp metadata_template_filter(changeset, _backend), do: changeset\n\n  defp validate_group(changeset) do\n    case Ecto.Changeset.get_change(changeset, :group) do\n      nil ->\n        changeset\n\n      group ->\n        groups = String.split(group, \" \")\n\n        case groups == Enum.uniq(groups) do\n          true ->\n            changeset\n\n          false ->\n            %{\n              changeset\n              | valid?: false,\n                errors: [{:group, {\"must be unique\", []}} | changeset.errors]\n            }\n        end\n    end\n  end\n\n  defp validate_metadata(\n         %Ecto.Changeset{changes: %{metadata: metadata}} = changeset\n       ) do\n    Enum.reduce_while(metadata, changeset, fn {_attribute, value}, changeset ->\n      case ExJsonSchema.Validator.validate(@metadata_schema, value) do\n        :ok ->\n          {:cont, changeset}\n\n        {:error, errors} ->\n          {:halt, Enum.reduce(errors, changeset, fn {message, path}, changeset ->\n            add_error(changeset, :metadata, \"#{message} at #{path}\")\n          end)}\n      end\n    end)\n  end\n\n  defp validate_metadata(changeset), do: changeset\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/schemas/user_authorized_scope.ex",
    "content": "defmodule BorutaIdentity.Accounts.UserAuthorizedScope do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias BorutaIdentity.Accounts.User\n\n  @type t :: %__MODULE__{\n          user: Ecto.Association.NotLoaded.t() | User.t(),\n          scope_id: String.t(),\n          inserted_at: DateTime.t(),\n          updated_at: DateTime.t()\n        }\n\n  @primary_key {:id, :binary_id, autogenerate: true}\n  @foreign_key_type :binary_id\n  schema \"users_authorized_scopes\" do\n    field(:scope_id, :string)\n    belongs_to(:user, User)\n\n    timestamps()\n  end\n\n  @doc false\n  def changeset(scope, attrs) do\n    scope\n    |> cast(attrs, [:scope_id, :user_id])\n    |> validate_required([:scope_id, :user_id])\n    |> unique_constraint([:scope_id, :user_id])\n    |> foreign_key_constraint(:user_id)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/schemas/user_role.ex",
    "content": "defmodule BorutaIdentity.Accounts.UserRole do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias BorutaIdentity.Accounts.Role\n  alias BorutaIdentity.Accounts.User\n\n  @type t :: %__MODULE__{\n    id: String.t(),\n    user_id: String.t(),\n    role_id: String.t()\n  }\n\n  @primary_key {:id, :binary_id, autogenerate: true}\n  @foreign_key_type :binary_id\n  schema \"roles_users\" do\n    belongs_to(:role, Role)\n    belongs_to(:user, User)\n\n    timestamps()\n  end\n\n  @doc false\n  def changeset(user_role, attrs) do\n    user_role\n    |> cast(attrs, [:role_id, :user_id])\n    |> validate_required([:role_id, :user_id])\n    |> unique_constraint([:role_id, :user_id], name: \"roles_users_role_id_user_id_index\", error_key: :users, message: \"must be unique\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/schemas/user_token.ex",
    "content": "defmodule BorutaIdentity.Accounts.UserToken do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Query\n\n  alias BorutaIdentity.Accounts.User\n\n  @hash_algorithm :sha256\n  @rand_size 32\n\n  # It is very important to keep the reset password token expiry short,\n  # since someone with access to the email may take over the account.\n  @reset_password_validity_in_days 1\n  @confirm_validity_in_days 7\n  @change_email_validity_in_days 7\n  @session_validity_in_days 60\n\n  @primary_key {:id, Ecto.UUID, autogenerate: true}\n  @foreign_key_type Ecto.UUID\n  schema \"users_tokens\" do\n    field :token, :binary\n    field :context, :string\n    field :sent_to, :string\n    field :revoked_at, :utc_datetime_usec\n    belongs_to :user, BorutaIdentity.Accounts.User\n\n    timestamps(updated_at: false)\n  end\n\n  @doc \"\"\"\n  Generates a token that will be stored in a signed place,\n  such as session or cookie. As they are signed, those\n  tokens do not need to be hashed.\n  \"\"\"\n  def build_session_token(user) do\n    token = :crypto.strong_rand_bytes(@rand_size)\n\n    {token,\n     %BorutaIdentity.Accounts.UserToken{token: token, context: \"session\", user_id: user.id}}\n  end\n\n  @doc \"\"\"\n  Checks if the token is valid and returns its underlying lookup query.\n\n  The query returns the user found by the token.\n  \"\"\"\n  def verify_session_token_query(token) do\n    query =\n      from u in User,\n        join: t in assoc(u, :user_tokens),\n        join: b in assoc(u, :backend),\n        where: t.token == ^token,\n        where: t.context == \"session\",\n        where: t.inserted_at > ago(@session_validity_in_days, \"day\"),\n        preload: [backend: b]\n\n    {:ok, query}\n  end\n\n  @doc \"\"\"\n  Builds a token with a hashed counter part.\n\n  The non-hashed token is sent to the user email while the\n  hashed part is stored in the database, to avoid reconstruction.\n  The token is valid for a week as long as users don't change\n  their email.\n  \"\"\"\n  def build_email_token(user, context) do\n    build_hashed_token(user, context, user.username)\n  end\n\n  defp build_hashed_token(user, context, sent_to) do\n    token = :crypto.strong_rand_bytes(@rand_size)\n    hashed_token = :crypto.hash(@hash_algorithm, token)\n\n    {Base.url_encode64(token, padding: false),\n     %BorutaIdentity.Accounts.UserToken{\n       token: hashed_token,\n       context: context,\n       sent_to: sent_to,\n       user_id: user.id\n     }}\n  end\n\n  @doc \"\"\"\n  Checks if the token is valid and returns its underlying lookup query.\n\n  The query returns the user found by the token.\n  \"\"\"\n  def verify_email_token_query(token, context) do\n    case Base.url_decode64(token, padding: false) do\n      {:ok, decoded_token} ->\n        hashed_token = :crypto.hash(@hash_algorithm, decoded_token)\n        days = days_for_context(context)\n\n        query =\n          from token in token_and_context_query(hashed_token, context),\n            join: user in assoc(token, :user),\n            where:\n              token.inserted_at > ago(^days, \"day\") and\n                token.sent_to == user.username and\n                is_nil(token.revoked_at),\n            select: user\n\n        {:ok, query}\n\n      :error ->\n        :error\n    end\n  end\n\n  def revoke_email_token_query(token, context) do\n    case Base.url_decode64(token, padding: false) do\n      {:ok, decoded_token} ->\n        hashed_token = :crypto.hash(@hash_algorithm, decoded_token)\n\n        {:ok, token_and_context_query(hashed_token, context)}\n      _error ->\n        {:error, \"Could not revoke given token.\"}\n    end\n  end\n\n  defp days_for_context(\"confirm\"), do: @confirm_validity_in_days\n  defp days_for_context(\"reset_password\"), do: @reset_password_validity_in_days\n\n  @doc \"\"\"\n  Checks if the token is valid and returns its underlying lookup query.\n\n  The query returns the user token record.\n  \"\"\"\n  def verify_confirm_email_token_query(token, context) do\n    case Base.url_decode64(token, padding: false) do\n      {:ok, decoded_token} ->\n        hashed_token = :crypto.hash(@hash_algorithm, decoded_token)\n\n        query =\n          from token in token_and_context_query(hashed_token, context),\n            where: token.inserted_at > ago(@change_email_validity_in_days, \"day\")\n\n        {:ok, query}\n\n      :error ->\n        :error\n    end\n  end\n\n  @doc \"\"\"\n  Returns the given token with the given context.\n  \"\"\"\n  def token_and_context_query(token, context) do\n    from BorutaIdentity.Accounts.UserToken, where: [token: ^token, context: ^context]\n  end\n\n  @doc \"\"\"\n  Gets all tokens for the given user for the given contexts.\n  \"\"\"\n  def user_and_contexts_query(user, :all) do\n    from t in BorutaIdentity.Accounts.UserToken, where: t.user_id == ^user.id\n  end\n\n  def user_and_contexts_query(user, [_ | _] = contexts) do\n    from t in BorutaIdentity.Accounts.UserToken,\n      where: t.user_id == ^user.id and t.context in ^contexts\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/sessions.ex",
    "content": "defmodule BorutaIdentity.Accounts.SessionError do\n  @enforce_keys [:message]\n  defexception [:message, :changeset, :template]\n\n  @type t :: %__MODULE__{\n          message: String.t(),\n          changeset: Ecto.Changeset.t() | nil,\n          template: BorutaIdentity.IdentityProviders.Template.t()\n        }\n\n  def exception(message) when is_binary(message) do\n    %__MODULE__{message: message}\n  end\n\n  def message(exception) do\n    exception.message\n  end\nend\n\ndefmodule BorutaIdentity.Accounts.SessionApplication do\n  @moduledoc \"\"\"\n  TODO SessionApplication documentation\n  \"\"\"\n\n  @callback session_initialized(\n              context :: any(),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) :: any()\n\n  @callback user_authenticated(\n              context :: any(),\n              user :: BorutaIdentity.Accounts.User.t(),\n              session_token :: String.t()\n            ) ::\n              any()\n\n  @callback authentication_failure(\n              context :: any(),\n              error :: BorutaIdentity.Accounts.SessionError.t()\n            ) ::\n              any()\n\n  @callback session_deleted(context :: any()) :: any()\nend\n\ndefmodule BorutaIdentity.Accounts.Sessions do\n  @moduledoc false\n\n  import BorutaIdentity.Accounts.Utils, only: [defwithclientidp: 2]\n\n  alias BorutaIdentity.Accounts.SessionError\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.UserToken\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.Repo\n\n  @type user_params :: %{\n          email: String.t()\n        }\n\n  @type authentication_params :: %{\n          email: String.t(),\n          password: String.t()\n        }\n\n  # TODO rename to fetch_user\n  @callback get_user(backend :: Backend.t(), user_params :: user_params()) ::\n              {:ok, impl_user :: any()} | {:error, reason :: String.t()}\n\n  @callback domain_user!(implementation_user :: any(), backend :: Backend.t()) ::\n              user :: User.t()\n\n  @callback check_user_against(\n              backend :: Backend.t(),\n              impl_user :: any(),\n              authentication_params :: authentication_params()\n            ) ::\n              {:ok, user :: User.t()} | {:error, reason :: String.t()}\n\n  # NOTE duplicate of BorutaIdentity.Accounts.Registrations\n  # @callback register(\n  #             backend :: BorutaIdentity.IdentityProviders.Backend.t(),\n  #             registration_params :: registration_params()\n  #           ) ::\n  #             {:ok, user :: User.t()}\n  #             | {:error, changeset :: Ecto.Changeset.t()}\n\n  @spec initialize_session(\n          context :: any(),\n          client_id :: String.t(),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp initialize_session(context, client_id, module) do\n    module.session_initialized(context, new_session_template(client_idp))\n  end\n\n  @spec create_session(\n          context :: any(),\n          client_id :: String.t(),\n          authentication_params :: authentication_params(),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp create_session(context, client_id, authentication_params, module) do\n    with {:ok, user} <- get_user(authentication_params, client_idp),\n         {:ok, user} <- maybe_check_password(user, authentication_params, client_idp),\n         :ok <- ensure_user_confirmed(user, client_idp),\n         {:ok, user, session_token} <- create_user_session(user) do\n      module.user_authenticated(context, user, session_token)\n    else\n      {:error, _reason} ->\n        module.authentication_failure(context, %SessionError{\n          template: new_session_template(client_idp),\n          message: \"Invalid email or password.\"\n        })\n\n      {:user_not_confirmed, reason} ->\n        module.authentication_failure(context, %SessionError{\n          template: new_confirmation_instructions_template(client_idp),\n          message: reason\n        })\n    end\n  end\n\n  def get_user(%{email: email}, %IdentityProvider{check_password: false} = client_idp) do\n    client_impl = IdentityProvider.implementation(client_idp)\n\n    apply(client_impl, :register, [client_idp.backend, %{\n      email: email,\n      password: SecureRandom.hex(),\n      metadata: %{\"check_password\" => %{\"value\" => false, \"display\" => [], \"status\" => \"valid\"}}\n    }])\n  end\n\n  def get_user(authentication_params, %IdentityProvider{check_password: true} = client_idp) do\n    client_impl = IdentityProvider.implementation(client_idp)\n\n    apply(client_impl, :get_user, [client_idp.backend, authentication_params])\n  end\n\n  def maybe_check_password(user, _authentication_params, %IdentityProvider{check_password: false}), do: {:ok, user}\n\n  def maybe_check_password(user, authentication_params, %IdentityProvider{backend: backend, check_password: true} = client_idp) do\n    client_impl = IdentityProvider.implementation(client_idp)\n\n    with {:ok, user} <- apply(client_impl, :check_user_against, [\n      backend,\n      user,\n      authentication_params\n    ]) do\n      {:ok, apply(client_impl, :domain_user!, [user, client_idp.backend])}\n    end\n  end\n\n  @spec delete_session(\n          context :: any(),\n          client_id :: String.t(),\n          session_token :: String.t(),\n          module :: atom()\n        ) ::\n          callback_result :: any()\n  def delete_session(context, _client_id, session_token, module) do\n    case delete_session(session_token) do\n      :ok ->\n        module.session_deleted(context)\n\n      {:error, \"Session not found.\"} ->\n        module.session_deleted(context)\n    end\n  end\n\n  @spec create_user_session(user :: User.t()) ::\n          {:ok, user :: User.t(), session_token :: String.t()}\n          | {:error, changeset :: Ecto.Changeset.t()}\n  def create_user_session(%User{} = user) do\n    with {_token, user_token} <- UserToken.build_session_token(user),\n         {:ok, session_token} <- Repo.insert(user_token),\n         {:ok, user} <- User.login_changeset(user) |> Repo.update() do\n      {:ok, user, session_token.token}\n    end\n  end\n\n  defp ensure_user_confirmed(_user, %IdentityProvider{confirmable: false}), do: :ok\n\n  defp ensure_user_confirmed(user, %IdentityProvider{confirmable: true}) do\n    case User.confirmed?(user) do\n      true -> :ok\n      false -> {:user_not_confirmed, \"Email confirmation is required to authenticate.\"}\n    end\n  end\n\n  defp new_session_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(identity_provider.id, :new_session)\n  end\n\n  defp new_confirmation_instructions_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(\n      identity_provider.id,\n      :new_confirmation_instructions\n    )\n  end\n\n  def delete_session(nil), do: {:error, \"Session not found.\"}\n\n  def delete_session(session_token) do\n    case Repo.delete_all(UserToken.token_and_context_query(session_token, \"session\")) do\n      {1, _} -> :ok\n      {_, _} -> {:error, \"Session not found.\"}\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/settings.ex",
    "content": "defmodule BorutaIdentity.Accounts.SettingsError do\n  @enforce_keys [:message]\n  defexception [:message, :changeset, :template]\n\n  @type t :: %__MODULE__{\n          message: String.t(),\n          changeset: Ecto.Changeset.t() | nil,\n          template: BorutaIdentity.IdentityProviders.Template.t()\n        }\n\n  def exception(message) when is_binary(message) do\n    %__MODULE__{message: message}\n  end\n\n  def message(exception) do\n    exception.message\n  end\nend\n\ndefmodule BorutaIdentity.Accounts.SettingsApplication do\n  @moduledoc false\n\n  @callback edit_user_initialized(\n              context :: any(),\n              user :: BorutaIdentity.Accounts.User.t(),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) :: any()\n\n  @callback user_updated(\n              context :: any(),\n              user :: BorutaIdentity.Accounts.User.t()\n            ) :: any()\n\n  @callback user_update_failure(\n              context :: any(),\n              error :: BorutaIdentity.Accounts.SettingsError.t()\n            ) :: any()\n\n  @callback user_destroyed(\n              context :: any(),\n              user :: BorutaIdentity.Accounts.User.t()\n            ) :: any()\n\n  @callback user_destroy_failure(\n              context :: any(),\n              error :: BorutaIdentity.Accounts.SettingsError.t()\n            ) :: any()\nend\n\ndefmodule BorutaIdentity.Accounts.Settings do\n  @moduledoc false\n\n  import BorutaIdentity.Accounts.Utils, only: [defwithclientidp: 2]\n\n  alias BorutaIdentity.Accounts.Deliveries\n  alias BorutaIdentity.Accounts.SettingsError\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.Repo\n\n  @type user_update_params :: %{\n          :current_password => String.t(),\n          optional(:email) => String.t(),\n          optional(:password) => String.t(),\n          optional(:metadata) => map()\n        }\n\n  @type authentication_params :: %{\n          password: String.t()\n        }\n\n  @callback update_user(\n              backend :: BorutaIdentity.IdentityProviders.Backend.t(),\n              impl_user :: any(),\n              user_update_params :: user_update_params()\n            ) ::\n              {:ok, user :: User.t()} | {:error, changeset :: Ecto.Changeset.t()}\n\n  # NOTE emits a compilation warning since callback is already defined in BorutaIdentity.Accounts.Sessions\n  # @callback check_user_against(\n  #             backend :: Backend.t(),\n  #             user :: User.t(),\n  #             authentication_params :: authentication_params(),\n  #             identity_provider :: IdentityProvider.t()\n  #           ) ::\n  #             {:ok, user :: User.t()} | {:error, reason :: String.t()}\n\n  @callback delete_user(id :: String.t()) :: :ok | {:error, reason :: String.t()}\n\n  @spec initialize_edit_user(\n          context :: any(),\n          client_id :: String.t(),\n          user :: User.t(),\n          module :: atom()\n        ) ::\n          callback_result :: any()\n  defwithclientidp initialize_edit_user(context, client_id, user, module) do\n    module.edit_user_initialized(context, user, edit_user_template(client_idp))\n  end\n\n  @spec update_user(\n          context :: any(),\n          client_id :: String.t(),\n          user :: User.t(),\n          user_update_params :: user_update_params(),\n          confirmation_url_fun :: (token :: String.t() -> confirmation_url :: String.t()),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp update_user(\n                     context,\n                     client_id,\n                     user,\n                     user_update_params,\n                     confirmation_url_fun,\n                     module\n                   ) do\n    client_impl = IdentityProvider.implementation(client_idp)\n    # TODO remove implementation_user from domain\n\n    user_update_params =\n      case user_update_params[:metadata] do\n        %{} = metadata ->\n          Map.put(\n            user_update_params,\n            :metadata,\n            User.user_metadata_filter(user, metadata, client_idp.backend)\n          )\n\n        nil ->\n          user_update_params\n      end\n\n    with {:ok, old_user} <-\n           apply(client_impl, :get_user, [client_idp.backend, %{email: user.username}]),\n         {:ok, _user} <-\n           apply(client_impl, :check_user_against, [\n             client_idp.backend,\n             old_user,\n             %{password: user_update_params[:current_password]}\n           ]),\n         # TODO wrap user update and confirmation email sending in a transaction\n         {:ok, user} <-\n           apply(client_impl, :update_user, [client_idp.backend, old_user, user_update_params]),\n         {:ok, user} <- maybe_unconfirm_user(old_user, user, client_idp),\n         :ok <-\n           maybe_deliver_email_confirmation_instructions(\n             client_idp.backend,\n             old_user,\n             user,\n             confirmation_url_fun,\n             client_idp\n           ) do\n      module.user_updated(context, user)\n    else\n      {:error, %Ecto.Changeset{} = changeset} ->\n        module.user_update_failure(context, %SettingsError{\n          template: edit_user_template(client_idp),\n          message: \"Could not update user with given params.\",\n          changeset: changeset\n        })\n\n      {:error, reason} ->\n        module.user_update_failure(context, %SettingsError{\n          template: edit_user_template(client_idp),\n          message: reason\n        })\n    end\n  end\n\n  @spec destroy_user(\n          context :: any(),\n          client_id :: String.t(),\n          user :: User.t(),\n          module :: atom()\n        ) ::\n          callback_result :: any() | {:error, reason :: String.t()} | {:error, Ecto.Changeset.t()}\n  defwithclientidp destroy_user(context, client_id, user, module) do\n    client_impl = IdentityProvider.implementation(client_idp)\n\n    with :ok <- apply(client_impl, :delete_user, [user.uid]),\n         {:ok, user} <- Repo.delete(user) do\n      module.user_destroyed(context, user)\n    else\n      {:error, _error} ->\n        module.user_destroy_failure(context, %SettingsError{\n          template: edit_user_template(client_idp),\n          message: \"User could not be deleted, please contact an administrator.\",\n        })\n    end\n  end\n\n  defp maybe_unconfirm_user(old_user, user, %IdentityProvider{confirmable: true}) do\n    case email_changed?(old_user, user) do\n      true -> User.unconfirm_changeset(user) |> Repo.update()\n      false -> {:ok, user}\n    end\n  end\n\n  defp maybe_unconfirm_user(_old_user, user, %IdentityProvider{confirmable: false}) do\n    {:ok, user}\n  end\n\n  defp maybe_deliver_email_confirmation_instructions(\n         _backend,\n         _old_user,\n         _user,\n         _confirmation_url_fun,\n         %IdentityProvider{confirmable: false}\n       ) do\n    :ok\n  end\n\n  defp maybe_deliver_email_confirmation_instructions(\n         backend,\n         old_user,\n         user,\n         confirmation_url_fun,\n         %IdentityProvider{confirmable: true}\n       ) do\n    case email_changed?(old_user, user) do\n      true ->\n        with {:ok, _confirmation_token} <-\n               Deliveries.deliver_user_confirmation_instructions(\n                 backend,\n                 user,\n                 confirmation_url_fun\n               ) do\n          :ok\n        end\n\n      false ->\n        :ok\n    end\n  end\n\n  defp email_changed?(%{email: email}, %User{username: email}), do: false\n  defp email_changed?(%{email: _email}, %User{username: nil}), do: false\n  defp email_changed?(_user, _user_update_params), do: true\n\n  defp edit_user_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(identity_provider.id, :edit_user)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/users.ex",
    "content": "defmodule BorutaIdentity.Accounts.Users do\n  @moduledoc false\n\n  import Ecto.Query\n\n  alias Boruta.Ecto.Scopes\n  alias Boruta.Oauth.Scope\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.UserAuthorizedScope\n  alias BorutaIdentity.Accounts.UserRole\n  alias BorutaIdentity.Accounts.UserToken\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.IdentityProviders.BackendRole\n  alias BorutaIdentity.Organizations.OrganizationUser\n  alias BorutaIdentity.Repo\n\n  @spec get_user_by_email(backend :: Backend.t(), email :: String.t()) :: user :: User.t() | nil\n  def get_user_by_email(backend, email) when is_binary(email) do\n    # TODO remove backend_user from domain\n    case apply(\n           Backend.implementation(backend),\n           :get_user,\n           [backend, %{email: email}]\n         ) do\n      {:ok, backend_user} ->\n        apply(\n          Backend.implementation(backend),\n          :domain_user!,\n          [backend_user, backend]\n        )\n\n      _ ->\n        nil\n    end\n  end\n\n  @spec get_user(id :: String.t()) :: user :: User.t() | nil\n  def get_user(id) when is_binary(id) do\n    Repo.one(\n      from(u in User,\n        left_join: as in assoc(u, :authorized_scopes),\n        left_join: b in assoc(u, :backend),\n        preload: [authorized_scopes: as, backend: b],\n        where: u.id == ^id\n      )\n    )\n  end\n\n  def get_user(_), do: nil\n\n  @doc \"\"\"\n  Gets the user with the given signed token.\n  \"\"\"\n  @spec get_user_by_session_token(token :: String.t()) :: user :: User.t() | nil\n  def get_user_by_session_token(token) do\n    {:ok, query} = UserToken.verify_session_token_query(token)\n    Repo.one(query)\n  end\n\n  @spec get_user_scopes(user_id :: String.t()) :: user :: list(UserAuthorizedScope.t()) | nil\n  def get_user_scopes(user_id) do\n    scopes = Scopes.all()\n\n    Repo.all(from(u in UserAuthorizedScope, where: u.user_id == ^user_id))\n    |> Enum.map(fn user_scope ->\n      Enum.find(scopes, fn %{id: id} -> id == user_scope.scope_id end)\n    end)\n    |> Enum.flat_map(fn\n      %{id: id, name: name} -> [%Scope{id: id, name: name}]\n      _ -> []\n    end)\n  end\n\n  @spec get_user_roles(user_id :: String.t()) ::\n          user :: list(BackendRole.t() | UserRole.t()) | nil\n  def get_user_roles(user_id) do\n    scopes = Scopes.all()\n\n    (Repo.all(\n       from(ur in UserRole,\n         left_join: r in assoc(ur, :role),\n         left_join: rs in assoc(r, :role_scopes),\n         where: ur.user_id == ^user_id,\n         preload: [role: {r, [role_scopes: rs]}]\n       )\n     ) ++\n       Repo.all(\n         from(br in BackendRole,\n           left_join: b in assoc(br, :backend),\n           left_join: r in assoc(br, :role),\n           left_join: u in assoc(b, :users),\n           left_join: rs in assoc(r, :role_scopes),\n           where: u.id == ^user_id,\n           preload: [role: {r, [role_scopes: rs]}]\n         )\n       ))\n    |> Enum.uniq_by(fn %{role: role} -> role end)\n    |> Enum.map(fn %{role: role} ->\n      %{\n        role\n        | scopes:\n            role.role_scopes\n            |> Enum.map(fn role_scope ->\n              Enum.find(scopes, fn %{id: id} -> id == role_scope.scope_id end)\n            end)\n            |> Enum.flat_map(fn\n              %{id: id, name: name} -> [%Scope{id: id, name: name}]\n              _ -> []\n            end)\n      }\n    end)\n  end\n\n  @spec get_user_organizations(user_id :: String.t()) :: user :: list(OrganizationUser.t()) | nil\n  def get_user_organizations(user_id) do\n    Repo.all(\n      from(ou in OrganizationUser,\n        left_join: o in assoc(ou, :organization),\n        where: ou.user_id == ^user_id,\n        preload: [organization: o]\n      )\n    )\n    |> Enum.map(fn %{organization: organization} -> organization end)\n  end\n\n  @spec put_user_webauthn_challenge(user :: User.t()) ::\n          {:ok, user :: User.t()} | {:error, changset :: Ecto.Changeset.t()}\n  def put_user_webauthn_challenge(user) do\n    user\n    |> User.webauthn_challenge_changeset()\n    |> Repo.update()\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/verifiable_credentials.ex",
    "content": "defmodule BorutaIdentity.Accounts.VerifiableCredentials do\n  @moduledoc false\n\n  alias Boruta.Oauth.Client\n  alias Boruta.Oauth.Scope\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.Repo\n\n  @authorization_details [\n    %{\n      \"type\" => \"openid_credential\",\n      \"format\" => \"jwt_vc_json\",\n      \"credential_definition\" => %{\n        \"type\" => [\n          \"VerifiableCredential\",\n          \"BorutaCredential\"\n        ]\n      },\n      \"credential_identifiers\" => [\n        \"FederatedAttributes\"\n      ]\n    }\n  ]\n\n  def credentials do\n    Enum.flat_map(@authorization_details, fn detail ->\n      detail[\"credential_definition\"][\"type\"]\n    end)\n    |> Enum.uniq()\n  end\n\n  def credentials_supported do\n    Repo.all(Backend)\n    |> Enum.flat_map(fn %Backend{verifiable_credentials: credentials} ->\n      credentials\n      |> Enum.reject(fn credential -> credential[\"version\"] != \"11\" end)\n      |> Enum.map(fn credential ->\n        %{\n          \"id\" => credential[\"credential_identifier\"],\n          \"types\" => String.split(credential[\"types\"], \" \"),\n          \"display\" => [Map.put(credential[\"display\"], \"locale\", \"en-US\")],\n          \"format\" => credential[\"format\"],\n          \"claims\" => Enum.map(credential[\"claims\"], fn %{\"name\" => name} -> name end),\n          \"cryptographic_binding_methods_supported\" => [\n            \"did:example\"\n          ]\n        }\n      end)\n    end)\n  end\n\n  def credential_configurations_supported do\n    Repo.all(Backend)\n    |> Enum.flat_map(fn %Backend{verifiable_credentials: credentials} ->\n      credentials\n      |> Enum.reject(fn credential -> credential[\"version\"] && credential[\"version\"] != \"13\" end)\n      |> Enum.map(fn\n        %{\"format\" => \"vc+sd-jwt\"} = credential ->\n          {credential[\"credential_identifier\"],\n           %{\n             \"format\" => credential[\"format\"],\n             \"scope\" => credential[\"credential_identifier\"],\n             \"cryptographic_binding_methods_supported\" => [\n               \"did:jwk\",\n               \"did:key\"\n             ],\n             \"credential_signing_alg_values_supported\" => Client.Crypto.signature_algorithms(),\n             \"proof_types_supported\" => %{\n               \"jwt\" => %{\n                 \"proof_signing_alg_values_supported\" => [\n                    \"ES256\",\n                    \"EdDSA\"\n                  ]\n                }\n             },\n             \"vct\" => credential[\"credential_identifier\"],\n             \"display\" => [\n               credential[\"display\"]\n               |> Map.put(\"locale\", \"en-US\")\n               |> Map.merge(\n                 %{\"logo\" => %{\"uri\" => credential[\"display\"][\"logo\"][\"url\"]}},\n                 fn _k, a, b -> Map.merge(a, b) end\n               )\n             ],\n             \"claims\" =>\n               Enum.map(credential[\"claims\"], fn claim ->\n                 {claim[\"name\"], %{\"display\" => [%{\"name\" => claim[\"label\"]}]}}\n               end)\n               |> Enum.into(%{})\n           }}\n\n        credential ->\n          {credential[\"credential_identifier\"],\n           %{\n             \"format\" => credential[\"format\"],\n             # TODO add scope to backends vc configuration\n             \"scope\" => credential[\"credential_identifier\"],\n             \"cryptographic_binding_methods_supported\" => [\n               \"did:jwk\",\n               \"did:key\"\n             ],\n             \"credential_signing_alg_values_supported\" => Client.Crypto.signature_algorithms(),\n             \"credential_definition\" => %{\n               \"type\" => String.split(credential[\"types\"], \" \"),\n               \"credentialSubject\" =>\n                 Enum.map(credential[\"claims\"], fn claim ->\n                   {claim[\"name\"], [%{\"name\" => claim[\"label\"]}]}\n                 end)\n                 |> Enum.into(%{})\n             },\n             \"display\" => [Map.put(credential[\"display\"], \"locale\", \"en-US\")]\n           }}\n      end)\n    end)\n    |> Enum.into(%{})\n  end\n\n  def authorization_details(%User{backend: %Backend{} = backend}, scope) do\n    ((Enum.map(backend.verifiable_credentials, fn credential ->\n      case (credential[\"scopes\"] || []) -- Scope.split(scope) do\n        [] ->\n          case credential[\"type\"] do\n            \"11\" ->\n              %{\n                \"type\" => \"openid_credential\",\n                \"format\" => credential[\"format\"],\n                \"credential_definition\" => %{\n                  \"type\" => String.split(credential[\"types\"], \" \")\n                },\n                \"credential_identifiers\" => [credential[\"credential_identifier\"]]\n              }\n\n            _ ->\n              %{\n                \"type\" => \"openid_credential\",\n                \"format\" => credential[\"format\"],\n                \"credential_configuration_id\" => credential[\"credential_identifier\"],\n                \"credential_identifiers\" => String.split(credential[\"types\"], \" \")\n              }\n          end\n        _scopes ->\n          nil\n      end\n    end) |> Enum.reject(&is_nil/1)) ++ default_authorization_details(scope)) |> Enum.uniq()\n  end\n\n  def authorization_details(_user, scope), do: default_authorization_details(scope)\n\n  def default_authorization_details(scope) do\n    Enum.map(Backend.default!().verifiable_credentials, fn credential ->\n      case (credential[\"scopes\"] || []) -- Scope.split(scope) do\n        [] ->\n          case credential[\"type\"] do\n            \"11\" ->\n              %{\n                \"type\" => \"openid_credential\",\n                \"format\" => credential[\"format\"],\n                \"credential_definition\" => %{\n                  \"type\" => String.split(credential[\"types\"], \" \")\n                },\n                \"credential_identifiers\" => [credential[\"credential_identifier\"]]\n              }\n\n            _ ->\n              %{\n                \"type\" => \"openid_credential\",\n                \"format\" => credential[\"format\"],\n                \"credential_configuration_id\" => credential[\"credential_identifier\"],\n                \"credential_identifiers\" => String.split(credential[\"types\"], \" \")\n              }\n          end\n        _scopes ->\n          nil\n      end\n    end) |> Enum.reject(&is_nil/1)\n  end\n\n  def public_credential_configuration do\n    backend = Backend.default!()\n\n    Enum.map(backend.verifiable_credentials, fn credential ->\n      {credential[\"credential_identifier\"],\n       %{\n         version: credential[\"version\"] || \"13\",\n         vct: credential[\"vct\"],\n         types: String.split(credential[\"types\"], \" \"),\n         format: credential[\"format\"],\n         time_to_live: credential[\"time_to_live\"] || 31_536_000,\n         scopes: credential[\"scopes\"],\n         claims:\n           case credential[\"claims\"] do\n             claim when is_binary(claim) -> String.split(claim, \" \")\n             claims when is_list(claims) -> claims\n           end\n       }}\n    end)\n    |> Enum.into(%{})\n  end\n\n  def credential_configuration(%User{backend: %Backend{} = backend}) do\n    Enum.map(backend.verifiable_credentials, fn credential ->\n      {credential[\"credential_identifier\"],\n       %{\n         version: credential[\"version\"] || \"13\",\n         vct: credential[\"vct\"],\n         types: String.split(credential[\"types\"], \" \"),\n         format: credential[\"format\"],\n         defered: credential[\"defered\"],\n         time_to_live: credential[\"time_to_live\"] || 31_536_000,\n         scopes: credential[\"scopes\"],\n         claims:\n           case credential[\"claims\"] do\n             claim when is_binary(claim) -> String.split(claim, \" \")\n             claims when is_list(claims) -> claims\n           end\n       }}\n    end)\n    |> Enum.into(%{})\n    |> Map.merge(public_credential_configuration())\n  end\n\n  def credential_configuration(_user), do: public_credential_configuration()\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts/verifiable_presentations.ex",
    "content": "defmodule BorutaIdentity.Accounts.VerifiablePresentations do\n  @moduledoc false\n\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  def presentation_configuration(%User{backend: %Backend{} = backend}) do\n    Enum.map(backend.verifiable_presentations, fn presentation ->\n      {presentation[\"presentation_identifier\"], %{\n        definition: Jason.decode!(presentation[\"presentation_definition\"])\n      }}\n    end)\n    |> Enum.into(%{})\n    |> Map.merge(public_presentation_configuration())\n  end\n\n  def presentation_configuration(_user) do\n    public_presentation_configuration()\n  end\n\n  def public_presentation_configuration do\n    backend = Backend.default!()\n\n    Enum.map(backend.verifiable_presentations, fn presentation ->\n      {presentation[\"presentation_identifier\"], %{\n        definition: Jason.decode!(presentation[\"presentation_definition\"])\n      }}\n    end)\n    |> Enum.into(%{})\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/accounts.ex",
    "content": "defmodule BorutaIdentity.Accounts.Utils do\n  @moduledoc false\n\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n\n  @spec client_identity_provider(client_id :: String.t() | nil) ::\n          {:ok, identity_provider :: IdentityProvider.t()} | {:error, reason :: String.t()}\n  def client_identity_provider(nil), do: {:error, \"Client identifier not provided.\"}\n\n  def client_identity_provider(client_id) do\n    case IdentityProviders.get_identity_provider_by_client_id(client_id) do\n      %IdentityProvider{} = identity_provider ->\n        {:ok, identity_provider}\n\n      nil ->\n        {:error,\n         \"identity provider not configured for given OAuth client. Please contact your administrator.\"}\n    end\n  end\n\n  @doc \"\"\"\n  Adds `client_impl` variable in function body context. The function definition must have\n  `context`, `client_id` and `module' as parameters.\n  \"\"\"\n  # TODO find a better way to delegate to the given client idp\n  defmacro defwithclientidp(fun, do: block) do\n    fun = Macro.escape(fun, unquote: true)\n    block = Macro.escape(block, unquote: true)\n\n    quote bind_quoted: [fun: fun, block: block] do\n      {name, params} = Macro.decompose_call(fun)\n\n      context_param =\n        Enum.find(params, fn {var, _, _} -> var == :context end) ||\n          raise \"`context` must be part of function parameters\"\n\n      client_id_param =\n        Enum.find(params, fn {var, _, _} -> var == :client_id end) ||\n          raise \"`client_id` must be part of function parameters\"\n\n      module_param =\n        Enum.find(params, fn {var, _, _} -> var == :module end) ||\n          raise \"`module` must be part of function parameters\"\n\n      def unquote({name, [line: __ENV__.line], params}) do\n        with {:ok, identity_provider} <-\n               BorutaIdentity.Accounts.Utils.client_identity_provider(unquote(client_id_param)),\n             :ok <-\n               BorutaIdentity.IdentityProviders.IdentityProvider.check_feature(\n                 identity_provider,\n                 unquote(name)\n               ) do\n          var!(client_idp) = identity_provider\n\n          unquote(block)\n        else\n          {:error, reason} ->\n            raise BorutaIdentity.Accounts.IdentityProviderError, reason\n        end\n      end\n    end\n  end\nend\n\ndefmodule BorutaIdentity.Accounts.IdentityProviderError do\n  @enforce_keys [:message]\n  defexception [:message, plug_status: 400]\n\n  @type t :: %__MODULE__{\n          message: String.t()\n        }\n\n  def exception(message) when is_binary(message) do\n    %__MODULE__{message: message}\n  end\n\n  def message(exception) do\n    exception.message\n  end\nend\n\ndefmodule BorutaIdentity.Accounts do\n  @moduledoc \"\"\"\n  The Accounts context.\n  \"\"\"\n\n  alias BorutaIdentity.Accounts.ChooseSessions\n  alias BorutaIdentity.Accounts.Confirmations\n  alias BorutaIdentity.Accounts.Consents\n  alias BorutaIdentity.Accounts.Registrations\n  alias BorutaIdentity.Accounts.ResetPasswords\n  alias BorutaIdentity.Accounts.Sessions\n  alias BorutaIdentity.Accounts.Settings\n  alias BorutaIdentity.Accounts.Users\n\n  ## Registrations\n\n  defdelegate initialize_registration(context, client_id, module), to: Registrations\n\n  defdelegate register(context, client_id, registration_params, confirmation_url_fun, module),\n    to: Registrations\n\n  ## Sessions\n\n  defdelegate initialize_session(context, client_id, module), to: Sessions\n  defdelegate create_session(context, client_id, authentication_params, module), to: Sessions\n  defdelegate delete_session(context, client_id, session_token, module), to: Sessions\n\n  ## Reset passwords\n\n  defdelegate initialize_password_instructions(context, client_id, module), to: ResetPasswords\n\n  defdelegate send_reset_password_instructions(\n                context,\n                client_id,\n                reset_password_params,\n                reset_password_url_fun,\n                module\n              ),\n              to: ResetPasswords\n\n  defdelegate initialize_password_reset(context, client_id, token, module), to: ResetPasswords\n\n  defdelegate reset_password(context, client_id, reset_password_params, module),\n    to: ResetPasswords\n\n  ## Confirmation\n\n  defdelegate initialize_confirmation_instructions(context, client_id, module), to: Confirmations\n\n  defdelegate send_confirmation_instructions(\n                context,\n                client_id,\n                confirmation_params,\n                confirmation_url_fun,\n                module\n              ),\n              to: Confirmations\n\n  defdelegate confirm_user(context, client_id, token, module), to: Confirmations\n\n  ## Consent\n\n  defdelegate initialize_consent(context, client_id, user, scope, module), to: Consents\n  defdelegate consent(context, client_id, user, params, module), to: Consents\n\n  ## Choose session\n\n  defdelegate initialize_choose_session(context, client_id, module), to: ChooseSessions\n\n  ## User settings\n\n  defdelegate initialize_edit_user(context, client_id, user, module), to: Settings\n\n  defdelegate update_user(context, client_id, user, params, confirmation_url_fun, module),\n    to: Settings\n\n  defdelegate destroy_user(context, client_id, user, module),\n    to: Settings\n\n  ## Deprecated Database getters\n\n  defdelegate get_user(id), to: Users\n  defdelegate get_user_by_email(backend, email), to: Users\n  defdelegate get_user_by_session_token(token), to: Users\n  defdelegate get_user_roles(user_id), to: Users\n  defdelegate get_user_scopes(user_id), to: Users\n  defdelegate get_user_organizations(user_id), to: Users\n  defdelegate put_user_webauthn_challenge(user), to: Users\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/admin.ex",
    "content": "defmodule BorutaIdentity.Admin do\n  @moduledoc \"\"\"\n  TODO Admin documentation\n  \"\"\"\n\n  import Ecto.Query\n\n  alias Boruta.Ecto.Admin\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.UserAuthorizedScope\n  alias BorutaIdentity.Accounts.UserRole\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.Organizations.OrganizationUser\n  alias BorutaIdentity.Repo\n  alias NimbleCSV.RFC4180, as: CSV\n\n  @type user_params ::\n          %{\n            optional(:username) => String.t(),\n            optional(:password) => String.t(),\n            optional(:group) => String.t(),\n            optional(:metadata) => map(),\n            optional(:roles) => list(map()),\n            optional(:authorized_scopes) => list(map()),\n            optional(:organizations) => list(map())\n          }\n\n  @type raw_user_params :: %{\n          username: String.t(),\n          hashed_password: String.t()\n        }\n\n  # NOTE emits a compilation warning since callback is already defined in BorutaIdentity.Accounts.Settings\n  # @callback delete_user(id :: String.t()) :: :ok | {:error, reason :: String.t()}\n\n  @callback create_user(\n              backend :: Backend.t(),\n              params :: user_params()\n            ) ::\n              {:ok, User.t()} | {:error, changeset :: Ecto.Changeset.t()}\n\n  @callback create_raw_user(\n              backend :: Backend.t(),\n              params :: user_params()\n            ) ::\n              {:ok, User.t()} | {:error, changeset :: Ecto.Changeset.t()}\n\n  @spec list_users(params :: map()) :: Scrivener.Page.t()\n  @spec list_users() :: Scrivener.Page.t()\n  def list_users(params \\\\ %{}) do\n    from(u in User)\n    |> user_preloads()\n    |> Repo.paginate(params)\n  end\n\n  @spec search_users(query :: String.t(), params :: map()) :: Scrivener.Page.t()\n  @spec search_users(query :: String.t()) :: Scrivener.Page.t()\n  def search_users(query, params \\\\ %{}) do\n    from(u in User,\n      where: fragment(\"username % ?\", ^query),\n      order_by: fragment(\"word_similarity(username, ?) DESC\", ^query)\n    )\n    |> user_preloads()\n    |> Repo.paginate(params)\n  end\n\n  @doc \"\"\"\n  Gets a single user.\n\n  ## Examples\n\n      iex> get_user(123)\n      %User{}\n\n      iex> get_user(456)\n      nil\n\n  \"\"\"\n  @spec get_user(id :: Ecto.UUID.t()) :: user :: User.t() | nil\n  def get_user(id) do\n    Repo.one(\n      from(u in User,\n        left_join: as in assoc(u, :authorized_scopes),\n        left_join: r in assoc(u, :roles),\n        left_join: o in assoc(u, :organizations),\n        join: b in assoc(u, :backend),\n        preload: [authorized_scopes: as, roles: r, backend: b, organizations: o],\n        where: u.id == ^id\n      )\n    )\n  end\n\n  use BorutaIdentity.PostUserCreationHook\n\n  @spec create_user(backend :: Backend.t(), params :: user_params()) ::\n          {:ok, User.t()} | {:error, Ecto.Changeset.t()}\n  @decorate post_user_creation_hook([])\n  def create_user(backend, params) do\n    with {:ok, user} <-\n           apply(\n             Backend.implementation(backend),\n             :create_user,\n             [backend, params]\n           ),\n         {:ok, user} <- update_user_authorized_scopes(user, params[:authorized_scopes] || []),\n         {:ok, user} <- update_user_organizations(user, params[:organizations] || []),\n         {:ok, user} <- update_user_roles(user, params[:roles] || []) do\n      {:ok, user}\n    end\n  end\n\n  @spec create_raw_user(backend :: Backend.t(), params :: raw_user_params()) ::\n          {:ok, User.t()} | {:error, Ecto.Changeset.t()}\n  @decorate post_user_creation_hook([])\n  def create_raw_user(backend, params) do\n    # TODO give the ability to provide authorized scopes at user creation\n    apply(\n      Backend.implementation(backend),\n      :create_raw_user,\n      [backend, params]\n    )\n  end\n\n  @type import_users_opts :: %{\n          optional(:username_header) => String.t(),\n          optional(:password_header) => String.t(),\n          optional(:hash_password) => boolean()\n        }\n\n  @spec import_users(backend :: Backend.t(), csv_path :: String.t(), opts :: import_users_opts()) ::\n          import_result :: map()\n  def import_users(backend, csv_path, opts \\\\ %{}) do\n    opts =\n      Map.merge(\n        %{\n          username_header: \"username\",\n          password_header: \"password\",\n          hash_password: false\n        },\n        opts\n      )\n\n    headers =\n      File.stream!(csv_path)\n      |> CSV.parse_stream(skip_headers: false)\n      |> Enum.take(1)\n      |> Enum.reduce(%{}, fn headers, _acc ->\n        username_index =\n          Enum.find_index(headers, fn header -> header == opts[:username_header] end)\n\n        password_index =\n          Enum.find_index(headers, fn header -> header == opts[:password_header] end)\n\n        metadata_headers = Enum.map(opts[:metadata_headers] || [], fn metadata_header ->\n          [origin, target] = String.split(metadata_header, \">\")\n\n          header_index = Enum.find_index(headers, fn header -> header == origin end)\n          {target, header_index}\n        end)\n        |> Enum.into(%{})\n\n        %{\n          username: {\"username\", username_index},\n          password: {\"password\", password_index},\n          metadata: metadata_headers\n        }\n      end)\n\n    File.stream!(csv_path)\n    |> CSV.parse_stream(skip_headers: true)\n    |> Stream.map(fn row ->\n      case opts[:hash_password] do\n        true ->\n          create_params = %{\n            username: headers[:username] && Enum.at(row, elem(headers[:username], 1)),\n            password: headers[:password] && Enum.at(row, elem(headers[:password], 1))\n          } |> Map.put(:metadata, Enum.map(headers[:metadata], fn header ->\n            {elem(header, 0), %{\n              \"value\" => Enum.at(row, elem(header, 1)),\n              \"status\" => \"valid\",\n              \"display\" => [\"status\", \"origin\"],\n              \"origin\" => \"import - #{:os.system_time(:second)}\"\n            }}\n          end) |> Enum.into(%{}))\n\n          create_user(backend, create_params)\n\n        _ ->\n          create_params = %{\n            username: headers[:username] && Enum.at(row, elem(headers[:username], 1)),\n            hashed_password: headers[:password] && Enum.at(row, elem(headers[:password], 1))\n          }\n\n          create_raw_user(backend, create_params)\n      end\n    end)\n    |> Stream.with_index(1)\n    |> Enum.reduce(\n      %{success_count: 0, error_count: 0, errors: []},\n      fn\n        {{:ok, _user}, _line},\n        %{\n          success_count: success_count,\n          error_count: error_count,\n          errors: errors\n        } ->\n          %{\n            success_count: success_count + 1,\n            error_count: error_count,\n            errors: errors\n          }\n\n        {{:error, changeset}, line},\n        %{\n          success_count: success_count,\n          error_count: error_count,\n          errors: errors\n        } ->\n          %{\n            success_count: success_count,\n            error_count: error_count + 1,\n            errors: errors ++ [%{line: line, changeset: changeset}]\n          }\n      end\n    )\n    |> Enum.into(%{})\n  end\n\n  @spec update_user_authorized_scopes(user :: %User{}, scopes :: list(map())) ::\n          {:ok, %User{}} | {:error, Ecto.Changeset.t()}\n  def update_user_authorized_scopes(%User{id: user_id} = user, scopes) do\n    Repo.delete_all(from(s in UserAuthorizedScope, where: s.user_id == ^user_id))\n\n    case Enum.reduce(scopes, Ecto.Multi.new(), fn attrs, multi ->\n           changeset =\n             UserAuthorizedScope.changeset(\n               %UserAuthorizedScope{},\n               %{\n                 \"scope_id\" => attrs[\"id\"] || attrs[:scope_id],\n                 \"user_id\" => user.id\n               }\n             )\n\n           Ecto.Multi.insert(multi, \"scope_-#{SecureRandom.uuid()}\", changeset)\n         end)\n         |> Repo.transaction() do\n      {:ok, _result} ->\n        {:ok, user |> Repo.reload() |> user_preloads()}\n\n      {:error, _multi_name, %Ecto.Changeset{} = changeset, _changes} ->\n        {:error, changeset}\n    end\n  end\n\n  @spec update_user_roles(user :: %User{}, roles :: list(map())) ::\n          {:ok, %User{}} | {:error, Ecto.Changeset.t()}\n  def update_user_roles(%User{id: user_id} = user, roles) do\n    Repo.delete_all(from(s in UserRole, where: s.user_id == ^user_id))\n\n    case Enum.reduce(roles, Ecto.Multi.new(), fn attrs, multi ->\n           changeset =\n             UserRole.changeset(\n               %UserRole{},\n               %{\n                 \"role_id\" => attrs[\"id\"] || attrs[:role_id],\n                 \"user_id\" => user.id\n               }\n             )\n\n           Ecto.Multi.insert(multi, \"role_-#{SecureRandom.uuid()}\", changeset)\n         end)\n         |> Repo.transaction() do\n      {:ok, _result} ->\n        {:ok, user |> Repo.reload() |> user_preloads()}\n\n      {:error, _multi_name, %Ecto.Changeset{} = changeset, _changes} ->\n        {:error, changeset}\n    end\n  end\n\n  @spec update_user_organizations(user :: %User{}, organizations :: list(map())) ::\n          {:ok, %User{}} | {:error, Ecto.Changeset.t()}\n  def update_user_organizations(%User{id: user_id} = user, organizations) do\n    Repo.delete_all(from(o in OrganizationUser, where: o.user_id == ^user_id))\n\n    case Enum.reduce(organizations, Ecto.Multi.new(), fn attrs, multi ->\n           changeset =\n             OrganizationUser.changeset(\n               %OrganizationUser{},\n               %{\n                 \"organization_id\" => attrs[\"id\"] || attrs[:organization_id],\n                 \"user_id\" => user.id\n               }\n             )\n\n           Ecto.Multi.insert(multi, \"organization_-#{SecureRandom.uuid()}\", changeset)\n         end)\n         |> Repo.transaction() do\n      {:ok, _result} ->\n        {:ok, user |> Repo.reload() |> user_preloads()}\n\n      {:error, _multi_name, %Ecto.Changeset{} = changeset, _changes} ->\n        {:error, changeset}\n    end\n  end\n\n  @spec update_user(user :: User.t(), user_params :: user_params()) ::\n          {:ok, user :: User.t()} | {:error, Ecto.Changeset.t()}\n  def update_user(user, user_params) do\n    with {:ok, user} <-\n           user\n           |> User.changeset(user_params)\n           |> Repo.update(),\n         {:ok, user} <-\n           update_user_authorized_scopes(\n             user,\n             user_params[:authorized_scopes] || user.authorized_scopes\n           ),\n         {:ok, user} <-\n           update_user_organizations(\n             user,\n             user_params[:organizations] || user.organizations\n           ) do\n      update_user_roles(user, user_params[:roles] || user.roles)\n    end\n  end\n\n  @spec delete_user(user_id :: Ecto.UUID.t()) ::\n          {:ok, user :: User.t()}\n          | {:error, atom()}\n          | {:error, Ecto.Changeset.t()}\n          | {:error, reason :: String.t()}\n  def delete_user(user_id) when is_binary(user_id) do\n    case get_user(user_id) do\n      nil ->\n        {:error, :not_found}\n\n      user ->\n        # TODO delete both provider and domain users in a transaction\n        # TODO manage identity federated users\n        with :ok <- apply(Backend.implementation(user.backend, user.account_type), :delete_user, [user.uid]) do\n          Repo.delete(user)\n        end\n    end\n  end\n\n  @spec delete_user_authorized_scopes_by_id(scope_id :: String.t()) :: {deleted :: integer(), nil}\n  def delete_user_authorized_scopes_by_id(scope_id) do\n    Repo.delete_all(from(s in UserAuthorizedScope, where: s.scope_id == ^scope_id))\n  end\n\n  defp user_preloads(users) when is_list(users) do\n    Repo.preload(users, [:backend, :authorized_scopes, :roles, :organizations])\n  end\n\n  defp user_preloads(%User{} = user) do\n    Repo.preload(user, [:backend, :authorized_scopes, :roles, :organizations])\n  end\n\n  defp user_preloads(queryable) do\n    preload(queryable, [:backend, :authorized_scopes, :roles, :organizations])\n  end\n\n  defdelegate list_organizations, to: BorutaIdentity.Organizations\n  defdelegate list_organizations(params), to: BorutaIdentity.Organizations\n  # defdelegate search_organizations(query), to: BorutaIdentity.Organizations\n  # defdelegate search_organizations(query, params), to: BorutaIdentity.Organizations\n  defdelegate get_organization(organization_id), to: BorutaIdentity.Organizations\n  defdelegate create_organization(organization_params), to: BorutaIdentity.Organizations\n\n  defdelegate update_organization(organization, organization_params),\n    to: BorutaIdentity.Organizations\n\n  defdelegate delete_organization(organization_id), to: BorutaIdentity.Organizations\n\n  # --------- TODO refactor below functions\n  alias BorutaIdentity.Accounts.Role\n\n  def list_roles do\n    Repo.all(\n      from r in Role,\n        left_join: rs in assoc(r, :role_scopes),\n        preload: [role_scopes: rs]\n    )\n    |> Enum.map(fn %Role{role_scopes: role_scopes} = role ->\n      scopes =\n        role_scopes\n        |> Enum.map(fn role_scope -> role_scope.scope_id end)\n        |> Admin.get_scopes_by_ids()\n\n      %{role | scopes: scopes}\n    end)\n  end\n\n  @doc \"\"\"\n  Gets a single role.\n\n  Raises `Ecto.NoResultsError` if the Role does not exist.\n\n  ## Examples\n\n      iex> get_role!(123)\n      %Role{}\n\n      iex> get_role!(456)\n      ** (Ecto.NoResultsError)\n\n  \"\"\"\n  def get_role!(id) do\n    %Role{role_scopes: role_scopes} =\n      role =\n      Repo.one!(\n        from r in Role,\n          left_join: rs in assoc(r, :role_scopes),\n          where: r.id == ^id,\n          preload: [role_scopes: rs]\n      )\n\n    scopes =\n      role_scopes\n      |> Enum.map(fn role_scope -> role_scope.scope_id end)\n      |> Admin.get_scopes_by_ids()\n\n    %{role | scopes: scopes}\n  end\n\n  def create_role(attrs \\\\ %{}) do\n    with {:ok, role} <-\n           %Role{}\n           |> Role.changeset(attrs)\n           |> Repo.insert() do\n      {:ok, get_role!(role.id)}\n    end\n  end\n\n  def update_role(%Role{} = role, attrs) do\n    with {:ok, role} <-\n           role\n           |> Role.changeset(attrs)\n           |> Repo.update() do\n      {:ok, get_role!(role.id)}\n    end\n  end\n\n  def delete_role(%Role{} = role) do\n    Repo.delete(role)\n  end\n\n  def change_role(%Role{} = role, attrs \\\\ %{}) do\n    Role.changeset(role, attrs)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/application.ex",
    "content": "defmodule BorutaIdentity.Application do\n  # See https://hexdocs.pm/elixir/Application.html\n  # for more information on OTP Applications\n  @moduledoc false\n\n  use Application\n\n  def start(_type, _args) do\n    children = [\n      BorutaIdentity.Repo,\n      BorutaIdentityWeb.Telemetry,\n      {Phoenix.PubSub, name: BorutaIdentity.PubSub},\n      BorutaIdentityWeb.Endpoint,\n      {Finch, name: BorutaIdentity.Finch}\n    ]\n\n    BorutaIdentity.Logger.start()\n    setup_database()\n\n    opts = [strategy: :one_for_one, name: BorutaIdentity.Supervisor]\n    Supervisor.start_link(children, opts)\n  end\n\n  # Tell Phoenix to update the endpoint configuration\n  # whenever the application is updated.\n  def config_change(changed, _new, removed) do\n    BorutaIdentityWeb.Endpoint.config_change(changed, removed)\n    :ok\n  end\n\n  def setup_database do\n    Enum.each([BorutaAuth.Repo, BorutaIdentity.Repo], fn repo ->\n      repo.__adapter__.storage_up(repo.config)\n    end)\n\n    :ok\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/clients.ex",
    "content": "defmodule BorutaIdentity.Clients do\n  @moduledoc false\n\n  alias Boruta.Ecto.Admin\n  alias Boruta.Ecto.Client\n  alias BorutaAuth.KeyPairs\n  alias BorutaAuth.KeyPairs.KeyPair\n  alias BorutaIdentity.IdentityProviders\n\n  def create_client(client_params) do\n    identity_provider_id = get_in(client_params, [\"identity_provider\", \"id\"])\n\n    BorutaAuth.Repo.transaction(fn ->\n      with {:ok, client} <- Admin.create_client(client_params),\n           {:ok, client} <- insert_global_key_pair(client, client_params[\"key_pair_id\"]),\n           {:ok, _client_identity_provider} <-\n             IdentityProviders.upsert_client_identity_provider(\n               client.id,\n               identity_provider_id\n             ) do\n        client\n      else\n        {:error, error} ->\n          BorutaAuth.Repo.rollback(error)\n      end\n    end)\n  end\n\n  def insert_global_key_pair(%Client{} = client, nil), do: {:ok, client}\n  def insert_global_key_pair(%Client{} = client, key_pair_id) do\n    %KeyPair{public_key: public_key, private_key: private_key} =\n      KeyPairs.get_key_pair!(key_pair_id)\n\n    Admin.regenerate_client_key_pair(client, public_key, private_key)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/configuration/error_template.ex",
    "content": "defmodule BorutaIdentity.Configuration.ErrorTemplate do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  @type t :: %__MODULE__{\n          id: String.t() | nil,\n          type: String.t(),\n          default: boolean(),\n          content: String.t(),\n          inserted_at: DateTime.t() | nil,\n          updated_at: DateTime.t() | nil\n        }\n\n  @template_types [\n    400,\n    401,\n    403,\n    404,\n    500\n  ]\n  @type template_type :: integer()\n\n  @default_templates %{\n    400 =>\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/errors/400.mustache\")\n      |> File.read!(),\n    401 =>\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/errors/401.mustache\")\n      |> File.read!(),\n    403 =>\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/errors/403.mustache\")\n      |> File.read!(),\n    404 =>\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/errors/404.mustache\")\n      |> File.read!(),\n    500 =>\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/errors/500.mustache\")\n      |> File.read!()\n  }\n\n  @foreign_key_type :binary_id\n  @primary_key {:id, :binary_id, autogenerate: true}\n  schema \"error_templates\" do\n    field(:content, :string)\n    field(:type, :string)\n\n    field(:default, :boolean, virtual: true, default: false)\n\n    timestamps()\n  end\n\n  def template_types, do: @template_types\n\n  @spec default_content(type :: template_type()) :: template_content :: String.t()\n  def default_content(type) when type in @template_types, do: @default_templates[type]\n\n  @spec default_template(type :: template_type()) :: %__MODULE__{} | nil\n  def default_template(type) when type in @template_types do\n    %__MODULE__{\n      default: true,\n      type: Integer.to_string(type),\n      content: default_content(type)\n    }\n  end\n\n  def default_template(_type), do: nil\n\n  @doc false\n  def changeset(template, attrs) do\n    template\n    |> cast(attrs, [:type, :content])\n    |> validate_inclusion(:type, Enum.map(@template_types, &Integer.to_string/1))\n    |> validate_required([:type, :content])\n    |> put_default()\n  end\n\n  defp put_default(changeset) do\n    case fetch_change(changeset, :content) do\n      {:ok, content} when not is_nil(content) ->\n        changeset\n\n      _ ->\n        change(\n          changeset,\n          content: default_template(changeset |> fetch_field!(:type) |> String.to_integer())\n        )\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/configuration.ex",
    "content": "defmodule BorutaIdentity.Configuration do\n  @moduledoc false\n\n  import Ecto.Query, only: [from: 2]\n\n  alias BorutaIdentity.Configuration.ErrorTemplate\n  alias BorutaIdentity.Repo\n\n  def get_error_template!(type) do\n    case Repo.get_by(ErrorTemplate, type: to_string(type)) do\n      nil -> ErrorTemplate.default_template(type)\n      template -> template\n    end || raise Ecto.NoResultsError, queryable: ErrorTemplate\n  end\n\n  def upsert_error_template(%ErrorTemplate{id: template_id} = template, attrs) do\n    changeset = ErrorTemplate.changeset(template, attrs)\n\n    case template_id do\n      nil -> Repo.insert(changeset)\n      _ -> Repo.update(changeset)\n    end\n  end\n\n  def delete_error_template!(type) do\n    template_type = to_string(type)\n\n    with {1, _results} <-\n           Repo.delete_all(\n             from(t in ErrorTemplate,\n               where: t.type == ^template_type\n             )\n           ),\n         %ErrorTemplate{} = template <- get_error_template!(type) do\n      template\n    else\n      {0, nil} -> raise Ecto.NoResultsError, queryable: ErrorTemplate\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/federated_accounts.ex",
    "content": "defmodule BorutaIdentity.Accounts.FederatedSessionApplication do\n  @moduledoc \"\"\"\n  TODO FederatedSessionApplication documentation\n  \"\"\"\n\n  @callback user_authenticated(\n              context :: any(),\n              user :: BorutaIdentity.Accounts.User.t(),\n              session_token :: String.t()\n            ) ::\n              any()\n\n  @callback authentication_failure(\n              context :: any(),\n              error :: BorutaIdentity.Accounts.SessionError.t()\n            ) ::\n              any()\nend\n\ndefmodule BorutaIdentity.FederatedAccounts do\n  @moduledoc false\n  import BorutaIdentity.Accounts.Utils, only: [defwithclientidp: 2]\n\n  require Logger\n\n  alias BorutaIdentity.Accounts.Federated\n  alias BorutaIdentity.Accounts.IdentityProviderError\n  alias BorutaIdentity.Accounts.SessionError\n  alias BorutaIdentity.Accounts.Sessions\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  @callback domain_user!(\n              federated_server_name :: String.t(),\n              access_token :: String.t(),\n              backend :: Backend.t()\n            ) ::\n              user :: User.t()\n\n  @spec create_federated_session(\n          context :: any(),\n          client_id :: String.t(),\n          federated_server_name :: any(),\n          code :: String.t(),\n          module :: atom()\n        ) :: callback_result :: any()\n  defwithclientidp create_federated_session(\n                     context,\n                     client_id,\n                     federated_server_name,\n                     code,\n                     module\n                   ) do\n    try do\n      case Backend.federated_oauth_client(client_idp.backend, federated_server_name) do\n        nil ->\n          raise IdentityProviderError, \"Could not fetch associated federated server.\"\n\n        oauth_client ->\n          %OAuth2.Client{token: token} =\n            OAuth2.Client.get_token!(oauth_client, code: code)\n\n          with %User{} = user <-\n                 Federated.domain_user!(federated_server_name, token.access_token, client_idp.backend),\n               {:ok, user, session_token} <- Sessions.create_user_session(user) do\n            module.user_authenticated(context, user, session_token)\n          end\n      end\n    rescue\n    error in OAuth2.Error ->\n        module.authentication_failure(context, %SessionError{\n          message: error.reason,\n          template: new_session_template(client_idp)\n        })\n    error in IdentityProviderError ->\n        module.authentication_failure(context, %SessionError{\n          message: error.message,\n          template: new_session_template(client_idp)\n        })\n      error ->\n        Logger.error(\"Federation failed \" <> inspect(error))\n\n        module.authentication_failure(context, %SessionError{\n          message: \"Could not fetch user information.\",\n          template: new_session_template(client_idp)\n        })\n    end\n  end\n\n  defp new_session_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(identity_provider.id, :new_session)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/hooks/post_user_creation_hook.ex",
    "content": "defmodule BorutaIdentity.PostUserCreationHook do\n  @moduledoc false\n\n  use Decorator.Define, post_user_creation_hook: 1\n\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Admin\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.Organizations\n\n  def post_user_creation_hook(_options, body, _context) do\n    quote do\n      with {:ok, user} = result <- unquote(body),\n           {:ok, user} <- BorutaIdentity.PostUserCreationHook.maybe_create_organization(user) do\n        {:ok, user}\n      end\n    end\n  end\n\n  @spec maybe_create_organization(user :: User.t()) ::\n          {:ok, user :: User.t()} | {:error, changeset :: Ecto.Changeset.t()}\n  def maybe_create_organization(\n        %User{backend: %Backend{create_default_organization: true}} = user\n      ) do\n    with {:ok, organization} <-\n           Organizations.create_organization(%{\n             name: \"default_#{user.uid}\",\n             label: \"Default\"\n           }) do\n      organizations =\n        [organization | user.organizations]\n        |> Enum.map(fn %{id: id} -> %{\"id\" => id} end)\n\n      Admin.update_user_organizations(user, organizations)\n    end\n  end\n\n  def maybe_create_organization(user), do: {:ok, user}\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/identity_providers/backend.ex",
    "content": "defmodule BorutaIdentity.IdentityProviders.Backend do\n  @moduledoc false\n\n  defmodule AuthCodeStrategy do\n    @moduledoc false\n\n    use OAuth2.Strategy\n\n    @impl true\n    def authorize_url(client, params) do\n      client\n      |> put_param(:response_type, \"code\")\n      |> put_param(:client_id, client.client_id)\n      |> put_param(:redirect_uri, client.redirect_uri)\n      |> merge_params(params)\n    end\n\n    @impl true\n    def get_token(client, params, headers) do\n      {code, params} = Keyword.pop(params, :code, client.params[\"code\"])\n\n      unless code do\n        raise OAuth2.Error, reason: \"Missing required key `code` for `#{inspect(__MODULE__)}`\"\n      end\n\n      client\n      |> put_param(:code, code)\n      |> put_param(:grant_type, \"authorization_code\")\n      |> put_param(:client_id, client.client_id)\n      |> put_param(:client_secret, client.client_secret)\n      |> put_param(:redirect_uri, client.redirect_uri)\n      |> merge_params(params)\n      |> basic_auth()\n      |> put_headers(headers)\n    end\n  end\n\n  use Ecto.Schema\n  use Nebulex.Caching\n  import Ecto.Changeset\n\n  alias BorutaIdentity.Accounts.EmailTemplate\n  alias BorutaIdentity.Accounts.Federated\n  alias BorutaIdentity.Accounts.Internal\n  alias BorutaIdentity.Accounts.Ldap\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Repo\n  alias BorutaIdentityWeb.Router.Helpers, as: Routes\n\n  @type t :: %__MODULE__{\n          type: String.t(),\n          name: String.t(),\n          is_default: boolean(),\n          metadata_fields: map(),\n          password_hashing_alg: String.t(),\n          password_hashing_opts: map(),\n          email_templates: Ecto.Association.NotLoaded.t() | list(EmailTemplate.t()),\n          create_default_organization: boolean(),\n          smtp_from: String.t() | nil,\n          smtp_relay: String.t() | nil,\n          smtp_username: String.t() | nil,\n          smtp_password: String.t() | nil,\n          smtp_tls: String.t() | nil,\n          smtp_port: integer() | nil,\n          ldap_pool_size: integer() | nil,\n          ldap_host: String.t() | nil,\n          ldap_user_rdn_attribute: String.t() | nil,\n          ldap_base_dn: String.t() | nil,\n          ldap_ou: String.t() | nil,\n          ldap_master_dn: String.t() | nil,\n          ldap_master_password: String.t() | nil,\n          inserted_at: DateTime.t() | nil,\n          updated_at: DateTime.t() | nil\n        }\n\n  @backend_types [Internal, Ldap]\n\n  def account_implementations do\n    Enum.map([Internal, Federated, Ldap], fn module ->\n      {apply(module, :account_type, []), module}\n    end)\n    |> Enum.into(%{})\n  end\n\n  @smtp_tls_types [\n    :always,\n    :never,\n    :if_available\n  ]\n\n  @password_hashing_modules %{\n    \"argon2\" => Argon2,\n    \"bcrypt\" => Bcrypt,\n    \"pbkdf2\" => Pbkdf2\n  }\n\n  @password_hashing_opts_schema %{\n    \"argon2\" => %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"salt_len\" => %{\"type\" => \"number\"},\n        \"t_cost\" => %{\"type\" => \"number\"},\n        \"m_cost\" => %{\"type\" => \"number\"},\n        \"parallelism\" => %{\"type\" => \"number\"},\n        \"format\" => %{\"type\" => \"string\", \"pattern\" => \"^(encoded|raw_hash|report)$\"},\n        \"hashlen\" => %{\"type\" => \"number\", \"minimum\" => 1, \"maximum\" => 128},\n        \"argon2_type\" => %{\"type\" => \"number\", \"minimum\" => 0, \"maximum\" => 2}\n      },\n      \"additionalProperties\" => false\n    },\n    \"bcrypt\" => %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"log_rounds\" => %{\"type\" => \"number\"},\n        \"legacy\" => %{\"type\" => \"boolean\"}\n      },\n      \"additionalProperties\" => false\n    },\n    \"pbkdf2\" => %{\n      \"type\" => \"object\",\n      \"properties\" => %{\n        \"salt_len\" => %{\"type\" => \"number\"},\n        \"format\" => %{\"type\" => \"string\", \"pattern\" => \"^(modular|django|hex)$\"},\n        \"digest\" => %{\"type\" => \"string\", \"pattern\" => \"^(sha224|sha256|sha384|sha512)$\"},\n        \"length\" => %{\"type\" => \"number\", \"minimum\" => 1, \"maximum\" => 64}\n      },\n      \"additionalProperties\" => false\n    }\n  }\n\n  @federated_server_schema ExJsonSchema.Schema.resolve(%{\n                             \"type\" => \"object\",\n                             \"properties\" => %{\n                               \"name\" => %{\"type\" => \"string\", \"pattern\" => \"^[^\\s]+$\"},\n                               \"client_id\" => %{\"type\" => \"string\"},\n                               \"client_secret\" => %{\"type\" => \"string\"},\n                               \"base_url\" => %{\"type\" => \"string\"},\n                               \"discovery_path\" => %{\"type\" => \"string\"},\n                               \"userinfo_path\" => %{\"type\" => \"string\"},\n                               \"authorize_path\" => %{\"type\" => \"string\"},\n                               \"token_path\" => %{\"type\" => \"string\"},\n                               \"scope\" => %{\"type\" => \"string\"},\n                               \"federated_attributes\" => %{\"type\" => \"string\"},\n                               \"metadata_endpoints\" => %{\n                                 \"type\" => \"array\",\n                                 \"items\" => %{\n                                   \"type\" => \"object\",\n                                   \"properties\" => %{\n                                     \"endpoint\" => %{\"type\" => \"string\"},\n                                     \"claims\" => %{\"type\" => \"string\"}\n                                   },\n                                   \"required\" => [\"endpoint\", \"claims\"]\n                                 }\n                               }\n                             },\n                             \"required\" => [\n                               \"name\",\n                               \"client_id\",\n                               \"client_secret\",\n                               \"base_url\"\n                             ],\n                             \"additionalProperties\" => false\n                           })\n\n  @metadata_fields_schema ExJsonSchema.Schema.resolve(%{\n                            \"type\" => \"array\",\n                            \"items\" => %{\n                              \"type\" => \"object\",\n                              \"properties\" => %{\n                                \"attribute_name\" => %{\"type\" => \"string\"},\n                                \"user_editable\" => %{\"type\" => \"boolean\"},\n                                \"scopes\" => %{\"type\" => \"array\", \"items\" => %{\"type\" => \"string\"}}\n                              },\n                              \"required\" => [\"attribute_name\"],\n                              \"additionalProperties\" => false\n                            }\n                          })\n\n  @verifiable_credential_schema ExJsonSchema.Schema.resolve(%{\n                                  \"type\" => \"object\",\n                                  \"properties\" => %{\n                                    \"version\" => %{\"type\" => \"string\"},\n                                    \"credential_identifier\" => %{\"type\" => \"string\"},\n                                    \"vct\" => %{\"type\" => \"string\"},\n                                    \"time_to_live\" => %{\"type\" => \"number\"},\n                                    \"types\" => %{\"type\" => \"string\"},\n                                    \"format\" => %{\"type\" => \"string\", \"pattern\" => \"jwt_vc|jwt_vc_json|vc\\\\+sd\\\\-jwt\"},\n                                    \"defered\" => %{\"type\" => \"boolean\"},\n                                    \"claims\" => %{\n                                      \"type\" => \"array\",\n                                      \"items\" => %{\n                                        \"type\" => \"object\",\n                                        \"properties\" => %{\n                                          \"type\" => %{\"type\" => \"string\"},\n                                          \"name\" => %{\"type\" => \"string\"},\n                                          \"label\" => %{\"type\" => \"string\"},\n                                          \"pointer\" => %{\"type\" => \"string\"},\n                                          # TODO check claims schema\n                                          \"claims\" => %{\"type\" => \"array\"},\n                                          # TODO check items schema\n                                          \"items\" => %{\"type\" => \"array\"}\n                                        },\n                                        \"required\" => [\"name\"],\n                                        \"additionalProperties\" => false\n                                      }\n                                    },\n                                    \"display\" => %{\n                                      \"type\" => \"object\",\n                                      \"properties\" => %{\n                                        \"name\" => %{\"type\" => \"string\"},\n                                        \"locale\" => %{\"type\" => \"string\"},\n                                        \"background_color\" => %{\"type\" => \"string\"},\n                                        \"text_color\" => %{\"type\" => \"string\"},\n                                        \"logo\" => %{\n                                          \"type\" => \"object\",\n                                          \"properties\" => %{\n                                            \"url\" => %{\"type\" => \"string\"},\n                                            \"alt_text\" => %{\"type\" => \"string\"}\n                                          }\n                                        }\n                                      },\n                                      \"required\" => [\"name\", \"logo\"],\n                                      \"additionalProperties\" => false\n                                    },\n                                    \"scopes\" => %{\"type\" => \"array\", \"items\" => %{\"type\" => \"string\"}}\n                                  },\n                                  \"required\" => [\n                                    \"version\",\n                                    \"credential_identifier\",\n                                    \"format\",\n                                    \"types\",\n                                    \"claims\",\n                                    \"display\"\n                                  ],\n                                  \"additionalProperties\" => false\n                                })\n\n  @verifiable_presentation_schema ExJsonSchema.Schema.resolve(%{\n                                  \"type\" => \"object\",\n                                  \"properties\" => %{\n                                    \"presentation_identifier\" => %{\"type\" => \"string\"},\n                                    \"presentation_definition\" => %{\"type\" => \"string\"}\n                                  },\n                                  \"required\" => [\n                                    \"presentation_identifier\",\n                                    \"presentation_definition\"\n                                  ],\n                                  \"additionalProperties\" => false\n  })\n\n  @spec backend_types() :: list(atom)\n  def backend_types, do: @backend_types\n\n  @primary_key {:id, Ecto.UUID, autogenerate: true}\n  schema \"backends\" do\n    field(:type, :string)\n    field(:is_default, :boolean, default: false)\n    field(:name, :string)\n    field(:metadata_fields, {:array, :map}, default: [])\n    field(:create_default_organization, :boolean, default: false)\n\n    # smtp config\n    field(:smtp_from, :string)\n    field(:smtp_relay, :string)\n    field(:smtp_username, :string)\n    field(:smtp_password, :string)\n    field(:smtp_ssl, :boolean)\n    field(:smtp_tls, :string)\n    field(:smtp_port, :integer)\n\n    # ldap config\n    field(:ldap_pool_size, :integer, default: 5)\n    field(:ldap_host, :string)\n    field(:ldap_user_rdn_attribute, :string)\n    field(:ldap_base_dn, :string)\n    field(:ldap_ou, :string)\n    field(:ldap_master_dn, :string)\n    field(:ldap_master_password, :string)\n\n    # internal config\n    field(:password_hashing_alg, :string, default: \"argon2\")\n    field(:password_hashing_opts, :map, default: %{})\n\n    # identity federation\n    field(:federated_servers, {:array, :map}, default: [])\n\n    # verifiable credentials\n    field(:verifiable_credentials, {:array, :map}, default: [])\n\n    # verifiable presentations\n    field(:verifiable_presentations, {:array, :map}, default: [])\n\n    has_many(:email_templates, EmailTemplate)\n    has_many(:users, User)\n\n    timestamps()\n  end\n\n  @spec default!() :: t()\n  @decorate cacheable(key: {__MODULE__, :default}, cache: Boruta.Cache)\n  def default! do\n    Repo.get_by!(__MODULE__, is_default: true)\n  end\n\n  @spec implementation(t()) :: atom()\n  @spec implementation(t(), account_type :: String.t()) :: atom()\n  def implementation(%__MODULE__{type: type}, account_type \\\\ nil) do\n    case account_type do\n      nil ->\n        String.to_atom(type)\n      account_type ->\n        account_implementations()[account_type]\n    end\n  end\n\n  @spec features(backend :: t()) :: list(atom())\n  def features(backend) do\n    apply(implementation(backend), :features, [])\n  end\n\n  @spec password_hashing_module(t()) :: atom()\n  def password_hashing_module(%__MODULE__{password_hashing_alg: password_hashing_alg}) do\n    @password_hashing_modules[password_hashing_alg]\n  end\n\n  @spec password_hashing_opts(t()) :: Keyword.t()\n  def password_hashing_opts(%__MODULE__{password_hashing_opts: password_hashing_opts}) do\n    Enum.map(password_hashing_opts, fn\n      {key, value} when is_binary(value) -> {String.to_atom(key), String.to_atom(value)}\n      {key, value} -> {String.to_atom(key), value}\n    end)\n    |> Enum.into([])\n  end\n\n  @spec email_template(backend :: t(), type :: atom()) :: EmailTemplate.t() | nil\n  def email_template(%__MODULE__{email_templates: email_templates} = backend, type)\n      when is_list(email_templates) do\n    case Enum.find(email_templates, fn\n           %EmailTemplate{type: template_type} -> Atom.to_string(type) == template_type\n         end) do\n      nil ->\n        template = EmailTemplate.default_template(type)\n\n        template &&\n          %{\n            template\n            | backend_id: backend.id,\n              backend: backend\n          }\n\n      template ->\n        %{template | backend: backend}\n    end\n  end\n\n  @spec federated_login_url(backend :: t(), federated_server_name :: String.t()) ::\n          login_url :: String.t()\n  def federated_login_url(%__MODULE__{} = backend, federated_server_name) do\n    case federated_oauth_client(backend, federated_server_name) do\n      nil ->\n        \"\"\n\n      client ->\n        OAuth2.Client.authorize_url!(client,\n          scope:\n            Enum.join(\n              [federated_server_scope(backend, federated_server_name)],\n              \" \"\n            )\n        )\n    end\n  end\n\n  @spec federated_oauth_client(backend :: t(), federated_server_name :: String.t()) ::\n          oauth_client :: OAuth2.Client.t() | nil\n  def federated_oauth_client(\n        %__MODULE__{federated_servers: federated_servers} = backend,\n        federated_server_name\n      ) do\n    case Enum.find(federated_servers, fn federated_server ->\n           federated_server[\"name\"] == federated_server_name\n         end) do\n      nil ->\n        nil\n\n      federated_server ->\n        base_url = URI.parse(federated_server[\"base_url\"])\n\n        endpoints =\n          case federated_server[\"discovery_path\"] do\n            nil ->\n              %{\n                authorize_url:\n                  URI.to_string(%{\n                    base_url\n                    | path: federated_server[\"authorize_path\"]\n                  }),\n                token_url:\n                  URI.to_string(%{\n                    base_url\n                    | path: federated_server[\"token_path\"]\n                  })\n              }\n\n            discovery_path ->\n              discover_federated_server_urls(backend, federated_server, discovery_path)\n          end\n\n        client =\n          OAuth2.Client.new(\n            strategy: AuthCodeStrategy,\n            token_method: :post,\n            client_id: federated_server[\"client_id\"],\n            client_secret: federated_server[\"client_secret\"],\n            site: base_url,\n            request_opts: [state: \"boruta\"],\n            authorize_url: endpoints[:authorize_url] || \"\",\n            token_url: endpoints[:token_url] || \"\",\n            redirect_uri: federated_redirect_url(backend, federated_server_name)\n          )\n\n        OAuth2.Client.put_serializer(client, \"application/json\", Jason)\n    end\n  end\n\n  @spec federated_server_scope(backend :: t(), federated_server_name :: String.t()) ::\n          server_scope :: String.t()\n  def federated_server_scope(\n        %__MODULE__{federated_servers: federated_servers},\n        federated_server_name\n      ) do\n    case Enum.find(federated_servers, fn federated_server ->\n           federated_server[\"name\"] == federated_server_name\n         end) do\n      nil ->\n        \"\"\n\n      federated_server ->\n        federated_server[\"scope\"] || \"\"\n    end\n  end\n\n  defp discover_federated_server_urls(\n         %__MODULE__{federated_servers: federated_servers} = backend,\n         federated_server,\n         discovery_path\n       ) do\n    base_url = URI.parse(federated_server[\"base_url\"])\n\n    case Finch.build(\n           :get,\n           URI.to_string(%{base_url | path: discovery_path}),\n           []\n         )\n         |> Finch.request(BorutaIdentity.Finch) do\n      {:ok, %Finch.Response{status: 200, body: body}} ->\n        discovery = Jason.decode!(body)\n\n        authorize_url = discovery[\"authorization_endpoint\"]\n\n        token_url = discovery[\"token_endpoint\"]\n\n        userinfo_url = discovery[\"userinfo_endpoint\"]\n\n        change(backend, %{\n          federated_servers:\n            Enum.map(federated_servers, fn %{\"name\" => name} = current_federated_server ->\n              case name == federated_server[\"name\"] do\n                true ->\n                  current_federated_server\n                  |> Map.put(\"authorize_path\", authorize_url)\n                  |> Map.put(\"token_path\", token_url)\n                  |> Map.put(\"userinfo_path\", userinfo_url)\n\n                false ->\n                  current_federated_server\n              end\n            end)\n        })\n        |> Repo.update()\n\n        %{\n          authorize_url: discovery[\"authorization_endpoint\"],\n          token_url: discovery[\"token_endpoint\"],\n          userinfo_url: discovery[\"userinfo_endpoint\"]\n        }\n\n      _error ->\n        %{}\n    end\n  end\n\n  @spec federated_redirect_url(backend :: t(), federated_server_name :: String.t()) ::\n          redirect_uri :: String.t()\n  def federated_redirect_url(%__MODULE__{id: backend_id}, federated_server_name) do\n    base_url = URI.parse(BorutaIdentityWeb.Endpoint.url())\n\n    base_url =\n      case base_url.port do\n        80 -> %{base_url | port: nil}\n        _ -> base_url\n      end\n\n    URI.to_string(%{\n      base_url\n      | path:\n          Routes.backends_path(\n            BorutaIdentityWeb.Endpoint,\n            :callback,\n            backend_id,\n            federated_server_name\n          )\n    })\n  end\n\n  @doc false\n  def changeset(backend, attrs) do\n    backend\n    |> cast(attrs, [\n      :id,\n      :type,\n      :name,\n      :is_default,\n      :create_default_organization,\n      :metadata_fields,\n      :password_hashing_alg,\n      :password_hashing_opts,\n      :ldap_pool_size,\n      :ldap_host,\n      :ldap_user_rdn_attribute,\n      :ldap_base_dn,\n      :ldap_ou,\n      :ldap_master_dn,\n      :ldap_master_password,\n      :smtp_from,\n      :smtp_relay,\n      :smtp_username,\n      :smtp_password,\n      :smtp_ssl,\n      :smtp_tls,\n      :smtp_port,\n      :federated_servers,\n      :verifiable_credentials,\n      :verifiable_presentations\n    ])\n    |> unique_constraint(:id, name: :backends_pkey)\n    |> validate_required([:name, :password_hashing_alg])\n    |> validate_metadata_fields()\n    |> validate_federated_servers()\n    |> validate_verifiable_credentials()\n    |> validate_verifiable_presentations()\n    |> validate_inclusion(:type, Enum.map(@backend_types, &Atom.to_string/1))\n    |> validate_inclusion(:smtp_tls, Enum.map(@smtp_tls_types, &Atom.to_string/1))\n    |> foreign_key_constraint(:identity_provider, name: :identity_providers_backend_id_fkey)\n    |> set_default()\n    |> validate_backend_by_type()\n  end\n\n  @doc false\n  def delete_changeset(%__MODULE__{id: backend_id} = backend) do\n    case default!().id == backend_id do\n      true ->\n        change(backend)\n        |> add_error(:is_default, \"Deleting a default backend is prohibited.\")\n\n      false ->\n        change(backend)\n    end\n    |> foreign_key_constraint(:identity_provider,\n      name: :identity_providers_backend_id_fkey,\n      message: \"This backend is linked to an identity provider. Please unlink it before continue.\"\n    )\n    |> foreign_key_constraint(:user,\n      name: :users_backend_id_fkey,\n      message: \"This backend has existing users. Please delete them before continue\"\n    )\n  end\n\n  defp validate_metadata_fields(\n         %Ecto.Changeset{changes: %{metadata_fields: metadata_fields}} = changeset\n       ) do\n    case ExJsonSchema.Validator.validate(@metadata_fields_schema, metadata_fields) do\n      :ok ->\n        changeset\n\n      {:error, errors} ->\n        Enum.reduce(errors, changeset, fn {message, path}, changeset ->\n          add_error(changeset, :metadata_fields, \"#{message} at #{path}\")\n        end)\n    end\n  end\n\n  defp validate_metadata_fields(changeset), do: changeset\n\n  defp validate_federated_servers(\n         %Ecto.Changeset{changes: %{federated_servers: federated_servers}} = changeset\n       ) do\n    Enum.reduce(federated_servers, changeset, fn federated_server, changeset ->\n      case ExJsonSchema.Validator.validate(@federated_server_schema, federated_server) do\n        :ok ->\n          changeset\n\n        {:error, errors} ->\n          Enum.reduce(errors, changeset, fn {message, path}, changeset ->\n            add_error(changeset, :federated_servers, \"#{message} at #{path}\")\n          end)\n      end\n    end)\n  end\n\n  defp validate_federated_servers(changeset), do: changeset\n\n  defp validate_verifiable_credentials(\n         %Ecto.Changeset{changes: %{verifiable_credentials: verifiable_credentials}} = changeset\n       ) do\n    Enum.reduce(verifiable_credentials, changeset, fn verifiable_credential, changeset ->\n      case ExJsonSchema.Validator.validate(@verifiable_credential_schema, verifiable_credential) do\n        :ok ->\n          changeset\n\n        {:error, errors} ->\n          Enum.reduce(errors, changeset, fn {message, path}, changeset ->\n            add_error(changeset, :verifiable_credentials, \"#{message} at #{path}\")\n          end)\n      end\n    end)\n  end\n\n  defp validate_verifiable_credentials(changeset), do: changeset\n\n  defp validate_verifiable_presentations(\n         %Ecto.Changeset{changes: %{verifiable_presentations: verifiable_presentations}} = changeset\n       ) do\n    Enum.reduce(verifiable_presentations, changeset, fn verifiable_presentation, changeset ->\n      case ExJsonSchema.Validator.validate(@verifiable_presentation_schema, verifiable_presentation) do\n        :ok ->\n          case Jason.decode(verifiable_presentation[\"presentation_definition\"]) do\n            {:ok, _map} ->\n              changeset\n            {:error, _error} ->\n              add_error(changeset, :verifiable_presentations, \"Verifiable presentation definition must be a valid JSON object\")\n          end\n\n        {:error, errors} ->\n          Enum.reduce(errors, changeset, fn {message, path}, changeset ->\n            add_error(changeset, :verifiable_presentations, \"#{message} at #{path}\")\n          end)\n      end\n    end)\n  end\n\n  defp validate_verifiable_presentations(changeset), do: changeset\n\n  defp set_default(%Ecto.Changeset{changes: %{is_default: false}} = changeset) do\n    Ecto.Changeset.add_error(\n      changeset,\n      :is_default,\n      \"There must be at least one default backend.\"\n    )\n  end\n\n  defp set_default(%Ecto.Changeset{changes: %{is_default: _is_default}} = changeset) do\n    # TODO use a transaction to change default backend\n    :ok = Boruta.Cache.delete({__MODULE__, :default})\n    case Ecto.Changeset.change(default!(), %{is_default: false}) |> Repo.update() do\n      {:ok, _backend} ->\n\n        changeset\n\n      {:error, changeset} ->\n        Ecto.Changeset.add_error(\n          changeset,\n          :is_default,\n          \"Cannot remove value from the existing default backend.\"\n        )\n    end\n  rescue\n    Ecto.NoResultsError -> changeset\n  end\n\n  defp set_default(changeset), do: changeset\n\n  defp validate_backend_by_type(changeset) do\n    type = get_field(changeset, :type)\n\n    validate_backend(changeset, String.to_atom(type))\n  end\n\n  defp validate_backend(changeset, Internal) do\n    changeset\n    |> validate_inclusion(:password_hashing_alg, Map.keys(@password_hashing_modules))\n    |> validate_password_hashing_opts()\n  end\n\n  defp validate_backend(changeset, Ldap) do\n    changeset\n    |> validate_required([\n      :ldap_pool_size,\n      :ldap_host,\n      :ldap_user_rdn_attribute,\n      :ldap_base_dn\n    ])\n    |> validate_inclusion(:ldap_pool_size, 1..50)\n  end\n\n  defp validate_backend(changeset, _implementation), do: changeset\n\n  defp validate_password_hashing_opts(changeset) do\n    alg = fetch_field!(changeset, :password_hashing_alg)\n    opts = fetch_field!(changeset, :password_hashing_opts)\n\n    case ExJsonSchema.Validator.validate(@password_hashing_opts_schema[alg], opts) do\n      :ok ->\n        changeset\n\n      {:error, errors} ->\n        Enum.reduce(errors, changeset, fn {message, path}, changeset ->\n          add_error(changeset, :password_hashing_opts, \"#{message} at #{path}\")\n        end)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/identity_providers/backend_role.ex",
    "content": "defmodule BorutaIdentity.IdentityProviders.BackendRole do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias BorutaIdentity.Accounts.Role\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  @type t :: %__MODULE__{\n    id: String.t(),\n    backend_id: String.t(),\n    role_id: String.t()\n  }\n\n  @primary_key {:id, :binary_id, autogenerate: true}\n  @foreign_key_type :binary_id\n  schema \"backends_roles\" do\n    belongs_to :role, Role\n    belongs_to :backend, Backend\n\n    timestamps()\n  end\n\n  @doc false\n  def changeset(role_scope, attrs) do\n    role_scope\n    |> cast(attrs, [:role_id, :backend_id])\n    |> validate_required([:role_id, :backend_id])\n    |> unique_constraint([:role_id, :backend_id])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/identity_providers/client_identity_provider.ex",
    "content": "defmodule BorutaIdentity.IdentityProviders.ClientIdentityProvider do\n  @moduledoc false\n\n  use Ecto.Schema\n\n  import Ecto.Changeset\n\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n\n  @type t :: %__MODULE__{\n    client_id: String.t(),\n    identity_provider: IdentityProvider.t() | Ecto.Association.NotLoaded.t(),\n    inserted_at: DateTime.t(),\n    updated_at: DateTime.t()\n  }\n\n  @foreign_key_type :binary_id\n  @primary_key {:id, :binary_id, autogenerate: true}\n  schema \"clients_identity_providers\" do\n    field(:client_id, :binary_id)\n\n    belongs_to(:identity_provider, IdentityProvider)\n\n    timestamps()\n  end\n\n  @doc false\n  def changeset(client_identity_provider, attrs) do\n    client_identity_provider\n    |> cast(attrs, [:client_id, :identity_provider_id])\n    |> validate_required([:client_id, :identity_provider_id])\n    |> validate_format(\n      :identity_provider_id,\n      ~r/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/\n    )\n    |> validate_format(\n      :client_id,\n      ~r/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/\n    )\n    |> foreign_key_constraint(:identity_provider_id)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/identity_providers/identity_provider.ex",
    "content": "defmodule BorutaIdentity.IdentityProviders.IdentityProvider do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.IdentityProviders.ClientIdentityProvider\n  alias BorutaIdentity.IdentityProviders.Template\n  alias BorutaIdentity.Repo\n\n  @type t :: %__MODULE__{\n          name: String.t(),\n          backend_id: String.t(),\n          backend: Backend.t(),\n          registrable: boolean(),\n          totpable: boolean(),\n          enforce_totp: boolean(),\n          confirmable: boolean(),\n          authenticable: boolean(),\n          check_password: boolean(),\n          reset_password: boolean(),\n          client_identity_providers:\n            list(ClientIdentityProvider.t()) | Ecto.Association.NotLoaded.t(),\n          inserted_at: DateTime.t(),\n          updated_at: DateTime.t()\n        }\n\n  @features %{\n    authenticable: [\n      # BorutaIdentity.Accounts.Sessions\n      :initialize_session,\n      # BorutaIdentity.Accounts.FederatedSessions\n      :create_federated_session,\n      # BorutaIdentity.Totp\n      :initialize_totp,\n      # BorutaIdentity.Accounts.Sessions\n      :create_session,\n      # BorutaIdentity.Accounts.Sessions\n      :delete_session,\n      # BorutaIdentity.Accounts.Consents\n      :initialize_consent,\n      # BorutaIdentity.Accounts.ChooseSessions\n      :initialize_choose_session\n    ],\n    totpable: [\n      # BorutaIdentity.Totp\n      :initialize_totp_registration,\n      # BorutaIdentity.Totp\n      :register_totp,\n      # BorutaIdentity.Totp\n      :initialize_totp,\n      # BorutaIdentity.Totp\n      :authenticate_totp\n    ],\n    webauthnable: [\n      # BorutaIdentity.Totp\n      :initialize_webauthn_registration,\n      # BorutaIdentity.Webauthn\n      :register_webauthn,\n      # BorutaIdentity.Webauthn\n      :initialize_webauthn,\n      # BorutaIdentity.Webauthn\n      :authenticate_webauthn\n    ],\n    registrable: [\n      # BorutaIdentity.Accounts.Registrations\n      :initialize_registration,\n      # BorutaIdentity.Accounts.Registrations\n      :register\n    ],\n    user_editable: [\n      # BorutaIdentity.Accounts.Settings\n      :initialize_edit_user,\n      # BorutaIdentity.Accounts.Settings\n      :update_user\n    ],\n    confirmable: [\n      # BorutaIdentity.Accounts.Confirmations\n      :initialize_confirmation_instructions,\n      # BorutaIdentity.Accounts.Confirmations\n      :send_confirmation_instructions,\n      # BorutaIdentity.Accounts.Confirmations\n      :confirm_user\n    ],\n    reset_password: [\n      # BorutaIdentity.Accounts.ResetPasswords\n      :initialize_password_instructions,\n      # BorutaIdentity.Accounts.ResetPasswords\n      :send_reset_password_instructions,\n      # BorutaIdentity.Accounts.ResetPasswords\n      :initialize_password_reset,\n      # BorutaIdentity.Accounts.ResetPasswords\n      :reset_password\n    ],\n    consentable: [\n      # BorutaIdentity.Accounts.Consents\n      :consent\n    ],\n    destroyable: [\n      # BorutaIdentity.Accounts.Settings\n      :destroy_user\n    ]\n  }\n\n  @primary_key {:id, :binary_id, autogenerate: true}\n  @foreign_key_type :binary_id\n  schema \"identity_providers\" do\n    field(:name, :string)\n    field(:choose_session, :boolean, default: true)\n    field(:totpable, :boolean, default: false)\n    field(:enforce_totp, :boolean, default: false)\n    field(:webauthnable, :boolean, default: false)\n    field(:enforce_webauthn, :boolean, default: false)\n    field(:registrable, :boolean, default: false)\n    field(:user_editable, :boolean, default: false)\n    field(:confirmable, :boolean, default: false)\n    field(:consentable, :boolean, default: false)\n    field(:authenticable, :boolean, default: true, virtual: true)\n    field(:destroyable, :boolean, default: true, virtual: true)\n    field(:check_password, :boolean, default: true)\n    field(:reset_password, :boolean, default: true, virtual: true)\n\n    has_many(:client_identity_providers, ClientIdentityProvider)\n    has_many(:templates, Template, on_replace: :delete_if_exists)\n    belongs_to(:backend, Backend)\n\n    timestamps()\n  end\n\n  @spec template(identity_provider :: t(), type :: atom()) :: Template.t() | nil\n  def template(%__MODULE__{templates: templates} = identity_provider, type)\n      when is_list(templates) do\n    case Enum.find(templates, fn\n           %Template{type: template_type} -> Atom.to_string(type) == template_type\n         end) do\n      nil ->\n        template = Template.default_template(type)\n\n        template &&\n          %{\n            template\n            | identity_provider_id: identity_provider.id,\n              identity_provider: identity_provider\n          }\n\n      template ->\n        %{template | identity_provider: identity_provider}\n    end\n  end\n\n  # TODO rename to backend\n  @spec implementation(client_identity_provider :: %__MODULE__{}) :: implementation :: atom()\n  def implementation(%__MODULE__{backend: backend}) do\n    Backend.implementation(backend)\n  end\n\n  @spec check_feature(identity_provider :: t(), action_name :: atom()) ::\n          :ok | {:error, reason :: String.t()}\n  def check_feature(identity_provider, requested_action_name) do\n    backend_features = apply(Backend.implementation(identity_provider.backend), :features, [])\n\n    with {feature_name, _actions} <-\n           Enum.find(@features, fn {_feature_name, actions} ->\n             Enum.member?(actions, requested_action_name)\n           end),\n         {:ok, true} <- identity_provider |> Map.from_struct() |> Map.fetch(feature_name),\n         true <- Enum.member?(backend_features, feature_name) do\n      :ok\n    else\n      false -> {:error, \"Feature is not enabled for identity provider backend implementation.\"}\n      {:ok, false} -> {:error, \"Feature is not enabled for client identity provider.\"}\n      nil -> {:error, \"This provider does not support this feature.\"}\n    end\n  end\n\n  @doc false\n  def changeset(identity_provider, attrs) do\n    identity_provider\n    |> Repo.preload(:templates)\n    |> cast(attrs, [\n      :id,\n      :name,\n      :check_password,\n      :choose_session,\n      :totpable,\n      :enforce_totp,\n      :webauthnable,\n      :enforce_webauthn,\n      :registrable,\n      :user_editable,\n      :consentable,\n      :confirmable,\n      :backend_id\n    ])\n    |> unique_constraint(:id, name: :relying_parties_pkey)\n    |> validate_required([:name, :backend_id])\n    |> unique_constraint(:name)\n    |> cast_assoc(:templates, with: &Template.assoc_changeset/2)\n    |> foreign_key_constraint(:backend_id, name: :identity_providers_backend_id_fkey)\n  end\n\n  @doc false\n  def delete_changeset(identity_provider) do\n    changeset = change(identity_provider)\n\n    case Repo.preload(identity_provider, :client_identity_providers) do\n      %__MODULE__{client_identity_providers: []} ->\n        changeset\n\n      %__MODULE__{client_identity_providers: client_identity_providers} ->\n        client_ids =\n          Enum.map(client_identity_providers, fn %ClientIdentityProvider{client_id: client_id} ->\n            client_id\n          end)\n\n        add_error(\n          changeset,\n          :client_identity_providers,\n          \"identity provider is associated with client(s) #{Enum.join(client_ids, \", \")}\"\n        )\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/identity_providers/template.ex",
    "content": "defmodule BorutaIdentity.IdentityProviders.Template do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n\n  @type t :: %__MODULE__{\n          id: String.t() | nil,\n          type: String.t(),\n          layout: t(),\n          default: boolean(),\n          content: String.t(),\n          inserted_at: DateTime.t() | nil,\n          updated_at: DateTime.t() | nil\n        }\n\n  @template_types [\n    :layout,\n    :new_session,\n    :choose_session,\n    :new_totp_registration,\n    :new_totp_authentication,\n    :new_webauthn_registration,\n    :new_webauthn_authentication,\n    :new_registration,\n    :new_consent,\n    :new_reset_password,\n    :edit_reset_password,\n    :new_confirmation_instructions,\n    :edit_user,\n    :credential_offer,\n    :cross_device_presentation\n  ]\n  @type template_type ::\n          :layout\n          | :new_session\n          | :choose_session\n          | :new_consent\n          | :new_totp_registration\n          | :new_totp_authentication\n          | :new_webauthn_registration\n          | :new_webauthn_authentication\n          | :new_registration\n          | :new_reset_password\n          | :edit_reset_password\n          | :new_confirmation_instructions\n          | :edit_user\n          | :credential_offer\n          | :cross_device_presentation\n\n  @default_templates %{\n    layout:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/layouts/app.mustache\")\n      |> File.read!(),\n    new_session:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/sessions/new.mustache\")\n      |> File.read!(),\n    choose_session:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/choose_session/index.mustache\")\n      |> File.read!(),\n    new_totp_registration:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/mfa/totp/registration.mustache\")\n      |> File.read!(),\n    new_totp_authentication:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/mfa/totp/authentication.mustache\")\n      |> File.read!(),\n    new_webauthn_registration:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/mfa/webauthn/registration.mustache\")\n      |> File.read!(),\n    new_webauthn_authentication:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/mfa/webauthn/authentication.mustache\")\n      |> File.read!(),\n    new_registration:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/registrations/new.mustache\")\n      |> File.read!(),\n    new_consent:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/consents/new.mustache\")\n      |> File.read!(),\n    new_reset_password:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/reset_passwords/new.mustache\")\n      |> File.read!(),\n    edit_reset_password:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/reset_passwords/edit.mustache\")\n      |> File.read!(),\n    new_confirmation_instructions:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/confirmations/new.mustache\")\n      |> File.read!(),\n    edit_user:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/settings/edit_user.mustache\")\n      |> File.read!(),\n    credential_offer:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/settings/credential_offer.mustache\")\n      |> File.read!(),\n    cross_device_presentation:\n      :code.priv_dir(:boruta_identity)\n      |> Path.join(\"templates/settings/verifiable_presentation.mustache\")\n      |> File.read!()\n  }\n\n  @foreign_key_type :binary_id\n  @primary_key {:id, :binary_id, autogenerate: true}\n  schema \"identity_provider_templates\" do\n    field(:content, :string)\n    field(:type, :string)\n\n    field(:default, :boolean, virtual: true, default: false)\n    field(:layout, :any, virtual: true, default: nil)\n\n    belongs_to(:identity_provider, IdentityProvider)\n\n    timestamps()\n  end\n\n  def template_types, do: @template_types\n\n  @spec default_content(type :: template_type()) :: template_content :: String.t()\n  def default_content(type) when type in @template_types, do: @default_templates[type]\n\n  @spec default_template(type :: template_type()) :: %__MODULE__{} | nil\n  def default_template(type) when type in @template_types do\n    %__MODULE__{\n      default: true,\n      type: Atom.to_string(type),\n      content: default_content(type)\n    }\n  end\n\n  def default_template(_type), do: nil\n\n  @doc false\n  def changeset(template, attrs) do\n    template\n    |> cast(attrs, [:type, :content, :identity_provider_id])\n    |> validate_required([:type, :identity_provider_id, :content])\n    |> validate_inclusion(:type, Enum.map(@template_types, &Atom.to_string/1))\n    |> foreign_key_constraint(:identity_provider_id)\n    |> put_default()\n  end\n\n  @doc false\n  def assoc_changeset(template, attrs) do\n    template\n    |> cast(attrs, [:type, :content])\n    |> validate_required([:type, :content])\n    |> validate_inclusion(:type, Enum.map(@template_types, &Atom.to_string/1))\n    |> put_default()\n  end\n\n  defp put_default(changeset) do\n    case get_field(changeset, :content) do\n      content when not is_nil(content) ->\n        changeset\n\n      _ ->\n        change(\n          changeset,\n          content: default_template(changeset |> fetch_field!(:type) |> String.to_atom()).content\n        )\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/identity_providers.ex",
    "content": "defmodule BorutaIdentity.IdentityProviders do\n  @moduledoc \"\"\"\n  The IdentityProviders context.\n  \"\"\"\n\n  use Nebulex.Caching\n  import Ecto.Query, warn: false\n  alias BorutaIdentity.Repo\n\n  alias Boruta.Ecto.Scopes\n  alias Boruta.Oauth.Scope\n  alias BorutaIdentity.IdentityProviders.BackendRole\n  alias BorutaIdentity.IdentityProviders.ClientIdentityProvider\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n\n  def list_identity_providers do\n    Repo.all(\n      from idp in IdentityProvider,\n        join: b in assoc(idp, :backend),\n        left_join: et in assoc(b, :email_templates),\n        preload: [backend: {b, email_templates: et}]\n    )\n  end\n\n  def get_identity_provider!(id) do\n    case Ecto.UUID.cast(id) do\n      {:ok, id} ->\n        Repo.one!(\n          from idp in IdentityProvider,\n            join: b in assoc(idp, :backend),\n            left_join: et in assoc(b, :email_templates),\n            where: idp.id == ^id,\n            preload: [backend: {b, email_templates: et}]\n        )\n\n      _ ->\n        raise Ecto.NoResultsError, queryable: IdentityProvider\n    end\n  end\n\n  def create_identity_provider(attrs \\\\ %{}) do\n    with {:ok, identity_provider} <-\n           %IdentityProvider{}\n           |> IdentityProvider.changeset(attrs)\n           |> Repo.insert() do\n      {:ok, Repo.preload(identity_provider, :backend)}\n    end\n  end\n\n  def update_identity_provider(%IdentityProvider{} = identity_provider, attrs) do\n    clear_identity_provider_by_client_id_cache()\n\n    identity_provider\n    |> IdentityProvider.changeset(attrs)\n    |> Repo.update()\n  end\n\n  def delete_identity_provider(%IdentityProvider{} = identity_provider) do\n    clear_identity_provider_by_client_id_cache()\n\n    identity_provider\n    |> IdentityProvider.delete_changeset()\n    |> Repo.delete()\n  end\n\n  def change_identity_provider(%IdentityProvider{} = identity_provider, attrs \\\\ %{}) do\n    IdentityProvider.changeset(identity_provider, attrs)\n  end\n\n  def upsert_client_identity_provider(client_id, identity_provider_id) do\n    clear_identity_provider_by_client_id_cache()\n\n    %ClientIdentityProvider{}\n    |> ClientIdentityProvider.changeset(%{\n      client_id: client_id,\n      identity_provider_id: identity_provider_id\n    })\n    |> Repo.insert(\n      on_conflict: [set: [identity_provider_id: identity_provider_id]],\n      conflict_target: :client_id\n    )\n  end\n\n  defp clear_identity_provider_by_client_id_cache do\n    Boruta.Cache.delete_all(\n      [\n        {\n          {:entry,\n           {BorutaIdentity.IdentityProviders, :identity_provider_by_client_id, :\"$1\"},\n           :\"$2\", :\"$3\", :\"$4\"},\n          [],\n          [true]\n        }\n      ]\n    )\n  end\n\n  def remove_client_identity_provider(client_id) do\n    query =\n      from(cr in ClientIdentityProvider,\n        where: cr.client_id == ^client_id,\n        select: cr\n      )\n\n    case Repo.delete_all(query) do\n      {1, [client_identity_provider]} ->\n        {:ok, client_identity_provider}\n\n      {0, []} ->\n        {:ok, nil}\n    end\n  end\n\n  @decorate cacheable(\n              key: {__MODULE__, :identity_provider_by_client_id, client_id},\n              cache: Boruta.Cache\n            )\n  def get_identity_provider_by_client_id(client_id) do\n    case Ecto.UUID.cast(client_id) do\n      {:ok, client_id} ->\n        Repo.one(\n          from(idp in IdentityProvider,\n            join: b in assoc(idp, :backend),\n            left_join: et in assoc(b, :email_templates),\n            join: cidp in assoc(idp, :client_identity_providers),\n            where: cidp.client_id == ^client_id,\n            preload: [backend: {b, email_templates: et}]\n          )\n        )\n\n      :error ->\n        nil\n    end\n  end\n\n  alias BorutaIdentity.IdentityProviders.Template\n\n  @decorate cacheable(\n              key: {__MODULE__, :identity_provider_template, identity_provider_id, type},\n              cache: Boruta.Cache\n            )\n  def get_identity_provider_template!(identity_provider_id, type) do\n    with %IdentityProvider{} = identity_provider_with_templates <-\n           Repo.one(\n             from(idp in IdentityProvider,\n               left_join: t in assoc(idp, :templates),\n               join: b in assoc(idp, :backend),\n               where: idp.id == ^identity_provider_id,\n               preload: [backend: b, templates: t]\n             )\n           ),\n         %Template{} = template <-\n           IdentityProvider.template(identity_provider_with_templates, type) do\n      %{template | layout: IdentityProvider.template(identity_provider_with_templates, :layout)}\n    else\n      nil -> raise Ecto.NoResultsError, queryable: Template\n    end\n  end\n\n  def upsert_template(%Template{id: template_id} = template, attrs) do\n    :ok =\n      Boruta.Cache.delete(\n        {__MODULE__, :identity_provider_template, template.identity_provider_id,\n         String.to_atom(template.type)}\n      )\n\n    changeset = Template.changeset(template, attrs)\n\n    case template_id do\n      nil -> Repo.insert(changeset)\n      _ -> Repo.update(changeset)\n    end\n  end\n\n  def delete_identity_provider_template!(identity_provider_id, type) do\n    template_type = Atom.to_string(type)\n\n    with :ok <-\n           Boruta.Cache.delete(\n             {__MODULE__, :identity_provider_template, identity_provider_id, type}\n           ),\n         {1, _results} <-\n           Repo.delete_all(\n             from(t in Template,\n               join: idp in assoc(t, :identity_provider),\n               where:\n                 idp.id == ^identity_provider_id and\n                   t.type == ^template_type\n             )\n           ),\n         %Template{} = template <- get_identity_provider_template!(identity_provider_id, type) do\n      template\n    else\n      {0, nil} -> raise Ecto.NoResultsError, queryable: Template\n      nil -> raise Ecto.NoResultsError, queryable: Template\n    end\n  end\n\n  alias BorutaIdentity.Accounts.EmailTemplate\n  alias BorutaIdentity.Accounts.Ldap\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  def list_backends do\n    Repo.all(Backend)\n  end\n\n  @decorate cacheable(key: {__MODULE__, :backend, id}, cache: Boruta.Cache)\n  def get_backend!(id) do\n    case Ecto.UUID.cast(id) do\n      {:ok, id} -> Repo.get!(Backend, id)\n      _ -> raise Ecto.NoResultsError, queryable: Backend\n    end\n  end\n\n  # TODO client backend association\n  # def get_backend_by_client_id(client_id) do\n  #   case Ecto.UUID.cast(client_id) do\n  #     {:ok, client_id} ->\n  #       Repo.one(\n  #         from(b in Backend,\n  #           join: idp in assoc(b, :identity_providers),\n  #           join: cidp in assoc(idp, :client_identity_providers),\n  #           where: cidp.client_id == ^client_id\n  #         )\n  #       )\n\n  #     :error ->\n  #       nil\n  #   end\n  # end\n\n  def create_backend(attrs \\\\ %{}) do\n    with {:ok, backend} <-\n           %Backend{type: \"Elixir.BorutaIdentity.Accounts.Internal\"}\n           |> Backend.changeset(attrs)\n           |> Repo.insert() do\n      update_backend_roles(backend, attrs[\"roles\"] || [])\n    end\n  end\n\n  def update_backend(%Backend{} = backend, attrs) do\n    :ok = Boruta.Cache.delete({__MODULE__, :backend, backend.id})\n    if backend.is_default do\n      :ok = Boruta.Cache.delete({Backend, :default})\n    end\n\n    ldap_pool_name = Ldap.pool_name(backend)\n\n    with {:ok, backend} <-\n           backend\n           |> Backend.changeset(attrs)\n           |> Repo.update(),\n         {:ok, backend} <- update_backend_roles(backend, attrs[\"roles\"] || []) do\n      Process.whereis(ldap_pool_name) &&\n        NimblePool.stop(ldap_pool_name)\n\n      {:ok, backend}\n    end\n  end\n\n  @spec update_backend_roles(backend :: %Backend{}, roles :: list(map())) ::\n          {:ok, %Backend{}} | {:error, Ecto.Changeset.t()}\n  def update_backend_roles(%Backend{id: backend_id} = backend, roles) do\n    :ok = Boruta.Cache.delete({__MODULE__, :backend, backend_id})\n    if backend.is_default do\n      :ok = Boruta.Cache.delete({Backend, :default})\n    end\n\n    Repo.delete_all(from(s in BackendRole, where: s.backend_id == ^backend_id))\n\n    case Enum.reduce(roles, Ecto.Multi.new(), fn attrs, multi ->\n           changeset =\n             BackendRole.changeset(\n               %BackendRole{},\n               %{\n                 \"role_id\" => attrs[\"id\"] || attrs[:role_id],\n                 \"backend_id\" => backend.id\n               }\n             )\n\n           Ecto.Multi.insert(multi, \"role_-#{SecureRandom.uuid()}\", changeset)\n         end)\n         |> Repo.transaction() do\n      {:ok, _result} ->\n        {:ok, backend |> Repo.reload()}\n\n      {:error, _multi_name, %Ecto.Changeset{} = changeset, _changes} ->\n        {:error, changeset}\n    end\n  end\n\n  @spec get_backend_roles(backend_id :: String.t()) :: backend :: list(BackendRole.t()) | nil\n  def get_backend_roles(backend_id) do\n    scopes = Scopes.all()\n\n    Repo.all(\n      from(br in BackendRole,\n        left_join: r in assoc(br, :role),\n        left_join: rs in assoc(r, :role_scopes),\n        where: br.backend_id == ^backend_id,\n        preload: [role: {r, [role_scopes: rs]}]\n      )\n    )\n    |> Enum.map(fn %{role: role} ->\n      %{\n        role\n        | scopes:\n            role.role_scopes\n            |> Enum.map(fn role_scope ->\n              Enum.find(scopes, fn %{id: id} -> id == role_scope.scope_id end)\n            end)\n            |> Enum.flat_map(fn\n              %{id: id, name: name} -> [%Scope{id: id, name: name}]\n              _ -> []\n            end)\n      }\n    end)\n  end\n\n  def delete_backend(%Backend{} = backend) do\n    :ok = Boruta.Cache.delete({__MODULE__, :backend, backend.id})\n    if backend.is_default do\n      :ok = Boruta.Cache.delete({Backend, :default})\n    end\n\n    ldap_pool_name = Ldap.pool_name(backend)\n\n    with {:ok, backend} <-\n           backend\n           |> Backend.delete_changeset()\n           |> Repo.delete() do\n      Process.whereis(ldap_pool_name) &&\n        NimblePool.stop(ldap_pool_name)\n\n      {:ok, backend}\n    end\n  end\n\n  @doc \"\"\"\n  Returns an `%Ecto.Changeset{}` for tracking backend changes.\n\n  ## Examples\n\n      iex> change_backend(backend)\n      %Ecto.Changeset{data: %Backend{}}\n\n  \"\"\"\n  def change_backend(%Backend{} = backend, attrs \\\\ %{}) do\n    Backend.changeset(backend, attrs)\n  end\n\n  def get_backend_email_template!(backend_id, type) do\n    with %Backend{} = backend <-\n           Repo.one(\n             from(b in Backend,\n               left_join: t in assoc(b, :email_templates),\n               where: b.id == ^backend_id,\n               preload: [email_templates: t]\n             )\n           ),\n         %EmailTemplate{} = template <- Backend.email_template(backend, type) do\n      template\n    else\n      nil -> raise Ecto.NoResultsError, queryable: Template\n    end\n  end\n\n  def upsert_email_template(%EmailTemplate{id: template_id} = template, attrs) do\n    changeset = EmailTemplate.changeset(template, attrs)\n\n    case template_id do\n      nil -> Repo.insert(changeset)\n      _ -> Repo.update(changeset)\n    end\n  end\n\n  @doc \"\"\"\n  Deletes an email template.\n\n  ## Examples\n\n      iex> delete_email_template!(template, :reset_password)\n      {:ok, %EmailTemplate{}}\n\n      iex> delete_email_template!(template, :unknown)\n      ** (Ecto.NoResultsError)\n\n  \"\"\"\n\n  def delete_email_template!(backend_id, type) do\n    template_type = Atom.to_string(type)\n\n    with {1, _results} <-\n           Repo.delete_all(\n             from(t in EmailTemplate,\n               join: b in assoc(t, :backend),\n               where:\n                 b.id == ^backend_id and\n                   t.type == ^template_type\n             )\n           ),\n         %EmailTemplate{} = template <- get_backend_email_template!(backend_id, type) do\n      template\n    else\n      {0, nil} -> raise Ecto.NoResultsError, queryable: Template\n      nil -> raise Ecto.NoResultsError, queryable: Template\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/ldap_repo.ex",
    "content": "defmodule BorutaIdentity.LdapRepo do\n  @moduledoc false\n\n  alias BorutaIdentity.Accounts.Ldap\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  @type user_properties :: %{\n          String.t() => list(String.t())\n        }\n\n  @callback open(host :: String.t()) :: {:ok, pid()} | {:error, reason :: any()}\n  @callback open(host :: String.t(), opts :: Keyword.t()) ::\n              {:ok, pid()} | {:error, reason :: any()}\n  @callback close(handle :: pid()) :: :ok\n  @callback simple_bind(handle :: pid(), dn :: String.t(), password :: String.t()) ::\n              :ok | {:error, any()}\n  @callback search(handle :: pid, backend :: Backend.t(), username :: String.t()) ::\n              {:ok, {dn :: String.t(), user_properties :: user_properties()}} | {:error, any()}\n  @callback modify(\n              handle :: pid,\n              backend :: Backend.t(),\n              user :: Ldap.User.t(),\n              username :: String.t()\n            ) ::\n              :ok | {:error, any()}\n  @callback modify_password(\n              handle :: pid,\n              user :: Ldap.User.t(),\n              new_password :: String.t(),\n              old_password :: String.t()\n            ) ::\n              :ok | {:error, any()}\n  @callback modify_password(\n              handle :: pid,\n              user :: Ldap.User.t(),\n              new_password :: String.t()\n            ) ::\n              :ok | {:error, any()}\n\n  def open(host, opts \\\\ []), do: impl().open(host, opts)\n\n  def close(handle), do: impl().close(handle)\n\n  def simple_bind(handle, dn, password), do: impl().simple_bind(handle, dn, password)\n\n  def search(handle, backend, username), do: impl().search(handle, backend, username)\n\n  def modify(handle, backend, user, username), do: impl().modify(handle, backend, user, username)\n\n  def modify_password(handle, user, new_password, old_password),\n    do: impl().modify_password(handle, user, new_password, old_password)\n\n  def modify_password(handle, user, new_password),\n    do: impl().modify_password(handle, user, new_password)\n\n  defp impl do\n    case Application.get_env(:boruta_identity, BorutaIdentity.LdapRepo) do\n      [adapter: adapter] ->\n        adapter\n\n      _ ->\n        BorutaIdentity.LdapAdapter\n    end\n  end\nend\n\ndefmodule BorutaIdentity.LdapAdapter do\n  @moduledoc false\n\n  @behaviour BorutaIdentity.LdapRepo\n\n  alias BorutaIdentity.Accounts.Ldap\n\n  @impl BorutaIdentity.LdapRepo\n  def open(host, opts \\\\ []) do\n    :eldap.open([String.to_charlist(host)], opts)\n  end\n\n  @impl BorutaIdentity.LdapRepo\n  def close(handle) do\n    :eldap.close(handle)\n  end\n\n  @impl BorutaIdentity.LdapRepo\n  def simple_bind(handle, dn, password) do\n    :eldap.simple_bind(handle, String.to_charlist(dn), password)\n  rescue\n    _ -> {:error, \"Authentication failed.\"}\n  end\n\n  @impl BorutaIdentity.LdapRepo\n  def search(handle, backend, username) do\n    user_rdn_attribute = String.to_charlist(backend.ldap_user_rdn_attribute)\n\n    base_dn =\n      [backend.ldap_ou, backend.ldap_base_dn]\n      |> Enum.reject(&is_nil/1)\n      |> Enum.join(\",\")\n\n    case :eldap.search(handle,\n           base: base_dn,\n           filter: :eldap.equalityMatch(user_rdn_attribute, String.to_charlist(username))\n         ) do\n      {:ok,\n       {:eldap_search_result,\n        [\n          {:eldap_entry, dn, user_properties}\n        ], _, _}} ->\n        user_properties =\n          Enum.map(user_properties, fn {key, values} ->\n            {to_string(key), List.first(values) |> to_string()}\n          end)\n          |> Enum.into(%{})\n\n        {:ok, {to_string(dn), user_properties}}\n\n      {:ok, {:eldap_search_result, _results, _, _}} ->\n        {:error, \"Multiple users matched the given #{user_rdn_attribute}.\"}\n\n      {:error, error} ->\n        {:error, error}\n    end\n  end\n\n  @impl BorutaIdentity.LdapRepo\n  def modify(handle, backend, %Ldap.User{dn: dn}, username) do\n    user_rdn_attribute = String.to_charlist(backend.ldap_user_rdn_attribute)\n    username = String.to_charlist(username)\n\n    :eldap.modify(handle, String.to_charlist(dn), [\n      :eldap.mod_replace(user_rdn_attribute, [username])\n    ])\n  end\n\n  @impl BorutaIdentity.LdapRepo\n  def modify_password(handle, %Ldap.User{dn: dn}, new_password, old_password)\n      when byte_size(new_password) > 0 do\n    :eldap.modify_password(\n      handle,\n      String.to_charlist(dn),\n      String.to_charlist(new_password),\n      String.to_charlist(old_password)\n    )\n  end\n\n  @impl BorutaIdentity.LdapRepo\n  def modify_password(handle, %Ldap.User{dn: dn}, new_password)\n      when byte_size(new_password) > 0 do\n    :eldap.modify_password(\n      handle,\n      String.to_charlist(dn),\n      String.to_charlist(new_password)\n    )\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/logger.ex",
    "content": "defmodule BorutaIdentity.Logger do\n  @moduledoc false\n\n  require Logger\n\n  alias BorutaIdentityWeb.ErrorHelpers\n\n  def start do\n    handlers = [\n      {\n        :boruta_identity_requests,\n        [:boruta_identity, :endpoint, :stop],\n        &__MODULE__.boruta_identity_request_handler/4\n      },\n      {\n        :authentication_log_in_success,\n        [:authentication, :log_in, :success],\n        &__MODULE__.authentication_log_in_success_handler/4\n      },\n      {\n        :authentication_log_in_failure,\n        [:authentication, :log_in, :failure],\n        &__MODULE__.authentication_log_in_failure_handler/4\n      },\n      {\n        :authentication_log_out_success,\n        [:authentication, :log_out, :success],\n        &__MODULE__.authentication_log_out_success_handler/4\n      },\n      {\n        :registration_create_success,\n        [:registration, :create, :success],\n        &__MODULE__.registration_create_success_handler/4\n      },\n      {\n        :registration_create_failure,\n        [:registration, :create, :failure],\n        &__MODULE__.registration_create_failure_handler/4\n      },\n      {\n        :registration_confirm_success,\n        [:registration, :confirm, :success],\n        &__MODULE__.registration_confirm_success_handler/4\n      },\n      {\n        :registration_confirm_failure,\n        [:registration, :confirm, :failure],\n        &__MODULE__.registration_confirm_failure_handler/4\n      },\n      {\n        :registration_update_success,\n        [:registration, :update, :success],\n        &__MODULE__.registration_update_success_handler/4\n      },\n      {\n        :registration_update_failure,\n        [:registration, :update, :failure],\n        &__MODULE__.registration_update_failure_handler/4\n      },\n      {\n        :authorization_consent_success,\n        [:authorization, :consent, :success],\n        &__MODULE__.authorization_consent_success_handler/4\n      },\n      {\n        :authorization_consent_failure,\n        [:authorization, :consent, :failure],\n        &__MODULE__.authorization_consent_failure_handler/4\n      }\n    ]\n\n    for {handler_id, event_name, fun} <- handlers do\n      :telemetry.attach(handler_id, event_name, fun, :ok)\n    end\n  end\n\n  def boruta_identity_request_handler(_, %{duration: duration}, %{conn: conn} = metadata, _) do\n    remote_ip = :inet.ntoa(conn.remote_ip)\n\n    case log_level(metadata[:options][:log], conn) do\n      false ->\n        :ok\n\n      level ->\n        Logger.log(\n          level,\n          fn ->\n            %{method: method, request_path: path, status: status, state: state} = conn\n            status = Integer.to_string(status)\n\n            [\n              \"boruta_identity\",\n              ?\\s,\n              method,\n              ?\\s,\n              path,\n              \" - \",\n              connection_type(state),\n              ?\\s,\n              status,\n              \" from \",\n              remote_ip,\n              \" in \",\n              duration(duration)\n            ]\n          end,\n          type: :request\n        )\n    end\n  end\n\n  def authentication_log_in_success_handler(\n        _,\n        _measurements,\n        %{sub: sub, backend: backend, client_id: client_id},\n        _\n      ) do\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"authentication\",\n          ?\\s,\n          \"log_in\",\n          \" - \",\n          \"success\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"sub\", sub),\n          log_attribute(\"backend_id\", backend.id),\n          log_attribute(\"provider\", backend.type)\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  def authentication_log_in_failure_handler(\n        _,\n        _measurements,\n        %{message: message, client_id: client_id},\n        _\n      ) do\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"authentication\",\n          ?\\s,\n          \"log_in\",\n          \" - \",\n          \"failure\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"message\", ~s{\"#{message}\"})\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  def authentication_log_out_success_handler(\n        _,\n        _measurements,\n        %{sub: sub, backend: backend, client_id: client_id},\n        _\n      ) do\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"authentication\",\n          ?\\s,\n          \"log_out\",\n          \" - \",\n          \"success\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"sub\", sub),\n          log_attribute(\"backend_id\", backend.id),\n          log_attribute(\"provider\", backend.type)\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  def registration_create_success_handler(\n        _,\n        _measurements,\n        %{sub: sub, backend: backend, client_id: client_id},\n        _\n      ) do\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"registration\",\n          ?\\s,\n          \"create\",\n          \" - \",\n          \"success\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"sub\", sub),\n          log_attribute(\"backend_id\", backend.id),\n          log_attribute(\"provider\", backend.type)\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  def registration_create_failure_handler(\n        _,\n        _measurements,\n        %{client_id: client_id, error: %Ecto.Changeset{} = changeset},\n        _\n      ) do\n    message = ErrorHelpers.error_messages(changeset) |> Enum.join(\", \")\n\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"registration\",\n          ?\\s,\n          \"create\",\n          \" - \",\n          \"failure\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"message\", ~s{\"#{message}\"})\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  def registration_confirm_success_handler(\n        _,\n        _measurements,\n        %{sub: sub, backend: backend, client_id: client_id, token: token},\n        _\n      ) do\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"registration\",\n          ?\\s,\n          \"confirm\",\n          \" - \",\n          \"success\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"sub\", sub),\n          log_attribute(\"backend\", backend),\n          log_attribute(\"token\", token)\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  def registration_confirm_failure_handler(\n        _,\n        _measurements,\n        %{client_id: client_id, message: message, token: token},\n        _\n      ) do\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"registration\",\n          ?\\s,\n          \"confirm\",\n          \" - \",\n          \"failure\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"message\", ~s{\"#{message}\"}),\n          log_attribute(\"token\", token)\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  def registration_update_success_handler(\n        _,\n        _measurements,\n        %{sub: sub, backend: backend, client_id: client_id},\n        _\n      ) do\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"registration\",\n          ?\\s,\n          \"update\",\n          \" - \",\n          \"success\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"sub\", sub),\n          log_attribute(\"provider\", backend.type),\n          log_attribute(\"backend_id\", backend.id),\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  def registration_update_failure_handler(\n        _,\n        _measurements,\n        %{sub: sub, backend: backend, client_id: client_id, error: message},\n        _\n      )\n      when is_binary(message) do\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"registration\",\n          ?\\s,\n          \"update\",\n          \" - \",\n          \"failure\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"sub\", sub),\n          log_attribute(\"provider\", backend.type),\n          log_attribute(\"backend_id\", backend.id),\n          log_attribute(\"message\", ~s{\"#{message}\"})\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  def registration_update_failure_handler(\n        _,\n        _measurements,\n        %{\n          sub: sub,\n          backend: backend,\n          client_id: client_id,\n          error: %Ecto.Changeset{} = changeset\n        },\n        _\n      ) do\n    message = ErrorHelpers.error_messages(changeset) |> Enum.join(\", \")\n\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"registration\",\n          ?\\s,\n          \"update\",\n          \" - \",\n          \"failure\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"sub\", sub),\n          log_attribute(\"provider\", backend.type),\n          log_attribute(\"backend_id\", backend.id),\n          log_attribute(\"message\", ~s{\"#{message}\"})\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  def authorization_consent_success_handler(\n        _,\n        _measurements,\n        %{sub: sub, backend: backend, client_id: client_id, scopes: scopes},\n        _\n      ) do\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"authorization\",\n          ?\\s,\n          \"consent\",\n          \" - \",\n          \"success\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"sub\", sub),\n          log_attribute(\"provider\", backend.type),\n          log_attribute(\"backend_id\", backend.id),\n          log_attribute(\"scope\", ~s{\"#{Enum.join(scopes, \" \")}\"})\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  def authorization_consent_failure_handler(\n        _,\n        _measurements,\n        %{sub: sub, backend: backend, client_id: client_id, scopes: scopes, message: message},\n        _\n      ) do\n    Logger.log(\n      :info,\n      fn ->\n        [\n          \"boruta_identity\",\n          ?\\s,\n          \"authorization\",\n          ?\\s,\n          \"consent\",\n          \" - \",\n          \"success\",\n          log_attribute(\"client_id\", client_id),\n          log_attribute(\"sub\", sub),\n          log_attribute(\"provider\", backend.type),\n          log_attribute(\"backend_id\", backend.id),\n          log_attribute(\"scope\", ~s{\"#{Enum.join(scopes, \" \")}\"}),\n          log_attribute(\"message\", ~s{\"#{message}\"})\n        ]\n      end,\n      type: :business\n    )\n  end\n\n  defp log_attribute(_key, nil), do: \"\"\n  defp log_attribute(key, attribute), do: \" #{key}=#{attribute}\"\n\n  # From Phoenix.Logger\n  defp log_level(nil, _conn), do: :info\n  defp log_level(level, _conn) when is_atom(level), do: level\n\n  defp log_level({mod, fun, args}, conn) when is_atom(mod) and is_atom(fun) and is_list(args) do\n    apply(mod, fun, [conn | args])\n  end\n\n  defp connection_type(:set_chunked), do: \"chunked\"\n  defp connection_type(_), do: \"sent\"\n\n  defp duration(duration) do\n    duration = System.convert_time_unit(duration, :native, :microsecond)\n\n    if duration > 1000 do\n      [duration |> div(1000) |> Integer.to_string(), \"ms\"]\n    else\n      [Integer.to_string(duration), \"µs\"]\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/organizations/organization.ex",
    "content": "defmodule BorutaIdentity.Organizations.Organization do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  @type t :: %__MODULE__{\n    id: String.t(),\n    name: String.t(),\n    label: String.t() | nil,\n    inserted_at: DateTime.t(),\n    updated_at: DateTime.t()\n  }\n\n  @primary_key {:id, Ecto.UUID, autogenerate: true}\n  @foreign_key_type Ecto.UUID\n  schema \"organizations\" do\n    field(:name, :string)\n    field(:label, :string)\n\n    timestamps()\n  end\n\n  @doc false\n  def changeset(organization, attrs) do\n    organization\n    |> cast(attrs, [:name, :label])\n    |> validate_required([:name])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/organizations/organization_user.ex",
    "content": "defmodule BorutaIdentity.Organizations.OrganizationUser do\n  @moduledoc false\n\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Organizations.Organization\n\n  @type t :: %__MODULE__{\n    user_id: String.t(),\n    organization_id: String.t(),\n    inserted_at: DateTime.t(),\n    updated_at: DateTime.t()\n  }\n\n  @foreign_key_type Ecto.UUID\n  @primary_key {:id, Ecto.UUID, autogenerate: true}\n  schema \"organizations_users\" do\n    belongs_to :user, User\n    belongs_to :organization, Organization\n\n    timestamps()\n  end\n\n  @doc false\n  def changeset(organization_user, attrs) do\n    organization_user\n    |> cast(attrs, [:organization_id, :user_id])\n    |> validate_required([:organization_id, :user_id])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/organizations.ex",
    "content": "defmodule BorutaIdentity.Organizations do\n  @moduledoc false\n\n  import Ecto.Query\n\n  alias BorutaIdentity.Organizations.Organization\n  alias BorutaIdentity.Repo\n\n  @type organization_params :: %{\n          name: String.t(),\n          label: String.t() | nil\n        }\n\n  @spec list_organizations() :: Scrivener.Page.t()\n  @spec list_organizations(params :: map()) :: Scrivener.Page.t()\n  def list_organizations(params \\\\ %{}) do\n    from(o in Organization)\n    |> Repo.paginate(Map.merge(params, %{\"page_size\" => 500}))\n  end\n\n  # @spec search_organizations(query :: String.t(), params :: map()) :: Scrivener.Page.t()\n  # @spec search_organizations(query :: String.t()) :: Scrivener.Page.t()\n  # def search_organizations(query, params \\\\ %{}) do\n  #   from(o in Organization,\n  #     where: fragment(\"name % ?\", ^query),\n  #     order_by: fragment(\"word_similarity(name, ?) DESC\", ^query)\n  #   )\n  #   |> Repo.paginate(params)\n  # end\n\n  @spec create_organization(organization_params :: organization_params()) ::\n          {:ok, organization :: Organization.t()} | {:error, changeset :: Ecto.Changeset.t()}\n  def create_organization(organization_params) do\n    Organization.changeset(%Organization{}, organization_params)\n    |> Repo.insert()\n  end\n\n  @spec delete_organization(organization_id :: String.t()) ::\n          {:ok, organization :: Organization.t()} | {:error, changeset :: Ecto.Changeset.t()}\n  def delete_organization(organization_id) do\n    case get_organization(organization_id) do\n      nil ->\n        {:error, :not_found}\n\n      organization ->\n        Repo.delete(organization)\n    end\n  end\n\n  @spec get_organization(organization_id :: String.t()) :: organization :: Organization.t() | nil\n  def get_organization(organization_id) do\n    case Ecto.UUID.cast(organization_id) do\n      {:ok, _} ->\n        Repo.get(Organization, organization_id)\n\n      _ ->\n        nil\n    end\n  end\n\n  @spec update_organization(\n          organization :: Organization.t(),\n          organization_params :: organization_params()\n        ) :: {:ok, organization :: Organization.t()} | {:error, changeset :: Ecto.Changeset.t()}\n  def update_organization(organization, organization_params) do\n    Organization.changeset(organization, organization_params)\n    |> Repo.update()\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/repo.ex",
    "content": "defmodule BorutaIdentity.Repo do\n  use Ecto.Repo,\n    otp_app: :boruta_identity,\n    adapter: Ecto.Adapters.Postgres\n\n  use Scrivener, page_size: 12\n\n  def set_limit(conn) do\n    Postgrex.query(conn, \"SELECT set_limit($1)\", [0.15])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/resource_owners.ex",
    "content": "defmodule BorutaIdentity.ResourceOwners do\n  @moduledoc false\n\n  @behaviour Boruta.Oauth.ResourceOwners\n\n  use BorutaIdentityWeb, :controller\n\n  alias Boruta.Oauth.ResourceOwner\n  alias Boruta.Oauth.Scope\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.Role\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.VerifiableCredentials\n  alias BorutaIdentity.Accounts.VerifiablePresentations\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.Organizations.Organization\n\n  @impl Boruta.Oauth.ResourceOwners\n  def get_by(username: username) do\n    backend = Backend.default!()\n\n    with {:ok, impl_user} <-\n           apply(Backend.implementation(backend), :get_user, [backend, %{email: username}]),\n         %User{id: id, username: email, last_login_at: last_login_at} <-\n           apply(Backend.implementation(backend), :domain_user!, [impl_user, backend]) do\n      {:ok,\n       %ResourceOwner{\n         sub: id,\n         username: email,\n         last_login_at: last_login_at,\n         # TODO find out why the impl user in extra_claims\n         extra_claims: %{user: impl_user}\n       }}\n    else\n      _ ->\n        {:error, \"Invalid username or password.\"}\n    end\n  end\n\n  def get_by(sub: \"unknown\", scope: _scope), do: %User{}\n  def get_by(sub: \"did:\" <> _key, scope: _scope), do: %User{}\n\n  def get_by(sub: sub, scope: scope) when not is_nil(sub) do\n    case Accounts.get_user(sub) do\n      %User{\n        id: id,\n        username: email,\n        last_login_at: last_login_at,\n        federated_metadata: federated_metadata\n      } = user ->\n        {:ok,\n         %ResourceOwner{\n           sub: id,\n           username: email,\n           last_login_at: last_login_at,\n           extra_claims: Map.merge(metadata(user, scope), federated_metadata),\n           authorization_details: VerifiableCredentials.authorization_details(user, scope),\n           credential_configuration: VerifiableCredentials.credential_configuration(user),\n           presentation_configuration: VerifiablePresentations.presentation_configuration(user)\n         }}\n\n      _ ->\n        {:error, \"Invalid username or password.\"}\n    end\n  end\n\n  def get_by(_), do: {:error, \"Invalid username or password.\"}\n\n  @impl Boruta.Oauth.ResourceOwners\n  def check_password(%ResourceOwner{extra_claims: extra_claims}, password) do\n    backend = Backend.default!()\n\n    case apply(\n           Backend.implementation(backend),\n           :check_user_against,\n           [backend, extra_claims[:user], %{password: password}]\n         ) do\n      {:ok, _user} ->\n        :ok\n\n      _ ->\n        {:error, \"Invalid username or password.\"}\n    end\n  end\n\n  @impl Boruta.Oauth.ResourceOwners\n  def authorized_scopes(%ResourceOwner{sub: \"unknown\"}), do: []\n  def authorized_scopes(%ResourceOwner{sub: \"did:\" <> _key}), do: []\n  def authorized_scopes(%ResourceOwner{sub: sub}) when not is_nil(sub) do\n    Accounts.get_user_scopes(sub) ++\n      Enum.flat_map(Accounts.get_user_roles(sub), fn %{scopes: scopes} -> scopes end)\n  end\n\n  def authorized_scopes(_), do: []\n\n  @impl Boruta.Oauth.ResourceOwners\n  def claims(%ResourceOwner{sub: sub}, scope) do\n    case Accounts.get_user(sub) do\n      %User{} = user ->\n        scope\n        |> Scope.split()\n        |> Enum.reduce(%{}, fn scope, acc -> merge_claims(scope, acc, user, sub) end)\n        |> Map.put(\"scope\", scope)\n\n      _ ->\n        %{}\n    end\n  end\n\n  @spec metadata(user :: User.t(), scope :: String.t()) :: metadata :: map()\n  def metadata(%User{username: username, metadata: %{} = metadata}, _scope) when metadata == %{}, do: %{\n    \"email\" => username\n  }\n\n  def metadata(user, scope) do\n    user.metadata\n    |> User.metadata_filter(user.backend)\n    |> metadata_scope_filter(scope, user.backend)\n    |> Enum.into(%{})\n    |> Map.put(\"email\", user.username)\n  end\n\n  defp merge_claims(\n    \"email\",\n    acc,\n    %User{\n      username: username,\n      confirmed_at: confirmed_at\n    },\n    _sub\n  ) do\n    Map.merge(acc, %{\n      \"email\" => username,\n      \"email_verified\" => !!confirmed_at\n    })\n  end\n\n  defp merge_claims(\"profile\", acc, _user, sub) do\n    roles = Accounts.get_user_roles(sub)\n    organizations = Accounts.get_user_organizations(sub)\n\n    acc\n    |> Map.put(\n      \"organizations\",\n      Enum.map(organizations, fn %Organization{} = organization ->\n        Map.from_struct(organization)\n        |> Map.take([:id, :name, :label])\n        |> Enum.map(fn {key, value} -> {Atom.to_string(key), value} end)\n        |> Enum.into(%{})\n      end)\n    )\n    |> Map.put(\"roles\", Enum.map(roles, fn %Role{name: name} -> name end))\n  end\n\n  defp merge_claims(_, acc, _user, _sub), do: acc\n\n  defp metadata_scope_filter(metadata, request_scope, %Backend{metadata_fields: metadata_fields}) do\n    Enum.filter(metadata, fn {key, _value} ->\n      # does backend metadata fields configuration allows current field according to scope ?\n      Enum.reduce(metadata_fields, true, fn\n        %{\"attribute_name\" => ^key, \"scopes\" => scopes}, acc ->\n          case scopes do\n            nil ->\n              acc && true\n\n            scopes ->\n              request_scopes = Scope.split(request_scope)\n              Enum.empty?(scopes -- request_scopes)\n          end\n\n        _, acc ->\n          acc && true\n      end)\n    end)\n    |> Enum.into(%{})\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/totp.ex",
    "content": "defmodule BorutaIdentity.TotpError do\n  @moduledoc false\n\n  @enforce_keys [:message, :totp_secret]\n  defexception [:message, :totp_secret, :changeset, :template, plug_status: 400]\n\n  @type t :: %__MODULE__{\n          message: String.t(),\n          totp_secret: String.t(),\n          changeset: Ecto.Changeset.t() | nil,\n          template: BorutaIdentity.IdentityProviders.Template.t()\n        }\n\n  def exception(message) when is_binary(message) do\n    %__MODULE__{message: message, totp_secret: \"\"}\n  end\n\n  def message(exception) do\n    exception.message\n  end\nend\n\ndefmodule BorutaIdentity.TotpRegistrationApplication do\n  @moduledoc false\n\n  @callback totp_registration_initialized(\n              context :: any(),\n              totp_secret :: String.t(),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) :: any()\n\n  @callback totp_registration_error(\n              context :: any(),\n              error :: BorutaIdentity.TotpError.t()\n            ) :: any()\n\n  @callback totp_registration_success(\n              context :: any(),\n              user :: BorutaIdentity.Accounts.User.t()\n            ) :: any()\nend\n\ndefmodule BorutaIdentity.TotpAuthenticationApplication do\n  @moduledoc false\n\n  @callback totp_initialized(\n              context :: any(),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) :: any()\n\n  @callback totp_not_required(context :: any()) :: any()\n\n  @callback totp_registration_missing(context :: any()) :: any()\n\n  @callback totp_authenticated(\n              context :: any(),\n              current_user :: BorutaIdentity.Accounts.User.t()\n            ) ::\n              any()\n\n  @callback totp_authentication_failure(\n              context :: any(),\n              error :: BorutaIdentity.TotpError.t()\n            ) :: any()\nend\n\ndefmodule BorutaIdentity.Totp do\n  @moduledoc false\n\n  defmodule Hotp do\n    @moduledoc \"\"\"\n    Implements HOTP generation as described in the IETF RFC\n    [HOTP: An HMAC-Based One-Time Password Algorithm](https://www.ietf.org/rfc/rfc4226.txt)\n    > This implementation defaults to 6 digits using the sha1 algorithm as hashing function\n    \"\"\"\n\n    import Bitwise\n\n    @hmac_algorithm :sha\n    @digits 6\n\n    @spec generate_hotp(secret :: String.t(), counter :: integer()) :: hotp :: String.t()\n    def generate_hotp(secret, counter) do\n      # Step 1: Generate an HMAC-SHA-1 value\n      hmac_result = :crypto.mac(:hmac, @hmac_algorithm, secret, <<counter::size(64)>>)\n\n      # Step 2: Dynamic truncation\n      truncated_hash = truncate_hash(hmac_result)\n\n      # Step 3: Compute HOTP value (6-digit OTP)\n      hotp = truncated_hash |> rem(10 ** @digits)\n\n      format_hotp(hotp)\n    end\n\n    defp truncate_hash(hmac_value) do\n      # NOTE the folowing hard coded values are part of the specification\n      offset = :binary.at(hmac_value, 19) &&& 0xF\n\n      with <<_::size(1), result::size(31)>> <- :binary.part(hmac_value, offset, 4) do\n        result\n      end\n    end\n\n    defp format_hotp(hotp) do\n      String.pad_leading(Integer.to_string(hotp), @digits, \"0\")\n    end\n  end\n\n  defmodule Admin do\n    @moduledoc false\n\n    import Boruta.Config,\n      only: [\n        issuer: 0\n      ]\n\n    @interval 30\n\n    @spec generate_totp(secret :: String.t()) :: totp :: String.t() | :error\n    def generate_totp(secret) do\n      with {:ok, secret} <- Base.decode32(secret, padding: false) do\n        Hotp.generate_hotp(secret, number_of_time_steps())\n      end\n    end\n\n    @spec check_totp(totp :: String.t(), secret :: String.t()) :: totp :: :ok | {:error, reason :: String.t()}\n    def check_totp(totp, secret) when is_binary(secret) do\n      with {:ok, secret} <- Base.decode32(secret, padding: false),\n           true <- Hotp.generate_hotp(secret, number_of_time_steps()) == totp do\n        :ok\n      else\n        _ -> {:error, \"Given TOTP is invalid.\"}\n      end\n    end\n\n    def check_totp(_totp, _secret), do: {:error, \"Given TOTP is invalid.\"}\n\n    def generate_secret do\n      SecureRandom.uuid() |> Base.encode32(padding: false)\n    end\n\n    def url(username, secret) do\n      \"otpauth://totp/#{username}?secret=#{secret}&issuer=#{issuer()}\"\n    end\n\n    defp number_of_time_steps do\n      floor(:os.system_time(:seconds) / @interval)\n    end\n  end\n\n  import BorutaIdentity.Accounts.Utils, only: [defwithclientidp: 2]\n\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.Repo\n  alias BorutaIdentity.TotpError\n\n  defwithclientidp initialize_totp_registration(context, client_id, totp_authenticated, current_user, module) do\n    totp_secret = Admin.generate_secret()\n\n    case {totp_authenticated, current_user.totp_registered_at} do\n      {true, _} ->\n        module.totp_registration_initialized(\n          context,\n          totp_secret,\n          new_totp_registration_template(client_idp)\n        )\n      {false, nil} ->\n        module.totp_registration_initialized(\n          context,\n          totp_secret,\n          new_totp_registration_template(client_idp)\n        )\n      _ ->\n        raise TotpError, \"Authenticator registration could not be initialized.\"\n    end\n  end\n\n  defwithclientidp register_totp(context, client_id, current_user, totp_params, module) do\n    with :ok <- Admin.check_totp(totp_params[:totp_code], totp_params[:totp_secret]),\n         {:ok, user} <-\n           current_user\n           |> User.totp_changeset(totp_params[:totp_secret])\n           |> Repo.update() do\n      module.totp_registration_success(context, user)\n    else\n      {:error, %Ecto.Changeset{} = changeset} ->\n        error = %TotpError{\n          message: \"Current user could not be updated.\",\n          changeset: changeset,\n          totp_secret: totp_params[:totp_secret],\n          template: new_totp_registration_template(client_idp)\n        }\n\n        module.totp_registration_error(context, error)\n\n      {:error, error} ->\n        error = %TotpError{\n          message: error,\n          totp_secret: totp_params[:totp_secret],\n          template: new_totp_registration_template(client_idp)\n        }\n\n        module.totp_registration_error(context, error)\n    end\n  end\n\n  defwithclientidp initialize_totp(context, client_id, current_user, module) do\n    case {client_idp, current_user} do\n      {%IdentityProvider{totpable: true}, %User{totp_registered_at: %DateTime{}}} ->\n        module.totp_initialized(context, new_totp_authentication_template(client_idp))\n\n      {%IdentityProvider{enforce_totp: true}, %User{totp_registered_at: nil}} ->\n        module.totp_registration_missing(context)\n\n      {%IdentityProvider{enforce_totp: true}, _} ->\n        module.totp_initialized(context, new_totp_authentication_template(client_idp))\n\n      {%IdentityProvider{enforce_totp: false}, _} ->\n        module.totp_not_required(context)\n    end\n  end\n\n  defwithclientidp authenticate_totp(context, client_id, %User{totp_registered_at: nil}, _totp_params, module) do\n    case client_idp.enforce_totp do\n      true ->\n        module.totp_registration_missing(context)\n      false ->\n        module.totp_not_required(context)\n    end\n  end\n\n  defwithclientidp authenticate_totp(context, client_id, user, totp_params, module) do\n    case Admin.check_totp(totp_params[:totp_code], user.totp_secret) do\n      :ok ->\n        module.totp_authenticated(context, user)\n\n      {:error, error} ->\n        error = %TotpError{\n          message: error,\n          totp_secret: totp_params[:totp_secret],\n          template: new_totp_authentication_template(client_idp)\n        }\n\n        module.totp_authentication_failure(context, error)\n    end\n  end\n\n  defp new_totp_registration_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(\n      identity_provider.id,\n      :new_totp_registration\n    )\n  end\n\n  defp new_totp_authentication_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(\n      identity_provider.id,\n      :new_totp_authentication\n    )\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity/webauthn.ex",
    "content": "defmodule BorutaIdentity.WebauthnError do\n  @moduledoc false\n\n  @enforce_keys [:message]\n  defexception [:message, :webauthn_options, :template, plug_status: 400]\n\n  @type t :: %__MODULE__{\n          message: String.t(),\n          webauthn_options: BorutaIdentity.Webauthn.Options.t() | nil,\n          template: BorutaIdentity.IdentityProviders.Template.t()\n        }\n\n  def exception(message) when is_binary(message) do\n    %__MODULE__{message: message}\n  end\n\n  def message(exception) do\n    exception.message\n  end\nend\n\ndefmodule BorutaIdentity.WebauthnRegistrationApplication do\n  @moduledoc false\n\n  @callback webauthn_registration_initialized(\n              context :: any(),\n              webauthn_options :: BorutaIdentity.Webauthn.Options.t(),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) :: any()\n\n  @callback webauthn_registration_error(\n              context :: any(),\n              error :: BorutaIdentity.WebauthnError.t()\n            ) :: any()\n\n  @callback webauthn_registration_success(\n              context :: any(),\n              user :: BorutaIdentity.Accounts.User.t()\n            ) :: any()\nend\n\ndefmodule BorutaIdentity.WebauthnAuthenticationApplication do\n  @moduledoc false\n\n  @callback webauthn_initialized(\n              context :: any(),\n              webauthn_options :: BorutaIdentity.Webauthn.Options.t(),\n              template :: BorutaIdentity.IdentityProviders.Template.t()\n            ) :: any()\n\n  @callback webauthn_not_required(context :: any()) :: any()\n\n  @callback webauthn_registration_missing(context :: any()) :: any()\n\n  @callback webauthn_authenticated(\n              context :: any(),\n              current_user :: BorutaIdentity.Accounts.User.t()\n            ) ::\n              any()\n\n  @callback webauthn_authentication_failure(\n              context :: any(),\n              error :: BorutaIdentity.WebauthnError.t()\n            ) :: any()\nend\n\ndefmodule BorutaIdentity.Webauthn do\n  @moduledoc false\n\n  defmodule Options do\n    @moduledoc false\n\n    alias Boruta.Config\n\n    @type t :: %__MODULE__{\n            rp: %{\n              id: String.t()\n            },\n            user: %{\n              id: String.t(),\n              displayName: String.t()\n            },\n            challenge: String.t(),\n            credential_id: String.t(),\n            publicKeyCredParams: %{\n              alg: integer(),\n              type: String.t()\n            }\n          }\n\n    @cose_alg_identifier %{\n      \"ES256\" => -7,\n      \"ES384\" => -35,\n      \"ES512\" => -36,\n      \"EdDSA\" => -8\n    }\n\n    @enforce_keys [:rp, :user, :challenge]\n    defstruct rp: nil,\n              user: nil,\n              challenge: nil,\n              credential_id: nil,\n              publicKeyCredParams: %{\n                alg: @cose_alg_identifier[\"ES256\"],\n                type: \"public-key\"\n              }\n  end\n\n  import BorutaIdentity.Accounts.Utils, only: [defwithclientidp: 2]\n\n  alias Boruta.Config\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.Repo\n  alias BorutaIdentity.WebauthnError\n\n  def options(user, true) do\n    with {:ok, user} <- Accounts.put_user_webauthn_challenge(user) do\n      options = %Options{\n        rp: %{\n          id: Config.issuer() |> URI.parse() |> Map.get(:host),\n          name: \"boruta\"\n        },\n        user: %{\n          id: user.id,\n          name: user.username,\n          displayName: user.username\n        },\n        challenge: user.webauthn_challenge,\n        credential_id: user.webauthn_identifier\n      }\n\n      {:ok, options}\n    end\n  end\n\n  def options(user, false) do\n    options = %Options{\n      rp: %{\n        id: Config.issuer() |> URI.parse() |> Map.get(:host),\n        name: \"boruta\"\n      },\n      user: %{\n        id: user.id,\n        name: user.username,\n        displayName: user.username\n      },\n      challenge: user.webauthn_challenge\n    }\n\n    {:ok, options}\n  end\n\n  defwithclientidp initialize_webauthn_registration(\n                     context,\n                     client_id,\n                     webauthn_authenticated,\n                     current_user,\n                     module\n                   ) do\n    case options(current_user, true) do\n      {:ok, webauthn_options} ->\n        case {webauthn_authenticated, current_user.webauthn_registered_at} do\n          {true, _} ->\n            module.webauthn_registration_initialized(\n              context,\n              webauthn_options,\n              new_webauthn_registration_template(client_idp)\n            )\n\n          {false, nil} ->\n            module.webauthn_registration_initialized(\n              context,\n              webauthn_options,\n              new_webauthn_registration_template(client_idp)\n            )\n\n          _error ->\n            raise WebauthnError, \"Authenticator registration could not be initialized.\"\n        end\n\n      _error ->\n        raise WebauthnError, \"Authenticator registration could not be initialized.\"\n    end\n  end\n\n  defwithclientidp initialize_webauthn(context, client_id, current_user, module) do\n    {:ok, webauthn_options} = options(current_user, true)\n\n    case {client_idp, current_user} do\n      {%IdentityProvider{webauthnable: true}, %User{webauthn_registered_at: %DateTime{}}} ->\n        module.webauthn_initialized(\n          context,\n          webauthn_options,\n          new_webauthn_authentication_template(client_idp)\n        )\n\n      {%IdentityProvider{enforce_webauthn: true}, %User{webauthn_registered_at: nil}} ->\n        module.webauthn_registration_missing(context)\n\n      {%IdentityProvider{enforce_webauthn: true}, _} ->\n        module.webauthn_initialized(\n          context,\n          webauthn_options,\n          new_webauthn_authentication_template(client_idp)\n        )\n\n      {%IdentityProvider{enforce_webauthn: false}, _} ->\n        module.webauthn_not_required(context)\n    end\n  end\n\n  defwithclientidp register_webauthn(context, client_id, current_user, webauthn_params, module) do\n    %{\n      attestation: attestation,\n      client_data: client_data,\n      identifier: identifier,\n      type: \"public-key\"\n    } = webauthn_params\n\n    wax_challenge =\n      Wax.new_registration_challenge(\n        origin: Config.issuer(),\n        attestation: \"direct\",\n        rp_id: Config.issuer() |> URI.parse() |> Map.get(:host),\n        trusted_attestation_types: [:basic, :uncertain, :attca, :anonca],\n        verify_trust_root: false\n      )\n\n    wax_challenge = %{wax_challenge | bytes: current_user.webauthn_challenge}\n\n    with {:ok, attestation} <- Base.decode64(attestation),\n         {:ok, {authenticator_data, _result}} <-\n           Wax.register(attestation, client_data, wax_challenge),\n         {:ok, user} <-\n           current_user\n           |> User.webauthn_public_key_changeset(\n             authenticator_data.attested_credential_data.credential_public_key,\n             identifier\n           )\n           |> Repo.update() do\n      module.webauthn_registration_success(context, user)\n    else\n      _ ->\n        # TODO provide more meaningful errors\n        case options(current_user, true) do\n          {:ok, webauthn_options} ->\n            error = %WebauthnError{\n              message: \"Authenticator could not be registered.\",\n              webauthn_options: webauthn_options,\n              template: new_webauthn_registration_template(client_idp)\n            }\n\n            module.webauthn_registration_error(context, error)\n\n          {:error, %Ecto.Changeset{}} ->\n            raise WebauthnError, \"Authenticator registration could not be initialized.\"\n        end\n    end\n  end\n\n  @dialyzer {:no_return, {:authenticate_webauthn, 5}}\n  defwithclientidp authenticate_webauthn(\n                     context,\n                     client_id,\n                     current_user,\n                     webauthn_params,\n                     module\n                   ) do\n    %{\n      signature: signature,\n      authenticator_data: authenticator_data,\n      client_data: client_data,\n      identifier: identifier\n    } = webauthn_params\n\n    wax_challenge =\n      Wax.new_registration_challenge(\n        origin: Config.issuer(),\n        attestation: \"direct\",\n        rp_id: Config.issuer() |> URI.parse() |> Map.get(:host),\n        trusted_attestation_types: [:basic, :uncertain, :attca, :anonca],\n        verify_trust_root: false,\n        allow_credentials: [{current_user.webauthn_identifier, current_user.webauthn_public_key}]\n      )\n\n    wax_challenge = %{wax_challenge | bytes: current_user.webauthn_challenge}\n\n    case Wax.authenticate(\n           identifier,\n           Base.decode64!(authenticator_data),\n           Base.decode64!(signature),\n           client_data,\n           wax_challenge,\n           []\n         ) do\n      {:ok, _auth_data} ->\n        module.webauthn_authenticated(context, current_user)\n\n      {:error, _error} ->\n        case options(current_user, true) do\n          {:ok, webauthn_options} ->\n            error = %WebauthnError{\n              message: \"Passkey could not be verified.\",\n              webauthn_options: webauthn_options,\n              template: new_webauthn_authentication_template(client_idp)\n            }\n\n            module.webauthn_authentication_failure(context, error)\n\n          {:error, %Ecto.Changeset{}} ->\n            raise WebauthnError, \"Authenticator registration could not be initialized.\"\n        end\n    end\n  end\n\n  defp new_webauthn_registration_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(\n      identity_provider.id,\n      :new_webauthn_registration\n    )\n  end\n\n  defp new_webauthn_authentication_template(identity_provider) do\n    IdentityProviders.get_identity_provider_template!(\n      identity_provider.id,\n      :new_webauthn_authentication\n    )\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity.ex",
    "content": "defmodule BorutaIdentity do\n  @moduledoc \"\"\"\n  BorutaIdentity keeps the contexts that define your domain\n  and business logic.\n\n  Contexts are also responsible for managing your data, regardless\n  if it comes from the database, an external API or others.\n  \"\"\"\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/concerns/authenticable.ex",
    "content": "defmodule BorutaIdentityWeb.Authenticable do\n  @moduledoc false\n\n  use BorutaIdentityWeb, :controller\n\n  alias Boruta.ClientsAdapter\n  alias Boruta.Oauth\n  alias BorutaIdentity.Accounts\n\n  # Make the remember me cookie valid for 60 days.\n  # If you want bump or reduce this value, also change\n  # the token expiry itself in UserToken.\n  @session_key :user_token\n  @max_age 60 * 60 * 24 * 60\n  @remember_me_cookie \"_boruta_identity_web_user_remember_me\"\n  @remember_me_options [sign: true, max_age: @max_age, same_site: \"Lax\"]\n\n  @spec remember_me_cookie() :: String.t()\n  def remember_me_cookie, do: @remember_me_cookie\n\n  @spec store_user_session(conn :: Plug.Conn.t(), session_token :: String.t()) ::\n          conn :: Plug.Conn.t()\n  def store_user_session(%Plug.Conn{body_params: params} = conn, session_token) do\n    user = session_token && Accounts.get_user_by_session_token(session_token)\n\n    conn\n    |> assign(:current_user, user)\n    |> put_session(@session_key, session_token)\n    |> put_session(:live_socket_id, \"users_sessions:#{Base.url_encode64(session_token)}\")\n    |> maybe_write_remember_me_cookie(session_token, params[\"user\"])\n  end\n\n  @spec get_user_session(conn :: Plug.Conn.t()) :: session_token :: String.t()\n  def get_user_session(conn) do\n    get_session(conn, @session_key)\n  end\n\n  @spec remove_user_session(conn :: Plug.Conn.t()) :: conn :: Plug.Conn.t()\n  def remove_user_session(conn) do\n    conn\n    |> delete_resp_cookie(@remember_me_cookie)\n    |> delete_session(@session_key)\n  end\n\n  defp maybe_write_remember_me_cookie(conn, token, %{\"remember_me\" => remember_me})\n       when remember_me in [\"true\", \"on\"] do\n    put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)\n  end\n\n  defp maybe_write_remember_me_cookie(conn, _token, _params) do\n    conn\n  end\n\n  @spec after_sign_in_path(conn :: Plug.Conn.t()) :: String.t()\n  def after_sign_in_path(conn), do: user_return_to_from_request(conn) || \"/\"\n\n  @spec after_registration_path(conn :: Plug.Conn.t()) :: String.t()\n  def after_registration_path(conn), do: user_return_to_from_request(conn) || \"/\"\n\n  @spec after_sign_out_path(conn :: Plug.Conn.t()) :: String.t()\n  def after_sign_out_path(%Plug.Conn{query_params: query_params} = conn) do\n    Routes.user_session_path(conn, :new, query_params)\n  end\n\n  @spec request_param(conn :: Plug.Conn.t()) :: request_param :: String.t()\n  def request_param(conn) do\n    case Oauth.Request.authorize_request(conn, %Oauth.ResourceOwner{sub: \"\"}) do\n      {:ok, %_{client_id: \"did:\" <> _key, scope: scope}} ->\n        user_return_to =\n          current_path(conn)\n          |> String.replace(~r/prompt=(login|none)/, \"\")\n          |> String.replace(~r/max_age=(\\d+)/, \"\")\n\n        {:ok, jwt, _payload} =\n          Joken.encode_and_sign(\n            %{\n              \"client_id\" => ClientsAdapter.public!().id,\n              \"scope\" => scope,\n              \"user_return_to\" => user_return_to\n            },\n            BorutaIdentityWeb.Token.application_signer()\n          )\n\n        jwt\n\n      {:ok, %_{client_id: client_id, scope: scope}} ->\n        # NOTE remove prompt and max_age params affecting redirections\n        user_return_to =\n          current_path(conn)\n          |> String.replace(~r/prompt=(login|none)/, \"\")\n          |> String.replace(~r/max_age=(\\d+)/, \"\")\n\n        {:ok, jwt, _payload} =\n          Joken.encode_and_sign(\n            %{\n              \"client_id\" => client_id,\n              \"scope\" => scope,\n              \"user_return_to\" => user_return_to\n            },\n            BorutaIdentityWeb.Token.application_signer()\n          )\n\n        jwt\n\n      _ ->\n        \"\"\n    end\n  end\n\n  @spec scope_from_request(conn :: Plug.Conn.t()) :: String.t() | nil\n  def scope_from_request(%Plug.Conn{query_params: query_params}) do\n    with {:ok, claims} <-\n           BorutaIdentityWeb.Token.verify(\n             query_params[\"request\"] || \"\",\n             BorutaIdentityWeb.Token.application_signer()\n           ),\n         {:ok, scope} <- Map.fetch(claims, \"scope\") do\n      scope\n    else\n      _ -> nil\n    end\n  end\n\n  @spec client_id_from_request(conn :: Plug.Conn.t()) :: String.t() | nil\n  def client_id_from_request(%Plug.Conn{query_params: query_params}) do\n    with {:ok, claims} <-\n           BorutaIdentityWeb.Token.verify(\n             query_params[\"request\"] || \"\",\n             BorutaIdentityWeb.Token.application_signer()\n           ),\n         {:ok, client_id} <- Map.fetch(claims, \"client_id\") do\n      client_id\n    else\n      _ -> nil\n    end\n  end\n\n  @spec user_return_to_from_request(conn :: Plug.Conn.t()) :: String.t() | nil\n  def user_return_to_from_request(%Plug.Conn{query_params: query_params}) do\n    with {:ok, claims} <-\n           BorutaIdentityWeb.Token.verify(\n             query_params[\"request\"] || \"\",\n             BorutaIdentityWeb.Token.application_signer()\n           ),\n         {:ok, user_return_to} <- Map.fetch(claims, \"user_return_to\") do\n      user_return_to\n    else\n      _ -> nil\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/backends_controller.ex",
    "content": "defmodule BorutaIdentityWeb.BackendsController do\n  # TODO test identity federation\n  @behaviour BorutaIdentity.Accounts.FederatedSessionApplication\n  use BorutaIdentityWeb, :controller\n\n  import BorutaIdentityWeb.Authenticable,\n    only: [\n      store_user_session: 2,\n      after_sign_in_path: 1,\n      client_id_from_request: 1\n    ]\n\n  alias BorutaIdentity.Accounts.Federated\n  alias BorutaIdentity.Accounts.IdentityProviderError\n  alias BorutaIdentity.Accounts.SessionError\n  alias BorutaIdentity.FederatedAccounts\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentityWeb.TemplateView\n\n  def authorize(\n        conn,\n        %{\n          \"id\" => backend_id,\n          \"federated_server_name\" => federated_server_name\n        } = params\n      ) do\n    backend = IdentityProviders.get_backend!(backend_id)\n\n    conn =\n      case client_id_from_request(conn) do\n        nil ->\n          raise IdentityProviderError, \"Client identifier not provided.\"\n\n        _client_id ->\n          put_session(conn, :request, params[\"request\"])\n      end\n\n    case Backend.federated_oauth_client(backend, federated_server_name) do\n      nil ->\n        raise IdentityProviderError, \"Could not fetch associated federated server\"\n\n      _oauth_client ->\n        conn\n        |> redirect(external: Backend.federated_login_url(backend, federated_server_name))\n    end\n  end\n\n  def callback(conn, %{\"federated_server_name\" => federated_server_name} = params) do\n    conn = request_from_session(conn)\n    client_id = client_id_from_request(conn)\n\n    FederatedAccounts.create_federated_session(\n      conn,\n      client_id,\n      federated_server_name,\n      params[\"code\"] || \"\",\n      __MODULE__\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.FederatedSessionApplication\n  def user_authenticated(conn, user, session_token) do\n    conn = request_from_session(conn)\n    client_id = client_id_from_request(conn)\n\n    :telemetry.execute(\n      [:authentication, :log_in, :success],\n      %{},\n      %{\n        sub: user.uid,\n        backend: %{user.backend | type: Federated},\n        client_id: client_id\n      }\n    )\n\n    conn\n    |> clear_session()\n    |> store_user_session(session_token)\n    |> put_session(:session_chosen, true)\n    |> redirect(to: after_sign_in_path(conn))\n  end\n\n  @impl BorutaIdentity.Accounts.FederatedSessionApplication\n  def authentication_failure(%Plug.Conn{} = conn, %SessionError{\n        message: message,\n        template: template\n      }) do\n    conn = request_from_session(conn)\n    client_id = client_id_from_request(conn)\n\n    :telemetry.execute(\n      [:authentication, :log_in, :failure],\n      %{},\n      %{\n        message: message,\n        client_id: client_id\n      }\n    )\n\n    conn\n    |> put_layout(false)\n    |> put_status(:unauthorized)\n    |> clear_session()\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        errors: [message]\n      }\n    )\n  end\n\n  defp request_from_session(conn) do\n    case get_session(conn, :request) do\n      nil ->\n        raise IdentityProviderError, \"Could not get request information.\"\n\n      request ->\n        %{\n          conn\n          | query_params: Map.put(conn.query_params, \"request\", request)\n        }\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/choose_session_controller.ex",
    "content": "defmodule BorutaIdentityWeb.ChooseSessionController do\n  @behaviour BorutaIdentity.Accounts.ChooseSessionApplication\n\n  use BorutaIdentityWeb, :controller\n\n  import BorutaIdentityWeb.Authenticable,\n    only: [client_id_from_request: 1]\n\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentityWeb.TemplateView\n\n  def index(conn, _params) do\n    client_id = client_id_from_request(conn)\n\n    Accounts.initialize_choose_session(conn, client_id, __MODULE__)\n  end\n\n  @impl BorutaIdentity.Accounts.ChooseSessionApplication\n  def choose_session_initialized(conn, template) do\n    current_user = conn.assigns[:current_user]\n\n    conn\n    |> put_session(:session_chosen, true)\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\", template: template, assigns: %{current_user: current_user})\n  end\n\n  @impl BorutaIdentity.Accounts.ChooseSessionApplication\n  def choose_session_not_required(conn) do\n    conn\n    |> put_session(:session_chosen, true)\n    |> redirect(to: Routes.user_session_path(conn, :new, conn.query_params))\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/fallback_controller.ex",
    "content": "defmodule BorutaIdentityWeb.FallbackController do\n  @moduledoc \"\"\"\n  Translates controller action results into valid `Plug.Conn` responses.\n\n  See `Phoenix.Controller.action_fallback/1` for more details.\n  \"\"\"\n  use BorutaIdentityWeb, :controller\n\n  alias BorutaIdentityWeb.ErrorView\n\n  def call(conn, {:error, :not_found}) do\n    conn\n    |> put_status(:not_found)\n    |> put_view(ErrorView)\n    |> render(:\"404\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/totp_controller.ex",
    "content": "defmodule BorutaIdentityWeb.TotpController do\n  @behaviour BorutaIdentity.TotpRegistrationApplication\n\n  use BorutaIdentityWeb, :controller\n\n  import BorutaIdentityWeb.Authenticable,\n    only: [\n      get_user_session: 1,\n      client_id_from_request: 1,\n      after_sign_in_path: 1\n    ]\n\n  alias BorutaIdentity.Totp\n  alias BorutaIdentity.TotpError\n  alias BorutaIdentityWeb.TemplateView\n\n  def new(conn, _params) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    totp_authenticated = Map.get(\n      get_session(conn, :totp_authenticated) || %{},\n      get_user_session(conn),\n      false\n    )\n    Totp.initialize_totp_registration(conn, client_id, totp_authenticated, current_user, __MODULE__)\n  end\n\n  def register(conn, %{\"totp\" => totp_params}) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns.current_user\n\n    totp_params = %{\n      totp_code: totp_params[\"totp_code\"],\n      totp_secret: totp_params[\"totp_secret\"]\n    }\n\n    Totp.register_totp(conn, client_id, current_user, totp_params, __MODULE__)\n  end\n\n  @impl BorutaIdentity.TotpRegistrationApplication\n  def totp_registration_initialized(conn, totp_secret, template) do\n    current_user = conn.assigns.current_user\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        current_user: current_user,\n        totp_secret: totp_secret\n      }\n    )\n  end\n\n  @impl BorutaIdentity.TotpRegistrationApplication\n  def totp_registration_error(conn, %TotpError{\n        changeset: %Ecto.Changeset{} = changeset,\n        totp_secret: totp_secret,\n        template: template\n      }) do\n    current_user = conn.assigns.current_user\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        changeset: changeset,\n        current_user: current_user,\n        totp_secret: totp_secret\n      }\n    )\n  end\n\n  def totp_registration_error(conn, %TotpError{\n        message: error,\n        totp_secret: totp_secret,\n        template: template\n      }) do\n    current_user = conn.assigns.current_user\n\n    conn\n    |> put_layout(false)\n    |> put_status(:unprocessable_entity)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        errors: [error],\n        current_user: current_user,\n        totp_secret: totp_secret\n      }\n    )\n  end\n\n  @impl BorutaIdentity.TotpRegistrationApplication\n  def totp_registration_success(%Plug.Conn{} = conn, _user) do\n    conn\n    |> put_flash(:info, \"TOTP authenticator registered successfully.\")\n    |> put_session(\n      :totp_authenticated,\n      (get_session(conn, :totp_authenticated) || %{})\n      |> Map.put(get_user_session(conn), true)\n    )\n    |> redirect(\n      to: after_sign_in_path(conn)\n    )\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/user_confirmation_controller.ex",
    "content": "defmodule BorutaIdentityWeb.UserConfirmationController do\n  @behaviour BorutaIdentity.Accounts.ConfirmationApplication\n\n  use BorutaIdentityWeb, :controller\n  import BorutaIdentityWeb.Authenticable, only: [client_id_from_request: 1]\n\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.ConfirmationError\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentityWeb.TemplateView\n\n  def new(conn, _params) do\n    client_id = client_id_from_request(conn)\n\n    Accounts.initialize_confirmation_instructions(conn, client_id, __MODULE__)\n  end\n\n  def create(%Plug.Conn{query_params: query_params} = conn, %{\"user\" => %{\"email\" => email}}) do\n    client_id = client_id_from_request(conn)\n    request = Map.get(query_params, \"request\")\n\n    confirmation_params = %{\n      email: email\n    }\n\n    Accounts.send_confirmation_instructions(\n      conn,\n      client_id,\n      confirmation_params,\n      &Routes.user_confirmation_url(conn, :confirm, &1, %{request: request}),\n      __MODULE__\n    )\n  end\n\n  # Do not log in the user after confirmation to avoid a\n  # leaked token giving the user access to the account.\n  def confirm(conn, %{\"token\" => token}) do\n    client_id = client_id_from_request(conn)\n\n    Accounts.confirm_user(conn, client_id, token, __MODULE__)\n  end\n\n  @impl BorutaIdentity.Accounts.ConfirmationApplication\n  def user_confirmed(%Plug.Conn{query_params: query_params} = conn, user) do\n    client_id = client_id_from_request(conn)\n\n    :telemetry.execute(\n      [:registration, :confirm, :success],\n      %{},\n      %{\n        client_id: client_id,\n        sub: user.uid,\n        backend: user.backend,\n        token: query_params[\"token\"]\n      }\n    )\n\n    conn\n    |> put_flash(:info, \"Account confirmed successfully.\")\n    |> redirect(to: Routes.user_session_path(conn, :new, %{request: query_params[\"request\"]}))\n  end\n\n  @impl BorutaIdentity.Accounts.ConfirmationApplication\n  def user_confirmation_failure(%Plug.Conn{query_params: query_params} = conn, %ConfirmationError{message: message}) do\n    client_id = client_id_from_request(conn)\n\n    :telemetry.execute(\n      [:registration, :confirm, :failure],\n      %{},\n      %{\n        client_id: client_id,\n        message: message,\n        token: query_params[\"token\"]\n      }\n    )\n\n    case conn.assigns[:current_user] do\n      %User{} ->\n        conn\n        |> put_flash(:error, message)\n        |> redirect(to: Routes.user_session_path(conn, :new, request: query_params[\"request\"]))\n\n      _ ->\n        conn\n        |> put_flash(:error, message)\n        |> redirect(to: Routes.user_session_path(conn, :new, request: query_params[\"request\"]))\n    end\n  end\n\n  @impl BorutaIdentity.Accounts.ConfirmationApplication\n  def confirmation_instructions_delivered(%Plug.Conn{query_params: query_params} = conn) do\n    conn\n    |> put_flash(\n      :info,\n      \"If your email is in our system and it has not been confirmed yet, \" <>\n        \"you will receive an email with instructions shortly.\"\n    )\n    |> redirect(to: Routes.user_session_path(conn, :new, request: query_params[\"request\"]))\n  end\n\n  @impl BorutaIdentity.Accounts.ConfirmationApplication\n  def confirmation_instructions_initialized(conn, template) do\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{}\n    )\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/user_consent_controller.ex",
    "content": "defmodule BorutaIdentityWeb.UserConsentController do\n  @behaviour BorutaIdentity.Accounts.ConsentApplication\n\n  use BorutaIdentityWeb, :controller\n\n  import BorutaIdentityWeb.Authenticable,\n    only: [client_id_from_request: 1, scope_from_request: 1, after_sign_in_path: 1]\n\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentityWeb.ErrorHelpers\n  alias BorutaIdentityWeb.TemplateView\n\n  action_fallback(BorutaIdentityWeb.FallbackController)\n\n  def index(conn, _params) do\n    current_user = conn.assigns[:current_user]\n    client_id = client_id_from_request(conn)\n    scope = scope_from_request(conn)\n\n    Accounts.initialize_consent(conn, client_id, current_user, scope, __MODULE__)\n  end\n\n  def consent(conn, params) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    consent_params = %{\n      client_id: client_id,\n      scopes: params[\"scopes\"] || []\n    }\n\n    Accounts.consent(conn, client_id, current_user, consent_params, __MODULE__)\n  end\n\n  @impl BorutaIdentity.Accounts.ConsentApplication\n  def consent_initialized(conn, client, scopes, template) do\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        scopes: scopes,\n        client: client\n      }\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.ConsentApplication\n  def consent_not_required(conn) do\n    redirect(conn, to: after_sign_in_path(conn))\n  end\n\n  @impl BorutaIdentity.Accounts.ConsentApplication\n  def consented(conn, scopes) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    :telemetry.execute(\n      [:authorization, :consent, :success],\n      %{},\n      %{\n        client_id: client_id,\n        sub: current_user.uid,\n        backend: current_user.backend,\n        scopes: scopes\n      }\n    )\n\n    redirect(conn, to: after_sign_in_path(conn))\n  end\n\n  @impl BorutaIdentity.Accounts.ConsentApplication\n  def consent_failed(%Plug.Conn{query_params: query_params} = conn, changeset) do\n    message = ErrorHelpers.error_messages(changeset) |> Enum.join(\", \")\n    request = query_params[\"request\"]\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    :telemetry.execute(\n      [:authorization, :consent, :failure],\n      %{},\n      %{\n        client_id: client_id,\n        sub: current_user.uid,\n        backend: current_user.backend,\n        scopes: Ecto.Changeset.get_field(changeset, :scopes),\n        message: message\n      }\n    )\n\n    conn\n    |> put_flash(:error, message)\n    |> redirect(to: Routes.user_session_path(conn, :new, %{request: request}))\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/user_registration_controller.ex",
    "content": "defmodule BorutaIdentityWeb.UserRegistrationController do\n  @behaviour BorutaIdentity.Accounts.RegistrationApplication\n\n  use BorutaIdentityWeb, :controller\n\n  import BorutaIdentityWeb.Authenticable,\n    only: [store_user_session: 2, after_registration_path: 1, client_id_from_request: 1]\n\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.RegistrationError\n  alias BorutaIdentityWeb.TemplateView\n\n  def new(conn, _params) do\n    client_id = client_id_from_request(conn)\n\n    Accounts.initialize_registration(conn, client_id, __MODULE__)\n  end\n\n  def create(%Plug.Conn{query_params: query_params} = conn, %{\"user\" => user_params}) do\n    client_id = client_id_from_request(conn)\n    request = query_params[\"request\"]\n\n    registration_params = %{\n      email: user_params[\"email\"],\n      password: user_params[\"password\"],\n      metadata: user_params[\"metadata\"]\n    }\n\n    Accounts.register(\n      conn,\n      client_id,\n      registration_params,\n      &Routes.user_confirmation_url(conn, :confirm, &1, %{request: request}),\n      __MODULE__\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.RegistrationApplication\n  def registration_initialized(%Plug.Conn{} = conn, template) do\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\", template: template, assigns: %{})\n  end\n\n  @impl BorutaIdentity.Accounts.RegistrationApplication\n  def registration_failure(%Plug.Conn{} = conn, %RegistrationError{\n        changeset: %Ecto.Changeset{} = changeset,\n        template: template\n      }) do\n    client_id = client_id_from_request(conn)\n\n    :telemetry.execute(\n      [:registration, :create, :failure],\n      %{},\n      %{\n        client_id: client_id,\n        error: changeset\n      }\n    )\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        changeset: changeset\n      }\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.RegistrationApplication\n  def registration_failure(%Plug.Conn{} = conn, %RegistrationError{\n        message: message,\n        template: template\n      }) do\n    client_id = client_id_from_request(conn)\n\n    :telemetry.execute(\n      [:registration, :create, :failure],\n      %{},\n      %{\n        client_id: client_id,\n        error: message\n      }\n    )\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        errors: [message]\n      }\n    )\n  end\n\n  def registration_failure(%Plug.Conn{} = conn, %RegistrationError{\n        user: user,\n        message: message,\n        template: template\n      }) do\n    client_id = client_id_from_request(conn)\n\n    # NOTE user is registered but his email is not confirmed\n    :telemetry.execute(\n      [:registration, :create, :success],\n      %{},\n      %{\n        client_id: client_id,\n        sub: user.uid,\n        backend: user.backend,\n        message: message\n      }\n    )\n\n    conn\n    |> put_layout(false)\n    |> put_flash(:info, \"Confirmation email has been sent. Please go to your mailbox and follow the provided link.\")\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        errors: [message]\n      }\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.RegistrationApplication\n  def user_registered(conn, user, session_token) do\n    client_id = client_id_from_request(conn)\n\n    :telemetry.execute(\n      [:registration, :create, :success],\n      %{},\n      %{\n        client_id: client_id,\n        sub: user.uid,\n        backend: user.backend\n      }\n    )\n\n    conn\n    |> store_user_session(session_token)\n    |> put_session(:session_chosen, true)\n    |> redirect(to: after_registration_path(conn))\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/user_reset_password_controller.ex",
    "content": "defmodule BorutaIdentityWeb.UserResetPasswordController do\n  @behaviour BorutaIdentity.Accounts.ResetPasswordApplication\n\n  use BorutaIdentityWeb, :controller\n\n  import BorutaIdentityWeb.Authenticable, only: [client_id_from_request: 1]\n\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.ResetPasswordError\n  alias BorutaIdentityWeb.TemplateView\n\n  def new(conn, _params) do\n    client_id = client_id_from_request(conn)\n\n    Accounts.initialize_password_instructions(conn, client_id, __MODULE__)\n  end\n\n  def create(%Plug.Conn{query_params: query_params} = conn, %{\"user\" => %{\"email\" => email}}) do\n    request = query_params[\"request\"]\n    client_id = client_id_from_request(conn)\n\n    user_params = %{\n      email: email\n    }\n\n    Accounts.send_reset_password_instructions(\n      conn,\n      client_id,\n      user_params,\n      &Routes.user_reset_password_url(conn, :edit, &1, %{request: request}),\n      __MODULE__\n    )\n  end\n\n  def edit(conn, params) do\n    client_id = client_id_from_request(conn)\n\n    Accounts.initialize_password_reset(conn, client_id, params[\"token\"], __MODULE__)\n  end\n\n  def update(conn, params) do\n    client_id = client_id_from_request(conn)\n\n    user_params = Map.get(params, \"user\", %{})\n\n    reset_password_params = %{\n      reset_password_token: params[\"token\"],\n      password: user_params[\"password\"],\n      password_confirmation: user_params[\"password_confirmation\"]\n    }\n\n    Accounts.reset_password(conn, client_id, reset_password_params, __MODULE__)\n  end\n\n  @impl BorutaIdentity.Accounts.ResetPasswordApplication\n  def password_instructions_initialized(\n        conn,\n        template\n      ) do\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{}\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.ResetPasswordApplication\n  def reset_password_instructions_delivered(%Plug.Conn{query_params: query_params} = conn) do\n    request = query_params[\"request\"]\n\n    conn\n    |> put_flash(\n      :info,\n      \"If your email is in our system, you will receive instructions to reset your password shortly.\"\n    )\n    |> redirect(to: Routes.user_session_path(conn, :new, %{request: request}))\n  end\n\n  @impl BorutaIdentity.Accounts.ResetPasswordApplication\n  def password_reset_initialized(\n        conn,\n        token,\n        template\n      ) do\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{token: token}\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.ResetPasswordApplication\n  def password_reseted(%Plug.Conn{query_params: query_params} = conn, _user) do\n    request = query_params[\"request\"]\n\n    # Do not log in the user after reset password to avoid a\n    # leaked token giving the user access to the account.\n    conn\n    |> put_flash(:info, \"Password reset successfully.\")\n    |> redirect(to: Routes.user_session_path(conn, :new, %{request: request}))\n  end\n\n  @impl BorutaIdentity.Accounts.ResetPasswordApplication\n  def password_reset_failure(%Plug.Conn{} = conn, %ResetPasswordError{\n        template: template,\n        changeset: %Ecto.Changeset{} = changeset,\n        token: token\n      }) do\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        changeset: changeset,\n        token: token\n      }\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.ResetPasswordApplication\n  def password_reset_failure(conn, %ResetPasswordError{\n        template: template,\n        token: token,\n        message: message\n      }) do\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        errors: [message],\n        token: token\n      }\n    )\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/user_session_controller.ex",
    "content": "defmodule BorutaIdentityWeb.UserSessionController do\n  @behaviour BorutaIdentity.Accounts.SessionApplication\n  @behaviour BorutaIdentity.TotpAuthenticationApplication\n  @behaviour BorutaIdentity.WebauthnAuthenticationApplication\n\n  use BorutaIdentityWeb, :controller\n\n  import BorutaIdentityWeb.Authenticable,\n    only: [\n      store_user_session: 2,\n      get_user_session: 1,\n      remove_user_session: 1,\n      after_sign_in_path: 1,\n      after_sign_out_path: 1,\n      client_id_from_request: 1\n    ]\n\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.SessionError\n  alias BorutaIdentity.Totp\n  alias BorutaIdentity.TotpError\n  alias BorutaIdentity.Webauthn\n  alias BorutaIdentity.WebauthnError\n  alias BorutaIdentityWeb.TemplateView\n\n  def new(conn, _params) do\n    client_id = client_id_from_request(conn)\n\n    Accounts.initialize_session(conn, client_id, __MODULE__)\n  end\n\n  def create(conn, %{\"user\" => user_params}) do\n    client_id = client_id_from_request(conn)\n\n    authentication_params = %{\n      email: user_params[\"email\"],\n      password: user_params[\"password\"]\n    }\n\n    Accounts.create_session(conn, client_id, authentication_params, __MODULE__)\n  end\n\n  def delete(conn, _params) do\n    client_id = client_id_from_request(conn)\n    session_token = get_user_session(conn)\n\n    Accounts.delete_session(conn, client_id, session_token, __MODULE__)\n  end\n\n  def initialize_totp(conn, _params) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    conn\n    |> Totp.initialize_totp(client_id, current_user, __MODULE__)\n  end\n\n  def authenticate_totp(conn, %{\"totp\" => totp_params}) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    totp_params = %{\n      totp_code: totp_params[\"totp_code\"]\n    }\n\n    Totp.authenticate_totp(conn, client_id, current_user, totp_params, __MODULE__)\n  end\n\n  def initialize_webauthn(conn, _params) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    conn\n    |> Webauthn.initialize_webauthn(client_id, current_user, __MODULE__)\n  end\n\n  @dialyzer {:no_return, {:authenticate_webauthn, 2}}\n  def authenticate_webauthn(conn, params) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    webauthn_params = %{\n      signature: params[\"signature\"],\n      authenticator_data: params[\"authenticator_data\"],\n      client_data: params[\"client_data\"],\n      identifier: params[\"identifier\"],\n      type: params[\"type\"]\n    }\n\n    Webauthn.authenticate_webauthn(conn, client_id, current_user, webauthn_params, __MODULE__)\n  end\n\n  @impl BorutaIdentity.WebauthnAuthenticationApplication\n  def webauthn_registration_missing(%Plug.Conn{query_params: query_params} = conn) do\n    conn\n    |> put_flash(:warning, \"You need to register a TOTP authenticator before continue.\")\n    |> redirect(\n      to: Routes.webauthn_path(BorutaIdentityWeb.Endpoint, :new, %{request: query_params[\"request\"]})\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.SessionApplication\n  def session_initialized(%Plug.Conn{} = conn, template) do\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{}\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.SessionApplication\n  def user_authenticated(conn, user, session_token) do\n    client_id = client_id_from_request(conn)\n\n    :telemetry.execute(\n      [:authentication, :log_in, :success],\n      %{},\n      %{\n        sub: user.uid,\n        backend: user.backend,\n        client_id: client_id\n      }\n    )\n\n    conn\n    |> store_user_session(session_token)\n    |> Totp.initialize_totp(client_id, user, __MODULE__)\n  end\n\n  @impl BorutaIdentity.Accounts.SessionApplication\n  def authentication_failure(%Plug.Conn{} = conn, %SessionError{\n        message: message,\n        template: template\n      }) do\n    client_id = client_id_from_request(conn)\n\n    :telemetry.execute(\n      [:authentication, :log_in, :failure],\n      %{},\n      %{\n        message: message,\n        client_id: client_id\n      }\n    )\n\n    conn\n    |> put_layout(false)\n    |> put_status(:unauthorized)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        errors: [message]\n      }\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.SessionApplication\n  def session_deleted(conn) do\n    client_id = client_id_from_request(conn)\n    user = conn.assigns[:current_user]\n\n    :telemetry.execute(\n      [:authentication, :log_out, :success],\n      %{},\n      %{\n        sub: user && user.uid,\n        backend: user && user.backend,\n        client_id: client_id\n      }\n    )\n\n    conn\n    |> remove_user_session()\n    |> put_flash(:info, \"Logged out successfully.\")\n    |> redirect(to: after_sign_out_path(conn))\n  end\n\n  @impl BorutaIdentity.TotpAuthenticationApplication\n  def totp_not_required(conn) do\n    conn\n    |> put_session(:session_chosen, true)\n    |> redirect(to: after_sign_in_path(conn))\n  end\n\n  @impl BorutaIdentity.TotpAuthenticationApplication\n  def totp_registration_missing(%Plug.Conn{query_params: query_params} = conn) do\n    conn\n    |> put_flash(:warning, \"You need to register a TOTP authenticator before continue.\")\n    |> redirect(\n      to: Routes.totp_path(BorutaIdentityWeb.Endpoint, :new, %{request: query_params[\"request\"]})\n    )\n  end\n\n  @impl BorutaIdentity.TotpAuthenticationApplication\n  def totp_initialized(%Plug.Conn{} = conn, template) do\n    current_user = conn.assigns[:current_user]\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        current_user: current_user\n      }\n    )\n  end\n\n  @impl BorutaIdentity.WebauthnAuthenticationApplication\n  def webauthn_initialized(%Plug.Conn{} = conn, webauthn_options, template) do\n    current_user = conn.assigns[:current_user]\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        current_user: current_user,\n        webauthn_options: webauthn_options\n      }\n    )\n  end\n\n  @impl BorutaIdentity.TotpAuthenticationApplication\n  def totp_authenticated(%Plug.Conn{} = conn, _user) do\n    conn\n    |> put_session(\n      :totp_authenticated,\n      (get_session(conn, :totp_authenticated) || %{})\n      |> Map.put(get_user_session(conn), true)\n    )\n    |> put_session(:session_chosen, true)\n    |> redirect(to: after_sign_in_path(conn))\n  end\n\n  @impl BorutaIdentity.TotpAuthenticationApplication\n  def totp_authentication_failure(%Plug.Conn{} = conn, %TotpError{\n        message: message,\n        template: template\n      }) do\n    current_user = conn.assigns.current_user\n\n    conn\n    |> put_layout(false)\n    |> put_status(:unauthorized)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        errors: [message],\n        current_user: current_user\n      }\n    )\n  end\n\n  @impl BorutaIdentity.WebauthnAuthenticationApplication\n  def webauthn_not_required(conn) do\n    conn\n    |> put_session(:session_chosen, true)\n    |> redirect(to: after_sign_in_path(conn))\n  end\n\n  @impl BorutaIdentity.WebauthnAuthenticationApplication\n  def webauthn_authenticated(%Plug.Conn{} = conn, _user) do\n    conn\n    |> put_session(\n      :webauthn_authenticated,\n      (get_session(conn, :webauthn_authenticated) || %{})\n      |> Map.put(get_user_session(conn), true)\n    )\n    |> put_session(:session_chosen, true)\n    |> redirect(to: after_sign_in_path(conn))\n  end\n\n  @impl BorutaIdentity.WebauthnAuthenticationApplication\n  def webauthn_authentication_failure(%Plug.Conn{} = conn, %WebauthnError{\n        message: message,\n        webauthn_options: webauthn_options,\n        template: template\n      }) do\n    current_user = conn.assigns.current_user\n\n    conn\n    |> put_layout(false)\n    |> put_status(:unauthorized)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        errors: [message],\n        webauthn_options: webauthn_options,\n        current_user: current_user\n      }\n    )\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/user_settings_controller.ex",
    "content": "defmodule BorutaIdentityWeb.UserSettingsController do\n  @behaviour BorutaIdentity.Accounts.SettingsApplication\n\n  use BorutaIdentityWeb, :controller\n\n  import BorutaIdentityWeb.Authenticable,\n    only: [\n      client_id_from_request: 1,\n      get_user_session: 1,\n      remove_user_session: 1,\n      after_sign_out_path: 1\n    ]\n\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.SettingsError\n  alias BorutaIdentityWeb.TemplateView\n\n  def edit(conn, _params) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    Accounts.initialize_edit_user(conn, client_id, current_user, __MODULE__)\n  end\n\n  def update(%Plug.Conn{query_params: query_params} = conn, %{\"user\" => user_params}) do\n    request = query_params[\"request\"]\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    user_update_params =\n      user_params\n      |> Enum.map(fn {key, value} -> {String.to_atom(key), value} end)\n      |> Enum.into(%{})\n\n    Accounts.update_user(\n      conn,\n      client_id,\n      current_user,\n      user_update_params,\n      &Routes.user_confirmation_url(conn, :confirm, &1, %{request: request}),\n      __MODULE__\n    )\n  end\n\n  def destroy(conn, _params) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    Accounts.destroy_user(conn, client_id, current_user, __MODULE__)\n  end\n\n  @impl BorutaIdentity.Accounts.SettingsApplication\n  def edit_user_initialized(conn, user, template) do\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\", template: template, assigns: %{current_user: user})\n  end\n\n  @impl BorutaIdentity.Accounts.SettingsApplication\n  def user_updated(%Plug.Conn{query_params: query_params} = conn, user) do\n    request = Map.get(query_params, \"request\")\n    client_id = client_id_from_request(conn)\n\n    :telemetry.execute(\n      [:registration, :update, :success],\n      %{},\n      %{\n        client_id: client_id,\n        sub: user.uid,\n        backend: user.backend\n      }\n    )\n\n    conn\n    |> put_flash(:info, \"Your information has been updated.\")\n    |> redirect(to: Routes.user_settings_path(conn, :edit, request: request))\n  end\n\n  @impl BorutaIdentity.Accounts.SettingsApplication\n  def user_update_failure(%Plug.Conn{} = conn, %SettingsError{\n        changeset: %Ecto.Changeset{} = changeset,\n        template: template\n      }) do\n    client_id = client_id_from_request(conn)\n    user = conn.assigns[:current_user]\n\n    :telemetry.execute(\n      [:registration, :update, :failure],\n      %{},\n      %{\n        client_id: client_id,\n        sub: user.uid,\n        backend: user.backend,\n        error: changeset\n      }\n    )\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        changeset: changeset\n      }\n    )\n  end\n\n  def user_update_failure(%Plug.Conn{} = conn, %SettingsError{\n        message: message,\n        template: template\n      }) do\n    client_id = client_id_from_request(conn)\n    user = conn.assigns[:current_user]\n\n    :telemetry.execute(\n      [:registration, :update, :failure],\n      %{},\n      %{\n        client_id: client_id,\n        sub: user.uid,\n        backend: user.backend,\n        error: message\n      }\n    )\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        errors: [message]\n      }\n    )\n  end\n\n  @impl BorutaIdentity.Accounts.SettingsApplication\n  def user_destroyed(conn, user) do\n    client_id = client_id_from_request(conn)\n    session_token = get_user_session(conn)\n\n    :telemetry.execute(\n      [:registration, :destroy, :success],\n      %{},\n      %{\n        client_id: client_id,\n        uid: user.uid,\n        id: user.id,\n        backend: user.backend\n      }\n    )\n\n    conn\n    |> remove_user_session()\n    |> put_flash(:info, \"User data destroyed.\")\n    Accounts.delete_session(conn, client_id, session_token, __MODULE__)\n  end\n\n  def session_deleted(conn) do\n    client_id = client_id_from_request(conn)\n    user = conn.assigns[:current_user]\n\n    :telemetry.execute(\n      [:authentication, :log_out, :success],\n      %{},\n      %{\n        sub: user && user.uid,\n        backend: user && user.backend,\n        client_id: client_id\n      }\n    )\n\n    conn\n    |> remove_user_session()\n    |> put_flash(:info, \"Your data has been deleted.\")\n    |> redirect(to: after_sign_out_path(conn))\n  end\n\n  @impl BorutaIdentity.Accounts.SettingsApplication\n  def user_destroy_failure(%Plug.Conn{} = conn, %SettingsError{\n        message: message,\n        template: template\n      }) do\n    client_id = client_id_from_request(conn)\n    user = conn.assigns[:current_user]\n\n    :telemetry.execute(\n      [:registration, :destroy, :failure],\n      %{},\n      %{\n        client_id: client_id,\n        uid: user.uid,\n        id: user.id,\n        backend: user.backend,\n        error: message\n      }\n    )\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> put_status(:unprocessable_entity)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        errors: [message]\n      }\n    )\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/wallet_controller.ex",
    "content": "defmodule BorutaIdentityWeb.WalletController do\n  use BorutaIdentityWeb, :controller\n\n  def index(conn, _params) do\n    conn\n    |> put_layout(false)\n    |> render(\"index.html\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/controllers/webauthn_controller.ex",
    "content": "defmodule BorutaIdentityWeb.WebauthnController do\n  @behaviour BorutaIdentity.WebauthnRegistrationApplication\n\n  use BorutaIdentityWeb, :controller\n\n  import BorutaIdentityWeb.Authenticable,\n    only: [\n      get_user_session: 1,\n      client_id_from_request: 1,\n      after_sign_in_path: 1\n    ]\n\n  alias BorutaIdentity.Webauthn\n  alias BorutaIdentity.WebauthnError\n  alias BorutaIdentityWeb.TemplateView\n\n  def new(conn, _params) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    webauthn_authenticated = Map.get(\n      get_session(conn, :webauthn_authenticated) || %{},\n      get_user_session(conn),\n      false\n    )\n    Webauthn.initialize_webauthn_registration(conn, client_id, webauthn_authenticated, current_user, __MODULE__)\n  end\n\n  def register(conn, params) do\n    client_id = client_id_from_request(conn)\n    current_user = conn.assigns[:current_user]\n\n    webauthn_params = %{\n      attestation: params[\"attestation\"],\n      client_data: params[\"client_data\"],\n      identifier: params[\"identifier\"],\n      type: params[\"type\"]\n    }\n\n    Webauthn.register_webauthn(conn, client_id, current_user, webauthn_params, __MODULE__)\n  end\n\n  @impl BorutaIdentity.WebauthnRegistrationApplication\n  def webauthn_registration_initialized(conn, webauthn_options, template) do\n    current_user = conn.assigns[:current_user]\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        current_user: current_user,\n        webauthn_options: webauthn_options\n      }\n    )\n  end\n\n  @impl BorutaIdentity.WebauthnRegistrationApplication\n  def webauthn_registration_error(conn, %WebauthnError{\n        message: message,\n        webauthn_options: webauthn_options,\n        template: template\n      }) do\n    current_user = conn.assigns[:current_user]\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        errors: [message],\n        webauthn_options: webauthn_options,\n        current_user: current_user\n      }\n    )\n  end\n\n  @impl BorutaIdentity.WebauthnRegistrationApplication\n  def webauthn_registration_success(%Plug.Conn{} = conn, _user) do\n    conn\n    |> put_flash(:info, \"Passkey registered successfully.\")\n    |> put_session(\n      :webauthn_authenticated,\n      (get_session(conn, :webauthn_authenticated) || %{})\n      |> Map.put(get_user_session(conn), true)\n    )\n    |> redirect(to: after_sign_in_path(conn))\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/endpoint.ex",
    "content": "defmodule BorutaIdentityWeb.Endpoint do\n  use Phoenix.Endpoint, otp_app: :boruta_identity\n\n  @session_options [\n    store: :cookie,\n    key: \"_boruta_web_key\",\n    signing_salt: \"OCKBuS86\"\n  ]\n\n  # Serve at \"/\" the static files from \"priv/static\" directory.\n  #\n  # You should set gzip to true if you are running phx.digest\n  # when deploying your static files in production.\n  plug RemoteIp\n  plug Plug.Static,\n    at: \"/\",\n    from: :boruta_identity,\n    gzip: false,\n    only: ~w(images wallet manifest.json favicon.ico robots.txt semantic-ui.min.css)\n\n  plug Plug.RequestId\n  plug Plug.Telemetry,\n    event_prefix: [:boruta_identity, :endpoint],\n    log: {__MODULE__, :log_level, []}\n\n  plug Plug.Parsers,\n    parsers: [:urlencoded, :multipart, :json],\n    pass: [\"*/*\"],\n    json_decoder: Phoenix.json_library()\n\n  plug Plug.MethodOverride\n  plug Plug.Head\n  plug Plug.Session, @session_options\n  plug BorutaIdentityWeb.Router\n\n  def log_level(%{path_info: [\"healthcheck\" | _]}), do: false\n  def log_level(_), do: :info\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/gettext.ex",
    "content": "defmodule BorutaIdentityWeb.Gettext do\n  @moduledoc \"\"\"\n  A module providing Internationalization with a gettext-based API.\n\n  By using [Gettext](https://hexdocs.pm/gettext),\n  your module gains a set of macros for translations, for example:\n\n      import BorutaIdentityWeb.Gettext\n\n      # Simple translation\n      gettext(\"Here is the string to translate\")\n\n      # Plural translation\n      ngettext(\"Here is the string to translate\",\n               \"Here are the strings to translate\",\n               3)\n\n      # Domain-based translation\n      dgettext(\"errors\", \"Here is the error message to translate\")\n\n  See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.\n  \"\"\"\n  use Gettext.Backend, otp_app: :boruta_identity\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/plugs/sessions.ex",
    "content": "defmodule BorutaIdentityWeb.Sessions do\n  @moduledoc false\n\n  use BorutaIdentityWeb, :controller\n\n  import BorutaIdentityWeb.Authenticable, only: [remember_me_cookie: 0, after_sign_in_path: 1]\n\n  alias BorutaIdentity.Accounts\n\n  @doc \"\"\"\n  Authenticates the user by looking into the session\n  and remember me token.\n  \"\"\"\n  @spec fetch_current_user(conn :: Plug.Conn.t(), list()) :: conn :: Plug.Conn.t()\n  def fetch_current_user(conn, _opts) do\n    {user_token, conn} = ensure_user_token(conn)\n    user = user_token && Accounts.get_user_by_session_token(user_token)\n    assign(conn, :current_user, user)\n  end\n\n  defp ensure_user_token(conn) do\n    if user_token = get_session(conn, :user_token) do\n      {user_token, conn}\n    else\n      conn = fetch_cookies(conn, signed: [remember_me_cookie()])\n\n      if user_token = conn.cookies[remember_me_cookie()] do\n        {user_token, put_session(conn, :user_token, user_token)}\n      else\n        {nil, conn}\n      end\n    end\n  end\n\n  @doc \"\"\"\n  Used for routes that require the user to not be authenticated.\n  \"\"\"\n  @spec redirect_if_user_is_authenticated(conn :: Plug.Conn.t(), list()) :: conn :: Plug.Conn.t()\n  def redirect_if_user_is_authenticated(conn, _opts) do\n    if conn.assigns[:current_user] do\n      conn\n      |> redirect(to: after_sign_in_path(conn))\n      |> halt()\n    else\n      conn\n    end\n  end\n\n  @doc \"\"\"\n  Used for routes that require the user to be authenticated.\n\n  If you want to enforce the user email is confirmed before\n  they use the application at all, here would be a good place.\n  \"\"\"\n  @spec require_authenticated_user(conn :: Plug.Conn.t(), list()) :: conn :: Plug.Conn.t()\n  def require_authenticated_user(conn, _opts) do\n    if conn.assigns[:current_user] do\n      conn\n    else\n      conn\n      |> put_flash(:error, \"You must log in to access this page.\")\n      |> redirect(to: Routes.user_session_path(conn, :new, conn.query_params))\n      |> halt()\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/router.ex",
    "content": "defmodule BorutaIdentityWeb.Router do\n  use BorutaIdentityWeb, :router\n  use Plug.ErrorHandler\n\n  import BorutaIdentityWeb.Sessions,\n    only: [\n      fetch_current_user: 2,\n      redirect_if_user_is_authenticated: 2,\n      require_authenticated_user: 2\n    ]\n  require Logger\n\n  alias BorutaIdentity.Configuration\n  alias BorutaIdentity.Configuration.ErrorTemplate\n\n  pipeline :browser do\n    plug(:accepts, [\"html\"])\n    plug(:fetch_session)\n    plug(:fetch_flash)\n    plug(:protect_from_forgery)\n    plug(:put_secure_browser_headers)\n    plug(:fetch_current_user)\n  end\n\n  pipeline :api do\n    plug(:accepts, [\"json\"])\n  end\n\n  # scope \"/\", BorutaIdentityWeb do\n  #   pipe_through :browser\n\n  #   get \"/\", PageController, :index\n  # end\n\n  ## Authentication routes\n\n  scope \"/\", BorutaIdentityWeb do\n    pipe_through([:browser, :redirect_if_user_is_authenticated])\n\n    get(\"/users/register\", UserRegistrationController, :new)\n    post(\"/users/register\", UserRegistrationController, :create)\n    get(\"/users/log_in\", UserSessionController, :new)\n    post(\"/users/log_in\", UserSessionController, :create)\n    get(\"/users/reset_password\", UserResetPasswordController, :new)\n    post(\"/users/reset_password\", UserResetPasswordController, :create)\n  end\n\n  scope \"/\", BorutaIdentityWeb do\n    pipe_through([:browser, :require_authenticated_user])\n\n    get(\"/users/totp_registration\", TotpController, :new)\n    post(\"/users/totp_registration\", TotpController, :register)\n    get(\"/users/webauthn_registration\", WebauthnController, :new)\n    post(\"/users/register_webauthn\", WebauthnController, :register)\n    get(\"/users/totp\", UserSessionController, :initialize_totp)\n    post(\"/users/totp_authenticate\", UserSessionController, :authenticate_totp)\n    get(\"/users/webauthn\", UserSessionController, :initialize_webauthn)\n    post(\"/users/webauthn_authenticate\", UserSessionController, :authenticate_webauthn)\n    get(\"/users/choose_session\", ChooseSessionController, :index)\n    get(\"/users/consent\", UserConsentController, :index)\n    post(\"/users/consent\", UserConsentController, :consent)\n    get(\"/users/settings\", UserSettingsController, :edit)\n    put(\"/users/settings\", UserSettingsController, :update)\n    post(\"/users/destroy\", UserSettingsController, :destroy)\n  end\n\n  scope \"/\", BorutaIdentityWeb do\n    pipe_through([:browser])\n\n    get(\"/backends/:id/:federated_server_name/authorize\", BackendsController, :authorize)\n    get(\"/backends/:id/:federated_server_name/callback\", BackendsController, :callback)\n    get(\"/users/log_out\", UserSessionController, :delete)\n    get(\"/users/confirm\", UserConfirmationController, :new)\n    post(\"/users/confirm\", UserConfirmationController, :create)\n    get(\"/users/confirm/:token\", UserConfirmationController, :confirm)\n    get(\"/users/reset_password/:token\", UserResetPasswordController, :edit)\n    put(\"/users/reset_password/:token\", UserResetPasswordController, :update)\n    get(\"/wallet\", WalletController, :index)\n  end\n\n  scope \"/wallet\", BorutaIdentityWeb do\n    pipe_through(:browser)\n\n    match(:get, \"/*path\", WalletController, :index)\n  end\n\n  @impl Plug.ErrorHandler\n  def handle_errors(conn, %{reason: %Plug.CSRFProtection.InvalidCSRFTokenError{message: message}}) do\n    with [referer] <- Plug.Conn.get_req_header(conn, \"referer\"),\n         %URI{path: path, query: query} <- URI.parse(referer) do\n      uri = %URI{path: path, query: query}\n\n      conn\n      |> Plug.Conn.fetch_session()\n      |> Phoenix.Controller.fetch_flash()\n      |> Phoenix.Controller.put_flash(:error, message)\n      |> Plug.Conn.put_status(:found)\n      |> Phoenix.Controller.redirect(to: URI.to_string(uri))\n    else\n      _ ->\n        render_error(conn, message)\n    end\n  end\n\n  def handle_errors(conn, %{reason: reason}) do\n    reason = %{\n      message: Map.get(reason, :message, inspect(reason))\n    }\n\n    render_error(conn, reason)\n  end\n\n  defp render_error(conn, reason) do\n    Logger.error(\"conn: #{inspect(conn)} reason: #{inspect(reason)}\")\n    %ErrorTemplate{content: template} = Configuration.get_error_template!(conn.status)\n\n    context = %{\n      reason: reason,\n      boruta_logo_path:\n        BorutaIdentityWeb.Router.Helpers.static_path(\n          BorutaIdentityWeb.Endpoint,\n          \"/images/logo-yellow.png\"\n        )\n    }\n\n    content = Mustachex.render(template, context)\n\n    conn\n    |> put_resp_content_type(\"text/html\")\n    |> send_resp(conn.status, content)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/telemetry.ex",
    "content": "defmodule BorutaIdentityWeb.Telemetry do\n  @moduledoc false\n\n  use Supervisor\n  import Telemetry.Metrics\n\n  def start_link(arg) do\n    Supervisor.start_link(__MODULE__, arg, name: __MODULE__)\n  end\n\n  @impl true\n  def init(_arg) do\n    children = [\n      # Telemetry poller will execute the given period measurements\n      # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics\n      {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}\n      # Add reporters as children of your supervision tree.\n      # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}\n    ]\n\n    Supervisor.init(children, strategy: :one_for_one)\n  end\n\n  def metrics do\n    [\n      # Phoenix Metrics\n      summary(\"phoenix.endpoint.stop.duration\",\n        unit: {:native, :millisecond}\n      ),\n      summary(\"phoenix.router_dispatch.stop.duration\",\n        tags: [:route],\n        unit: {:native, :millisecond}\n      ),\n\n      # Database Metrics\n      summary(\"boruta_identity.repo.query.total_time\", unit: {:native, :millisecond}),\n      summary(\"boruta_identity.repo.query.decode_time\", unit: {:native, :millisecond}),\n      summary(\"boruta_identity.repo.query.query_time\", unit: {:native, :millisecond}),\n      summary(\"boruta_identity.repo.query.queue_time\", unit: {:native, :millisecond}),\n      summary(\"boruta_identity.repo.query.idle_time\", unit: {:native, :millisecond}),\n\n      # VM Metrics\n      summary(\"vm.memory.total\", unit: {:byte, :kilobyte}),\n      summary(\"vm.total_run_queue_lengths.total\"),\n      summary(\"vm.total_run_queue_lengths.cpu\"),\n      summary(\"vm.total_run_queue_lengths.io\")\n    ]\n  end\n\n  defp periodic_measurements do\n    [\n      # A module, function and arguments to be invoked periodically.\n      # This function must call :telemetry.execute/3 and a metric must be added above.\n      # {BorutaIdentityWeb, :count_users, []}\n    ]\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/templates/error/400.html.eex",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Phoenix Framework</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <div class=\"ui error message\" style=\"position: absolute; top: 1em; right: 1em; left: 1em; text-align: center\">\n        <%= @reason.message %>\n      </div>\n      <h1 class=\"ui icon header\">\n        <i class=\"redo alternate icon\"></i>\n        <div class=\"content\">\n          Request could not be processed\n          <div class=\"sub header\">The given request could not be processed. Please retry with valid parameters.</div>\n        </div>\n      </h1>\n    </div>\n    </main>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/templates/error/403.html.eex",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Phoenix Framework</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <div class=\"ui error message\" style=\"position: absolute; top: 1em; right: 1em; left: 1em; text-align: center\">\n        <%= @reason.message %>\n      </div>\n      <h1 class=\"ui icon header\">\n        <i class=\"ban icon\"></i>\n        <div class=\"content\">\n          Forbidden\n          <div class=\"sub header\">You are forbidden to access this resource.</div>\n        </div>\n      </h1>\n    </div>\n    </main>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/templates/error/404.html.eex",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Phoenix Framework</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <h1 class=\"ui icon header\">\n        <i class=\"question icon\"></i>\n        <div class=\"content\">\n          Page not found\n          <div class=\"sub header\">The page you requested was not found. Please contact your administrator.</div>\n        </div>\n      </h1>\n    </div>\n    </main>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/templates/error/500.html.eex",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Phoenix Framework</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <h1 class=\"ui icon header\">\n        <i class=\"exclamation icon\"></i>\n        <div class=\"content\">\n          Internal server error\n          <div class=\"sub header\">An unexpected error occured. Please contact your administrator.</div>\n        </div>\n      </h1>\n    </div>\n    </main>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/templates/layout/app.html.eex",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>BorutaIdentity · Identity provider</title>\n    <link rel=\"stylesheet\" href=\"<%= Routes.static_path(@conn, \"/accounts/css/app.css\") %>\"/>\n    <script defer type=\"text/javascript\" src=\"<%= Routes.static_path(@conn, \"/accounts/js/app.js\") %>\"></script>\n  </head>\n  <body>\n    <div class=\"wrapper\">\n      <header>\n\n      </header>\n      <main role=\"main\" class=\"container\">\n        <div class=\"alert alert-info\" role=\"alert\"><%= get_flash(@conn, :info) %></div>\n        <div class=\"alert alert-danger\" role=\"alert\"><%= get_flash(@conn, :error) %></div>\n        <div class=\"content\">\n          <%= @inner_content %>\n        </div>\n      </main>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/templates/wallet/index.html.eex",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Administration panel</title>\n    <link rel=\"stylesheet\" href=\"/accounts/semantic-ui.min.css\" />\n    <script>\n      window.env = {\n        BORUTA_ADMIN_OAUTH_CLIENT_ID: '<%= System.get_env(\"BORUTA_ADMIN_OAUTH_CLIENT_ID\", \"6a2f41a3-c54c-fce8-32d2-0324e1c32e20\") %>',\n        BORUTA_ADMIN_OAUTH_BASE_URL: '<%= System.get_env(\"BORUTA_ADMIN_OAUTH_BASE_URL\", \"http://localhost:4000\") %>',\n        BORUTA_ADMIN_BASE_URL: '<%= System.get_env(\"BORUTA_ADMIN_BASE_URL\", \"http://localhost:4001\") %>',\n        BORUTA_OAUTH_BASE_URL: '<%= System.get_env(\"BORUTA_OAUTH_BASE_URL\", \"http://localhost:4000\") %>',\n      }\n    </script>\n    <link rel=\"manifest\" type=\"text/css\" href=\"<%= Routes.static_path(@conn, \"/wallet/manifest.webmanifest\") %>\" media=\"all\"/>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"<%= Routes.static_path(@conn, \"/wallet/boruta-wallet.css\") %>\" media=\"all\"/>\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but boruta-admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n\n    <script type=\"module\" src=\"<%= Routes.static_path(@conn, \"/wallet/app.es.js\") %>\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/token.ex",
    "content": "defmodule BorutaIdentityWeb.Token do\n  @moduledoc false\n\n  use Joken.Config\n\n  def application_signer do\n    Joken.Signer.create(\n      \"HS512\",\n      Application.get_env(:boruta_identity, BorutaIdentityWeb.Endpoint)[:secret_key_base]\n    )\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/views/error_helpers.ex",
    "content": "defmodule BorutaIdentityWeb.ErrorHelpers do\n  @moduledoc \"\"\"\n  Conveniences for translating and building error messages.\n  \"\"\"\n\n  use Phoenix.HTML\n\n  def error_messages(nil), do: []\n\n  def error_messages(%Ecto.Changeset{} = changeset) do\n    Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->\n      Regex.replace(~r\"%{(\\w+)}\", msg, fn _, key ->\n        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()\n      end)\n    end)\n    |> Enum.map(&error_message/1)\n  end\n\n  def error_message({field, messages}) do\n    message = Enum.flat_map(messages, fn\n      errors when is_map(errors) ->\n        Enum.map(errors, &error_message/1)\n      message ->\n        [message]\n    end)\n    |> Enum.join(\", \")\n\n    Phoenix.Naming.humanize(field) <> \": \" <> message\n  end\n\n  def errors_tag(errors) do\n    content_tag(\n      :ul,\n      Enum.map(errors, &error_tag/1)\n    )\n  end\n\n  def error_tag({field, {_msg, _opts} = error}) do\n    content_tag(\n      :li,\n      [\n        content_tag(\n          :strong,\n          Phoenix.Naming.humanize(field) <> \":\"\n        ),\n        content_tag(:span, \" \"),\n        content_tag(:span, translate_error(error))\n      ]\n    )\n  end\n\n  def error_tag({field, [\"\" <> _first | _rest] = messages}) do\n    content_tag(\n      :li,\n      [\n        content_tag(\n          :strong,\n          Atom.to_string(field)\n        ),\n        content_tag(:span, \" \"),\n        content_tag(:span, Enum.join(messages, \", \"))\n      ]\n    )\n  end\n\n  def error_tag({field, errors}) when is_list(errors) do\n    Enum.map(errors, fn\n      %{} = errors ->\n        [\n          content_tag(\n            :strong,\n            Atom.to_string(field)\n          ),\n          content_tag(:span, \" \"),\n          Enum.map(errors, fn error -> error_tag(error) end)\n        ]\n    end)\n  end\n\n  @doc \"\"\"\n  Generates tag for inlined form input errors.\n  \"\"\"\n  def error_tag(form, field) do\n    Enum.map(Keyword.get_values(form.errors, field), fn error ->\n      content_tag(:span, translate_error(error))\n    end)\n  end\n\n  @doc \"\"\"\n  Translates an error message using gettext.\n  \"\"\"\n  def translate_error({msg, opts}) do\n    # When using gettext, we typically pass the strings we want\n    # to translate as a static argument:\n    #\n    #     # Translate \"is invalid\" in the \"errors\" domain\n    #     dgettext(\"errors\", \"is invalid\")\n    #\n    #     # Translate the number of files with plural rules\n    #     dngettext(\"errors\", \"1 file\", \"%{count} files\", count)\n    #\n    # Because the error messages we show in our forms and APIs\n    # are defined inside Ecto, we need to translate them dynamically.\n    # This requires us to call the Gettext module passing our gettext\n    # backend as first argument.\n    #\n    # Note we use the \"errors\" domain, which means translations\n    # should be written to the errors.po file. The :count option is\n    # set by Ecto and indicates we should also apply plural rules.\n    if count = opts[:count] do\n      Gettext.dngettext(BorutaIdentityWeb.Gettext, \"errors\", msg, msg, count, opts)\n    else\n      Gettext.dgettext(BorutaIdentityWeb.Gettext, \"errors\", msg, opts)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/views/error_view.ex",
    "content": "defmodule BorutaIdentityWeb.ErrorView do\n  use BorutaIdentityWeb, :view\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/views/template_view.ex",
    "content": "defmodule BorutaIdentityWeb.TemplateView do\n  use BorutaIdentityWeb, :view\n\n  alias BorutaIdentity.IdentityProviders.Template\n  alias BorutaIdentityWeb.ErrorHelpers\n\n  def render(\"template.html\", %{\n        conn: conn,\n        template: %Template{\n          layout: layout,\n          content: content,\n          identity_provider: identity_provider\n        },\n        assigns: assigns\n      }) do\n    assigns =\n      assigns\n      |> Map.put(:identity_provider, identity_provider)\n      |> Map.put(:conn, conn)\n\n    context =\n      context(%{}, assigns)\n      |> Map.put(:messages, messages(conn))\n      |> Map.put(:_csrf_token, Plug.CSRFProtection.get_csrf_token())\n      |> Map.merge(errors(assigns))\n      |> Map.merge(paths(conn, assigns))\n      |> Map.merge(identity_provider_configurations(identity_provider))\n\n    {:safe, Mustachex.render(layout.content, context, partials: %{inner_content: content})}\n  end\n\n  def context(context, %{conn: conn, identity_provider: identity_provider} = assigns) do\n    %Plug.Conn{query_params: query_params} = conn\n    request = Map.get(query_params, \"request\")\n    backend = identity_provider.backend\n\n    federated_servers =\n      Enum.map(backend.federated_servers, fn federated_server ->\n        federated_server_name = federated_server[\"name\"]\n\n        {federated_server_name,\n         %{\n           login_url:\n             Routes.backends_path(\n               BorutaIdentityWeb.Endpoint,\n               :authorize,\n               backend.id,\n               federated_server_name,\n               %{request: request}\n             )\n         }}\n      end)\n      |> Enum.into(%{})\n\n    %{federated_servers: federated_servers}\n    |> Map.merge(context)\n    |> context(Map.delete(assigns, :identity_provider))\n  end\n\n  def context(context, %{current_user: current_user, totp_secret: totp_secret} = assigns) do\n    {:ok, base64_totp_registration_qr_code} =\n      BorutaIdentity.Totp.Admin.url(current_user.username, totp_secret)\n      |> QRCode.create()\n      |> QRCode.render(:svg)\n      |> QRCode.to_base64()\n\n    %{\n      totp_secret: totp_secret,\n      base64_totp_registration_qr_code: base64_totp_registration_qr_code\n    }\n    |> Map.merge(context)\n    |> context(Map.delete(assigns, :totp_secret))\n  end\n\n  def context(context, %{client: client} = assigns) do\n    client = Map.take(client, [:name])\n\n    %{client: client}\n    |> Map.merge(context)\n    |> context(Map.delete(assigns, :client))\n  end\n\n  def context(context, %{resource_owner: resource_owner} = assigns) do\n    resource_owner = Map.take(resource_owner, [:sub, :extra_claims, :claims])\n\n    %{resource_owner: resource_owner}\n    |> Map.merge(context)\n    |> context(Map.delete(assigns, :resource_owner))\n  end\n\n  def context(context, %{credential_offer: credential_offer} = assigns) do\n    {:ok, base64_credential_offer_qr_code} =\n      text_from_credential_offer(credential_offer)\n      |> QRCode.create()\n      |> QRCode.render(:svg)\n      |> QRCode.to_base64()\n\n    %{\n      base64_credential_offer_qr_code: base64_credential_offer_qr_code,\n      credential_offer_deeplink: text_from_credential_offer(credential_offer)\n    }\n    |> Map.merge(context)\n    |> context(Map.delete(assigns, :credential_offer))\n  end\n\n  def context(context, %{presentation_deeplink: presentation_deeplink} = assigns) do\n    {:ok, base64_presentation_qr_code} = presentation_deeplink\n      |> QRCode.create()\n      |> QRCode.render(:svg)\n      |> QRCode.to_base64()\n\n    %{\n      base64_presentation_qr_code: base64_presentation_qr_code,\n      presentation_deeplink: presentation_deeplink\n    }\n    |> Map.merge(context)\n    |> context(Map.delete(assigns, :presentation_deeplink))\n  end\n\n  def context(context, %{webauthn_options: webauthn_options} = assigns) do\n    options = Map.from_struct(webauthn_options)\n\n    %{webauthn_options: options}\n    |> Map.merge(context)\n    |> context(Map.delete(assigns, :webauthn_options))\n  end\n\n  def context(context, %{code: code} = assigns) do\n\n    %{code: code}\n    |> Map.merge(context)\n    |> context(Map.delete(assigns, :code))\n  end\n\n  def context(context, %{current_user: current_user} = assigns) do\n    current_user = Map.take(current_user, [:username, :webauthn_registered_at, :totp_registered_at, :metadata])\n\n    current_user = %{\n      current_user\n      | totp_registered_at:\n          current_user.totp_registered_at &&\n            current_user.totp_registered_at |> DateTime.truncate(:second) |> DateTime.to_string(),\n      webauthn_registered_at:\n          current_user.webauthn_registered_at &&\n            current_user.webauthn_registered_at |> DateTime.truncate(:second) |> DateTime.to_string()\n    }\n\n    %{current_user: current_user}\n    |> Map.merge(context)\n    |> context(Map.delete(assigns, :current_user))\n  end\n\n  def context(context, %{scopes: scopes} = assigns) do\n    scopes = Enum.map(scopes, &Map.from_struct/1)\n\n    %{scopes: scopes}\n    |> Map.merge(context)\n    |> context(Map.delete(assigns, :scopes))\n  end\n\n  def context(context, %{}), do: context\n\n  defp text_from_credential_offer(credential_offer) do\n    # TODO Jason.Encode implementation for CredentialOfferResponse\n    \"#{credential_offer.redirect_uri}?credential_offer=#{credential_offer\n      |> Map.from_struct()\n      |> Map.take([:credential_configuration_ids, :client_id, :credential_issuer, :grants])\n      |> Jason.encode!()\n      |> URI.encode_www_form()}\"\n  end\n\n  defp paths(conn, assigns) do\n    %Plug.Conn{query_params: query_params} = conn\n    request = Map.get(query_params, \"request\")\n\n    %{\n      boruta_logo_path: Routes.static_path(BorutaIdentityWeb.Endpoint, \"/images/logo-yellow.png\"),\n      choose_session_path:\n        Routes.choose_session_path(BorutaIdentityWeb.Endpoint, :index, %{request: request}),\n      create_user_reset_password_path:\n        Routes.user_reset_password_path(BorutaIdentityWeb.Endpoint, :create, %{request: request}),\n      create_user_confirmation_path:\n        Routes.user_confirmation_path(BorutaIdentityWeb.Endpoint, :create, %{request: request}),\n      create_user_consent_path: Routes.user_consent_path(conn, :consent, %{request: request}),\n      create_user_registration_path:\n        Routes.user_registration_path(BorutaIdentityWeb.Endpoint, :create, %{request: request}),\n      create_user_session_path:\n        Routes.user_session_path(BorutaIdentityWeb.Endpoint, :create, %{request: request}),\n      create_user_session_totp_authentication_path:\n        Routes.user_session_path(BorutaIdentityWeb.Endpoint, :authenticate_totp, %{\n          request: request\n        }),\n      create_user_session_webauthn_authentication_path:\n        Routes.user_session_path(BorutaIdentityWeb.Endpoint, :authenticate_webauthn, %{\n          request: request\n        }),\n      delete_user_session_path:\n        Routes.user_session_path(BorutaIdentityWeb.Endpoint, :delete, %{request: request}),\n      edit_user_path:\n        Routes.user_settings_path(BorutaIdentityWeb.Endpoint, :edit, %{request: request}),\n      destroy_user_path:\n        Routes.user_settings_path(BorutaIdentityWeb.Endpoint, :destroy, %{request: request}),\n      new_user_totp_registration_path:\n        Routes.totp_path(BorutaIdentityWeb.Endpoint, :new, %{request: request}),\n      create_user_totp_registration_path:\n        Routes.totp_path(BorutaIdentityWeb.Endpoint, :register, %{request: request}),\n      new_user_webauthn_registration_path:\n        Routes.webauthn_path(BorutaIdentityWeb.Endpoint, :new, %{request: request}),\n      create_user_webauthn_registration_path:\n        Routes.webauthn_path(BorutaIdentityWeb.Endpoint, :register, %{request: request}),\n      new_user_registration_path:\n        Routes.user_registration_path(BorutaIdentityWeb.Endpoint, :new, %{request: request}),\n      new_user_reset_password_path:\n        Routes.user_reset_password_path(BorutaIdentityWeb.Endpoint, :new, %{request: request}),\n      new_user_session_path:\n        Routes.user_session_path(BorutaIdentityWeb.Endpoint, :new, %{request: request}),\n      update_user_reset_password_path:\n        Routes.user_reset_password_path(\n          BorutaIdentityWeb.Endpoint,\n          :update,\n          Map.get(assigns, :token, \"\"),\n          %{request: request}\n        ),\n      update_user_path:\n        Routes.user_settings_path(BorutaIdentityWeb.Endpoint, :update, %{request: request})\n    }\n  end\n\n  defp errors(%{errors: errors}) do\n    formatted_errors = Enum.map(errors, &%{message: &1})\n\n    %{valid?: false, errors: formatted_errors}\n  end\n\n  defp errors(%{changeset: changeset}) do\n    formatted_errors =\n      changeset\n      |> ErrorHelpers.error_messages()\n      |> Enum.map(fn message -> %{message: message} end)\n\n    %{valid?: false, errors: formatted_errors}\n  end\n\n  defp errors(_assigns), do: %{errors: [], valid?: true}\n\n  defp messages(conn) do\n    get_flash(conn)\n    |> Enum.map(fn {type, value} ->\n      %{\n        \"type\" => type,\n        \"content\" => value\n      }\n    end)\n  end\n\n  defp identity_provider_configurations(identity_provider) do\n    %{\n      registrable?: identity_provider.registrable,\n      totpable?: identity_provider.totpable,\n      user_editable?: identity_provider.user_editable\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web/views/wallet_view.ex",
    "content": "defmodule BorutaIdentityWeb.WalletView do\n  use BorutaIdentityWeb, :view\nend\n"
  },
  {
    "path": "apps/boruta_identity/lib/boruta_identity_web.ex",
    "content": "defmodule BorutaIdentityWeb do\n  @moduledoc \"\"\"\n  The entrypoint for defining your web interface, such\n  as controllers, views, channels and so on.\n\n  This can be used in your application as:\n\n      use BorutaIdentityWeb, :controller\n      use BorutaIdentityWeb, :view\n\n  The definitions below will be executed for every view,\n  controller, etc, so keep them short and clean, focused\n  on imports, uses and aliases.\n\n  Do NOT define functions inside the quoted expressions\n  below. Instead, define any helper function in modules\n  and import those modules here.\n  \"\"\"\n\n  def controller do\n    quote do\n      use Phoenix.Controller, namespace: BorutaIdentityWeb\n\n      import Plug.Conn\n      import BorutaIdentityWeb.Gettext\n      alias BorutaIdentityWeb.Router.Helpers, as: Routes\n    end\n  end\n\n  def view do\n    quote do\n      use Phoenix.View,\n        root: \"lib/boruta_identity_web/templates\",\n        namespace: BorutaIdentityWeb\n\n      # Import convenience functions from controllers\n      import Phoenix.Controller,\n        only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]\n\n      # Include shared imports and aliases for views\n      unquote(view_helpers())\n    end\n  end\n\n  def router do\n    quote do\n      use Phoenix.Router\n\n      import Plug.Conn\n      import Phoenix.Controller\n    end\n  end\n\n  def channel do\n    quote do\n      use Phoenix.Channel\n      import BorutaIdentityWeb.Gettext\n    end\n  end\n\n  defp view_helpers do\n    quote do\n      # Use all HTML functionality (forms, tags, etc)\n      use Phoenix.HTML\n\n      # Import basic rendering functionality (render, render_layout, etc)\n      import Phoenix.View\n\n      import BorutaIdentityWeb.ErrorHelpers\n      import BorutaIdentityWeb.Gettext\n      alias BorutaIdentityWeb.Router.Helpers, as: Routes\n    end\n  end\n\n  @doc \"\"\"\n  When used, dispatch to the appropriate controller/view/etc.\n  \"\"\"\n  defmacro __using__(which) when is_atom(which) do\n    apply(__MODULE__, which, [])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/mix.exs",
    "content": "defmodule BorutaIdentity.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :boruta_identity,\n      version: \"0.1.0\",\n      build_path: \"../../_build\",\n      config_path: \"../../config/config.exs\",\n      deps_path: \"../../deps\",\n      lockfile: \"../../mix.lock\",\n      elixir: \"~> 1.7\",\n      elixirc_paths: elixirc_paths(Mix.env()),\n      compilers: [:phoenix] ++ Mix.compilers(),\n      start_permanent: Mix.env() == :prod,\n      aliases: aliases(),\n      deps: deps()\n    ]\n  end\n\n  # Configuration for the OTP application.\n  #\n  # Type `mix help compile.app` for more information.\n  def application do\n    [\n      mod: {BorutaIdentity.Application, []},\n      extra_applications: [:logger, :runtime_tools, :eldap, :boruta]\n    ]\n  end\n\n  # Specifies which paths to compile per environment.\n  defp elixirc_paths(:test), do: [\"lib\", \"test/support\"]\n  defp elixirc_paths(_), do: [\"lib\"]\n\n  # Specifies your project dependencies.\n  #\n  # Type `mix help deps` for examples and options.\n  defp deps do\n    [\n      {:argon2_elixir, \"~> 2.0\"},\n      {:bcrypt_elixir, \"~> 3.0\"},\n      {:boruta_auth, in_umbrella: true},\n      {:bypass, \"~> 2.1.0\", only: :test},\n      {:decorator, \"~> 1.2\"},\n      {:ecto_sql, \"~> 3.4\"},\n      {:ex_json_schema, \"~> 0.9\"},\n      {:ex_machina, \"~> 2.4\", only: :test},\n      {:finch, \"~> 0.8\"},\n      {:gen_smtp, \"~> 1.1\"},\n      {:gettext, \"~> 0.11\"},\n      {:jason, \"~> 1.0\"},\n      {:mox, \"~> 1.0\"},\n      {:mustachex, git: \"https://github.com/jui/mustachex.git\"},\n      {:nebulex, \"~> 2.0\"},\n      {:shards, \"~> 1.0\"},\n      {:nimble_csv, \"~> 1.2\"},\n      {:nimble_pool, \"~> 0.2\"},\n      {:oauth2, \"~> 2.0\"},\n      {:pbkdf2_elixir, \"~> 2.0\"},\n      {:phoenix, \"~> 1.6.0\", override: true},\n      {:phoenix_ecto, \"~> 4.1\"},\n      {:phoenix_html, \"~> 3.0\"},\n      {:phoenix_live_reload, \"~> 1.2\", only: :dev},\n      {:phoenix_pubsub, \"~> 2.0\"},\n      {:plug_cowboy, \"~> 2.0\"},\n      {:postgrex, \">= 0.0.0\"},\n      {:qr_code, \"~> 3.0.0\"},\n      {:remote_ip, \"~> 1.1\"},\n      {:scrivener_ecto, \"~> 2.7\"},\n      {:secure_random, \"~> 0.5\"},\n      {:swoosh, \"~> 1.5\"},\n      {:telemetry_metrics, \"~> 0.6\"},\n      {:telemetry_poller, \"~> 0.5\"},\n      {:wax_, \"~> 0.6.0\"}\n    ]\n  end\n\n  # Aliases are shortcuts or tasks specific to the current project.\n  # For example, to install project dependencies and perform other setup tasks, run:\n  #\n  #     $ mix setup\n  #\n  # See the documentation for `Mix` for more info on aliases.\n  defp aliases do\n    [\n      setup: [\"deps.get\", \"ecto.setup\", \"cmd npm install --prefix assets\"],\n      \"ecto.setup\": [\"ecto.create\", \"ecto.migrate\", \"run priv/repo/seeds.exs\"],\n      \"ecto.reset\": [\"ecto.drop\", \"ecto.setup\"],\n      test: [\"ecto.create --quiet\", \"ecto.migrate --quiet\", \"test\"]\n    ]\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/gettext/en/LC_MESSAGES/errors.po",
    "content": "## `msgid`s in this file come from POT (.pot) files.\n##\n## Do not add, change, or remove `msgid`s manually here as\n## they're tied to the ones in the corresponding POT file\n## (with the same domain).\n##\n## Use `mix gettext.extract --merge` or `mix gettext.merge`\n## to merge POT files into PO files.\nmsgid \"\"\nmsgstr \"\"\n\"Language: en\\n\"\n\n## From Ecto.Changeset.cast/4\nmsgid \"can't be blank\"\nmsgstr \"\"\n\n## From Ecto.Changeset.unique_constraint/3\nmsgid \"has already been taken\"\nmsgstr \"\"\n\n## From Ecto.Changeset.put_change/3\nmsgid \"is invalid\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_acceptance/3\nmsgid \"must be accepted\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_format/3\nmsgid \"has invalid format\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_subset/3\nmsgid \"has an invalid entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_exclusion/3\nmsgid \"is reserved\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_confirmation/3\nmsgid \"does not match confirmation\"\nmsgstr \"\"\n\n## From Ecto.Changeset.no_assoc_constraint/3\nmsgid \"is still associated with this entry\"\nmsgstr \"\"\n\nmsgid \"are still associated with this entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_length/3\nmsgid \"should be %{count} character(s)\"\nmsgid_plural \"should be %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have %{count} item(s)\"\nmsgid_plural \"should have %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at least %{count} character(s)\"\nmsgid_plural \"should be at least %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at least %{count} item(s)\"\nmsgid_plural \"should have at least %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at most %{count} character(s)\"\nmsgid_plural \"should be at most %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at most %{count} item(s)\"\nmsgid_plural \"should have at most %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n## From Ecto.Changeset.validate_number/3\nmsgid \"must be less than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be less than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be equal to %{number}\"\nmsgstr \"\"\n"
  },
  {
    "path": "apps/boruta_identity/priv/gettext/errors.pot",
    "content": "## This is a PO Template file.\n##\n## `msgid`s here are often extracted from source code.\n## Add new translations manually only if they're dynamic\n## translations that can't be statically extracted.\n##\n## Run `mix gettext.extract` to bring this file up to\n## date. Leave `msgstr`s empty as changing them here has no\n## effect: edit them in PO (`.po`) files instead.\n## From Ecto.Changeset.cast/4\nmsgid \"can't be blank\"\nmsgstr \"\"\n\n## From Ecto.Changeset.unique_constraint/3\nmsgid \"has already been taken\"\nmsgstr \"\"\n\n## From Ecto.Changeset.put_change/3\nmsgid \"is invalid\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_acceptance/3\nmsgid \"must be accepted\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_format/3\nmsgid \"has invalid format\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_subset/3\nmsgid \"has an invalid entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_exclusion/3\nmsgid \"is reserved\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_confirmation/3\nmsgid \"does not match confirmation\"\nmsgstr \"\"\n\n## From Ecto.Changeset.no_assoc_constraint/3\nmsgid \"is still associated with this entry\"\nmsgstr \"\"\n\nmsgid \"are still associated with this entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_length/3\nmsgid \"should be %{count} character(s)\"\nmsgid_plural \"should be %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have %{count} item(s)\"\nmsgid_plural \"should have %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at least %{count} character(s)\"\nmsgid_plural \"should be at least %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at least %{count} item(s)\"\nmsgid_plural \"should have at least %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at most %{count} character(s)\"\nmsgid_plural \"should be at most %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at most %{count} item(s)\"\nmsgid_plural \"should have at most %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n## From Ecto.Changeset.validate_number/3\nmsgid \"must be less than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be less than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be equal to %{number}\"\nmsgstr \"\"\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/.formatter.exs",
    "content": "[\n  import_deps: [:ecto_sql],\n  inputs: [\"*.exs\"]\n]\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20210127190501_create_users_auth_tables.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateUsersAuthTables do\n  use Ecto.Migration\n\n  def change do\n    execute \"CREATE EXTENSION IF NOT EXISTS citext\", \"\"\n\n    drop_if_exists table(:users)\n    create table(:users, primary_key: false) do\n      add :id, :binary_id, primary_key: true\n      add :email, :citext, null: false\n      add :hashed_password, :string, null: false\n      add :confirmed_at, :naive_datetime\n      timestamps()\n    end\n\n    create unique_index(:users, [:email])\n\n    create table(:users_tokens, primary_key: false) do\n      add :id, :binary_id, primary_key: true\n      add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false\n      add :token, :binary, null: false\n      add :context, :string, null: false\n      add :sent_to, :string\n      timestamps(updated_at: false)\n    end\n\n    create index(:users_tokens, [:user_id])\n    create unique_index(:users_tokens, [:context, :token])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20210128080043_create_users_authorized_scopes.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateUsersAuthorizedScopes do\n  use Ecto.Migration\n\n  def change do\n    create(table(:users_authorized_scopes, primary_key: false)) do\n      add :id, :uuid, primary_key: true\n      add :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false\n      add :name, :string, null: false\n\n      timestamps()\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20210208110903_user_authorized_scopes_unique_index.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.UserAuthorizedScopesUniqueIndex do\n  use Ecto.Migration\n\n  def change do\n    create unique_index(:users_authorized_scopes, [:name, :user_id])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20210302213536_create_consents.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateConsents do\n  use Ecto.Migration\n\n  def change do\n    create table(:consents, primary_key: false) do\n      add :id, :binary_id, primary_key: true\n\n      add :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false\n      add :client_id, :string, null: false\n      add :scopes, {:array, :string}, default: []\n\n      timestamps()\n    end\n\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20210806194842_add_last_login_at_to_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddLastLoginAtToUsers do\n  use Ecto.Migration\n\n  def change do\n    alter table(:users) do\n      add :last_login_at, :utc_datetime_usec\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20211002132445_modify_users_confirmed_at.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.ModifyUsersConfirmedAt do\n  use Ecto.Migration\n\n  def change do\n    alter table(:users) do\n      modify :confirmed_at, :utc_datetime_usec\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20211129225646_create_relying_parties.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateRelyingParties do\n  use Ecto.Migration\n\n  def change do\n    create table(:relying_parties, primary_key: false) do\n      add :id, :binary_id, primary_key: true\n      add :name, :string\n      add :type, :string\n\n      timestamps()\n    end\n\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20211130230927_create_clients_relying_parties.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateClientsRelyingParties do\n  use Ecto.Migration\n\n  def change do\n    create table(:clients_relying_parties, primary_key: false) do\n      add :id, :binary_id, primary_key: true\n\n      add :relying_party_id, references(:relying_parties, type: :uuid, on_delete: :delete_all), null: false\n      add :client_id, :uuid, null: false\n\n      timestamps()\n    end\n\n    create index(\"clients_relying_parties\", [:client_id], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220117220007_add_registrable_to_relying_parties.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddRegistrableToRelyingParties do\n  use Ecto.Migration\n\n  def change do\n    alter table(:relying_parties) do\n      add :registrable, :boolean, null: false, default: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220118122834_add_unique_name_to_relying_parties.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddUniqueNameToRelyingParties do\n  use Ecto.Migration\n\n  def change do\n    create index(:relying_parties, [:name], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220120214356_create_relying_party_templates.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateRelyingPartyTemplates do\n  use Ecto.Migration\n\n  def change do\n    create table(:relying_party_templates, primary_key: false) do\n      add :id, :binary_id, primary_key: true\n\n      add :relying_party_id, references(:relying_parties, type: :uuid, on_delete: :delete_all), null: false\n      add :type, :string, null: false\n      add :content, :text, default: \"\"\n\n      timestamps()\n    end\n\n    create index(:relying_party_templates, [:relying_party_id, :type], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220131133951_add_confirmable_to_relying_parties.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddConfirmableToRelyingParties do\n  use Ecto.Migration\n\n  def change do\n    alter table(:relying_parties) do\n      add :confirmable, :boolean, default: false, null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220218144931_add_consentable_to_relying_parties.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddConsentableToRelyingParties do\n  use Ecto.Migration\n\n  def change do\n    alter table(:relying_parties) do\n      add :consentable, :boolean, default: false, null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220221123627_add_choose_session_to_relying_parties.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddChooseSessionToRelyingParties do\n  use Ecto.Migration\n\n  def change do\n    alter table(:relying_parties) do\n      add :choose_session, :boolean, default: true, null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220520212652_add_user_editable_to_relying_parties.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddUserEditableToRelyingParties do\n  use Ecto.Migration\n\n  def change do\n    alter table(:relying_parties) do\n      add :user_editable, :boolean, default: false, null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220528155902_create_internal_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateInternalUsers do\n  use Ecto.Migration\n\n  def up do\n    drop constraint(:users_authorized_scopes, \"users_authorized_scopes_user_id_fkey\")\n    alter table(:users_authorized_scopes) do\n      modify :user_id, :uuid\n    end\n    drop constraint(:users_tokens, \"users_tokens_user_id_fkey\")\n    alter table(:users_tokens) do\n      modify :user_id, :uuid\n    end\n    drop constraint(:consents, \"consents_user_id_fkey\")\n    alter table(:consents) do\n      modify :user_id, :uuid\n    end\n\n    rename table(:users), to: table(:internal_users)\n\n    execute(\"CREATE TABLE users as (SELECT * FROM internal_users)\")\n\n    alter table(:users, primary_key: false) do\n      modify :id, :uuid, primary_key: true\n      add :provider, :string\n      add :uid, :string\n      remove :hashed_password\n    end\n\n    execute(\"\"\"\n    ALTER TABLE users\n      ALTER COLUMN provider TYPE varchar(255)\n        USING '#{to_string(BorutaIdentity.Accounts.Internal)}'\n    \"\"\")\n    execute(\"\"\"\n    ALTER TABLE users\n      ALTER COLUMN provider SET NOT NULL\n    \"\"\")\n    execute(\"\"\"\n    ALTER TABLE users\n      ALTER COLUMN uid TYPE varchar(255)\n        USING (users.id::varchar)\n    \"\"\")\n    execute(\"\"\"\n    ALTER TABLE users\n      ALTER COLUMN uid SET NOT NULL\n    \"\"\")\n\n    rename table(:users), :email, to: :username\n\n    alter table(:internal_users) do\n      remove :last_login_at\n      remove :confirmed_at\n    end\n\n    create index(:users, [:provider, :uid], unique: true)\n\n    alter table(:users_authorized_scopes) do\n      modify :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false\n    end\n    alter table(:users_tokens) do\n      modify :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false\n    end\n    alter table(:consents) do\n      modify :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false\n    end\n  end\n\n  def down do\n    drop constraint(:users_authorized_scopes, \"users_authorized_scopes_user_id_fkey\")\n    alter table(:users_authorized_scopes) do\n      modify :user_id, :uuid\n    end\n    drop constraint(:users_tokens, \"users_tokens_user_id_fkey\")\n    alter table(:users_tokens) do\n      modify :user_id, :uuid\n    end\n    drop constraint(:consents, \"consents_user_id_fkey\")\n    alter table(:consents) do\n      modify :user_id, :uuid\n    end\n\n    drop index(:users, [:provider, :uid], unique: true)\n\n    alter table(:users, primary_key: false) do\n      add :hashed_password, :string\n    end\n\n    execute(\"\"\"\n    CREATE OR REPLACE FUNCTION hashed_password(uid varchar(255))\n    RETURNS varchar(255) AS\n    $$\n      DECLARE hp varchar(255);\n      BEGIN\n        SELECT hashed_password INTO hp\n        FROM internal_users\n        WHERE id = $1::uuid;\n\n        RETURN hp;\n      END;\n    $$ LANGUAGE plpgsql\n    \"\"\")\n    execute(\"\"\"\n    ALTER TABLE users\n      ALTER COLUMN hashed_password TYPE varchar(255)\n        USING hashed_password(users.uid)\n    \"\"\")\n    execute(\"\"\"\n    ALTER TABLE users\n      ALTER COLUMN hashed_password SET NOT NULL\n    \"\"\")\n\n    alter table(:users, primary_key: false) do\n      remove :provider\n      remove :uid\n    end\n\n    drop table(:internal_users)\n\n    rename table(:users), :username, to: :email\n\n    alter table(:users_authorized_scopes) do\n      modify :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false\n    end\n    alter table(:users_tokens) do\n      modify :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false\n    end\n    alter table(:consents) do\n      modify :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220607201657_add_user_authorized_scopes_scope_id.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddUserAuthorizedScopesScopeId do\n  use Ecto.Migration\n\n  alias Boruta.Ecto.Admin\n  alias BorutaAuth.Repo\n\n  def up do\n    Repo.start_link([])\n\n    {:ok, %Postgrex.Result{rows: scopes}} =\n      BorutaAuth.Repo.query(\"\"\"\n        SELECT id, name\n        FROM oauth_scopes\n      \"\"\")\n\n    json_scopes = Enum.map(scopes, fn [id, name] -> %{id: id, name: name} end)\n\n    execute(\"\"\"\n    CREATE OR REPLACE FUNCTION scope_id_from_name(name varchar(255))\n    RETURNS uuid AS\n    $$\n      DECLARE scope_id varchar(255);\n      BEGIN\n        SELECT scopes::jsonb ->> 'id' INTO scope_id\n        FROM unnest(ARRAY['#{json_scopes |> Enum.join(\"', '\")}']) as scopes\n        WHERE scopes::jsonb ->> 'name' = $1;\n\n        RETURN scope_id::uuid;\n      END;\n    $$ LANGUAGE plpgsql\n    \"\"\")\n\n    alter table(:users_authorized_scopes) do\n      add(:scope_id, :uuid)\n    end\n\n    create index(:users_authorized_scopes, [:scope_id, :user_id], unique: true)\n    drop index(:users_authorized_scopes, [:name, :user_id])\n\n    execute(\"\"\"\n    ALTER TABLE users_authorized_scopes\n      ALTER COLUMN scope_id TYPE varchar(255)\n        USING scope_id_from_name(users_authorized_scopes.name)\n    \"\"\")\n\n    execute(\"\"\"\n    ALTER TABLE users_authorized_scopes\n      ALTER COLUMN scope_id SET NOT NULL\n    \"\"\")\n\n    alter table(:users_authorized_scopes) do\n      remove(:name)\n    end\n  end\n\n  def down do\n    Repo.start_link([])\n\n    json_scopes =\n      Admin.list_scopes()\n      |> Enum.map(fn %{name: name, id: id} -> %{name: name, id: id} end)\n      |> Enum.map(&Jason.encode!/1)\n\n    execute(\"\"\"\n    CREATE OR REPLACE FUNCTION scope_name_from_id(id varchar(255))\n    RETURNS varchar(255) AS\n    $$\n      DECLARE scope_name varchar(255);\n      BEGIN\n        SELECT scopes::jsonb ->> 'name' INTO scope_name\n        FROM unnest(ARRAY['#{json_scopes |> Enum.join(\"', '\")}']) as scopes\n        WHERE scopes::jsonb ->> 'id' = $1;\n\n        RETURN scope_name;\n      END;\n    $$ LANGUAGE plpgsql\n    \"\"\")\n\n    alter table(:users_authorized_scopes) do\n      add(:name, :string)\n    end\n\n    drop index(:users_authorized_scopes, [:scope_id, :user_id])\n    create index(:users_authorized_scopes, [:name, :user_id], unique: true)\n\n    execute(\"\"\"\n    ALTER TABLE users_authorized_scopes\n      ALTER COLUMN name TYPE varchar(255)\n        USING scope_name_from_id(users_authorized_scopes.scope_id)\n    \"\"\")\n\n    execute(\"\"\"\n    ALTER TABLE users_authorized_scopes\n      ALTER COLUMN name SET NOT NULL\n    \"\"\")\n\n    alter table(:users_authorized_scopes) do\n      remove(:scope_id)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220617195827_rename_relying_parties.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.RenameRelyingParties do\n  use Ecto.Migration\n\n  def change do\n    drop index(:relying_parties, [:name])\n    drop constraint(:clients_relying_parties, \"clients_relying_parties_relying_party_id_fkey\")\n    drop constraint(:relying_party_templates, \"relying_party_templates_relying_party_id_fkey\")\n\n    rename table(:relying_parties), to: table(:identity_providers)\n    rename table(:relying_party_templates), to: table(:identity_provider_templates)\n    rename table(:clients_relying_parties), to: table(:clients_identity_providers)\n\n    rename table(:identity_provider_templates), :relying_party_id, to: :identity_provider_id\n    alter table(:identity_provider_templates) do\n      modify :identity_provider_id, references(:identity_providers, type: :binary_id, on_delete: :delete_all)\n    end\n\n    rename table(:clients_identity_providers), :relying_party_id, to: :identity_provider_id\n    alter table(:clients_identity_providers) do\n      modify :identity_provider_id, references(:identity_providers, type: :binary_id, on_delete: :delete_all)\n    end\n\n    create index(:identity_providers, [:name], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220628073937_create_error_templates.exs",
    "content": "defmodule BorutaAuth.Repo.Migrations.CreateErrorTemplates do\n  use Ecto.Migration\n\n  def change do\n    create table(:error_templates, primary_key: false) do\n      add(:id, :uuid, primary_key: true)\n      add(:type, :string, null: false)\n      add(:content, :text, null: false)\n\n      timestamps()\n    end\n\n    create index(:error_templates, [:type], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220812123254_create_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateBackends do\n  use Ecto.Migration\n\n  def change do\n    create table(:backends, primary_key: false) do\n      add :id, :binary_id, primary_key: true\n      add :name, :string, null: false\n      add :type, :string, null: false\n      add :password_hashing_alg, :string, null: false\n      add :password_hashing_salt, :string, null: false, default: \"\"\n\n      timestamps()\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220815073225_add_backend_id_to_identity_providers.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddBackendIdToIdentityProviders do\n  use Ecto.Migration\n\n  def up do\n    backend_id = SecureRandom.uuid()\n    now = DateTime.utc_now()\n    execute(\"\"\"\n    INSERT INTO backends (id, type, name, password_hashing_alg, inserted_at, updated_at)\n      VALUES ('#{backend_id}', 'Elixir.BorutaIdentity.Accounts.Internal', 'Default', 'argon2', '#{DateTime.to_iso8601(now)}', '#{DateTime.to_iso8601(now)}')\n    \"\"\")\n\n    alter table(:identity_providers) do\n      add :backend_id, references(:backends, type: :binary_id, on_delete: :nothing)\n    end\n    execute(\"\"\"\n    UPDATE identity_providers\n      SET backend_id = '#{backend_id}'\n    \"\"\")\n    alter table(:identity_providers) do\n      modify :backend_id, :binary_id, null: false\n    end\n  end\n\n  def down do\n    alter table(:identity_providers) do\n      remove :backend_id, references(:backends, type: :binary_id, on_delete: :nothing), null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220815091033_remove_type_from_identity_providers.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.RemoveTypeFromIdentityProviders do\n  use Ecto.Migration\n\n  def change do\n    alter table(:identity_providers) do\n      remove :type, :string, null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220815115719_add_default_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddDefaultToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      add :is_default, :boolean, default: false, null: false\n    end\n    execute(\"\"\"\n      UPDATE backends SET is_default = true\n        WHERE id = (SELECT id FROM backends LIMIT 1)\n    \"\"\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220816074610_add_password_hashing_opts_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddPasswordHashingOptsToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      remove :password_hashing_salt, :string\n      add :password_hashing_opts, :map, default: %{}\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220817134821_change_users_provider_to_backend_id.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.ChangeUsersProviderToBackendId do\n  use Ecto.Migration\n\n  def up do\n    alter table(:users) do\n      remove :provider, :string\n      add :backend_id, references(:backends, type: :uuid, on_delete: :nothing)\n    end\n\n    execute(\"\"\"\n      UPDATE users\n        SET backend_id = (\n          SELECT id\n            FROM backends\n            WHERE type = 'Elixir.BorutaIdentity.Accounts.Internal'\n            LIMIT 1\n        )\n    \"\"\")\n\n    create index(:users, [:backend_id, :uid], unique: true)\n\n    alter table(:users) do\n      modify :backend_id, :uuid, null: false\n    end\n  end\n\n  def down do\n    alter table(:users) do\n      add :provider, :string, default: \"Elixir.BorutaIdentity.Accounts.Internal\"\n      remove :backend_id\n    end\n\n    create index(:users, [:provider, :uid], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220817150643_add_backend_id_to_internal_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddBackendIdToInternalUsers do\n  use Ecto.Migration\n\n  def up do\n    alter table(:internal_users) do\n      add :backend_id, references(:backends, type: :uuid, on_delete: :nothing)\n    end\n\n    execute(\"\"\"\n      UPDATE internal_users\n        SET backend_id = (\n          SELECT id\n            FROM backends\n            WHERE type = 'Elixir.BorutaIdentity.Accounts.Internal'\n            LIMIT 1\n        )\n    \"\"\")\n\n    create index(:internal_users, [:backend_id, :email], unique: true)\n\n    alter table(:internal_users) do\n      modify :backend_id, :uuid, null: false\n    end\n  end\n\n  def down do\n    alter table(:internal_users) do\n      remove :backend_id\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220826055043_add_mail_configuration_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddMailConfigurationToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      add :smtp_from, :string\n      add :smtp_relay, :string\n      add :smtp_username, :string\n      add :smtp_password, :string\n      add :smtp_tls, :string\n      add :smtp_port, :integer\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220904073628_create_pg_trgm_extension.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreatePgTrgmExtension do\n  use Ecto.Migration\n\n  def up do\n    execute(\"CREATE EXTENSION IF NOT EXISTS pg_trgm\")\n  end\n\n  def down do\n    execute(\"DROP EXTENSION pg_trgm\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220904183116_create_trgm_index.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateTrgmIndex do\n  use Ecto.Migration\n\n  def up do\n    execute(\"CREATE INDEX username_trgm_idx ON users USING GIN (username gin_trgm_ops)\")\n  end\n\n  def down do\n    execute(\"DROP INDEX username_trgm_idx\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220911195248_add_ldap_configuration_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddLdapConfigurationToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      add :ldap_pool_size, :integer, default: 5\n      add :ldap_host, :string\n      add :ldap_password, :string\n      add :ldap_base_dn, :string\n      add :ldap_ou, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20220915191039_add_ldap_ser_rdn_attribute_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddLdapSerRdnAttributeToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      remove :ldap_password, :string\n      add :ldap_user_rdn_attribute, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20221008202236_add_ldap_master_credentials_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddLdapMasterCredentialsToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      add :ldap_master_dn, :string\n      add :ldap_master_password, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20221026092004_create_email_templates.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateEmailTemplates do\n  use Ecto.Migration\n\n  def change do\n    create table(:email_templates, primary_key: false) do\n      add :id, :binary_id, primary_key: true\n\n      add :type, :string, null: false\n      add :txt_content, :text, default: \"\"\n      add :html_content, :text, default: \"\"\n\n      add :backend_id, references(:backends, type: :uuid, on_delete: :delete_all), null: false\n      timestamps()\n    end\n\n    create index(:email_templates, [:backend_id, :type], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20221026211130_add_smtp_ssl_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddSmtpSslToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      add :smtp_ssl, :boolean\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20221028083326_add_revoked_at_to_users_tokens.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddRevokedAtToUsersTokens do\n  use Ecto.Migration\n\n  def change do\n    alter table(:users_tokens) do\n      add :revoked_at, :utc_datetime_usec\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20221108140432_add_metadata_fields_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddMetadataFieldsToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      add :metadata_fields, {:array, :jsonb}, default: []\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20221108144651_add_metadata_to_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddMetadataToUsers do\n  use Ecto.Migration\n\n  def change do\n    alter table(:users) do\n      add :metadata, :jsonb, default: \"{}\", null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230303151220_add_group_to_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddGroupToUsers do\n  use Ecto.Migration\n\n  def change do\n    alter table(:users) do\n      add :group, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230502111802_add_identity_federation_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddIdentityFederationToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      add :federated_servers, {:array, :jsonb}, default: []\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230605073651_create_roles.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateRoles do\n  use Ecto.Migration\n\n  def change do\n    create table(:roles, primary_key: false) do\n      add :id, :uuid, primary_key: true\n      add :name, :string, null: false\n\n      timestamps()\n    end\n\n    create index(:roles, [:name], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230605074117_create_roles_scopes.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateRolesScopes do\n  use Ecto.Migration\n\n  def change do\n    create table(:roles_scopes, primary_key: false) do\n      add :id, :uuid, primary_key: true\n      add :role_id, references(:roles, type: :uuid, on_delete: :delete_all), null: false\n      add :scope_id, :uuid, null: false\n\n      timestamps()\n    end\n\n    create index(:roles_scopes, [:role_id, :scope_id], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230615082520_create_roles_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateRolesUsers do\n  use Ecto.Migration\n\n  def change do\n    create table(:roles_users, primary_key: false) do\n      add :id, :uuid, primary_key: true\n      add :role_id, references(:roles, type: :uuid, on_delete: :delete_all), null: false\n      add :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false\n\n      timestamps()\n    end\n\n    create index(:roles_users, [:role_id, :user_id], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230626064411_create_backends_roles.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateBackendsRoles do\n  use Ecto.Migration\n\n  def change do\n    create table(:backends_roles, primary_key: false) do\n      add :id, :uuid, primary_key: true\n      add :backend_id, references(:backends, type: :uuid, on_delete: :delete_all), null: false\n      add :role_id, references(:roles, type: :uuid, on_delete: :delete_all), null: false\n\n      timestamps()\n    end\n\n    create index(:backends_roles, [:backend_id, :role_id], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230805134343_add_totpable_to_identity_providers.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddTotpableToIdentityProviders do\n  use Ecto.Migration\n\n  def change do\n    alter table(:identity_providers) do\n      add :totpable, :boolean, default: false, null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230805160200_add_totp_to_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddTotpToUsers do\n  use Ecto.Migration\n\n  def change do\n    alter table(:users) do\n      add :totp_secret, :string\n      add :totp_registered_at, :utc_datetime_usec\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230810130509_add_enforce_totp_to_identity_providers.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddEnforceTotpToIdentityProviders do\n  use Ecto.Migration\n\n  def change do\n    alter table(:identity_providers) do\n      add :enforce_totp, :boolean, default: false, null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230903150227_create_organizations.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateOrganizations do\n  use Ecto.Migration\n\n  def change do\n    create table(:organizations, primary_key: false) do\n      add :id, :uuid, primary_key: true\n      add :name, :string, null: false\n\n      timestamps()\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230908094746_add_label_to_organizations.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddLabelToOrganizations do\n  use Ecto.Migration\n\n  def change do\n    alter table(:organizations) do\n      add :label, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230908113944_create_organizations_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.CreateOrganizationsUsers do\n  use Ecto.Migration\n\n  def change do\n    create table(:organizations_users, primary_key: false) do\n      add :id, :uuid, primary_key: true\n      add :organization_id, references(:organizations, type: :uuid), null: false\n      add :user_id, references(:users, type: :uuid), null: false\n\n      timestamps()\n    end\n\n    create index(:organizations_users, [:organization_id, :user_id], unique: true)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20230909103013_add_create_default_organization_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddCreateDefaultOrganizationToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      add :create_default_organization, :boolean, null: false, default: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20240109125818_add_verifiable_credentails_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddVerifiableCredentailsToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      add :verifiable_credentials, {:array, :jsonb}, default: []\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20240110094020_add_federated_metadata_to_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddFederatedMetadataToUsers do\n  use Ecto.Migration\n\n  def change do\n    alter table(:users) do\n      add :federated_metadata, :jsonb, default: \"{}\"\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20240426110841_organizations_users_reference.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.OrganizationsUsersReference do\n  use Ecto.Migration\n\n  def up do\n    execute \"ALTER TABLE organizations_users DROP CONSTRAINT organizations_users_organization_id_fkey\"\n    execute \"ALTER TABLE organizations_users DROP CONSTRAINT organizations_users_user_id_fkey\"\n\n    alter table(:organizations_users, primary_key: false) do\n      modify :organization_id, references(:organizations, type: :uuid, on_delete: :nothing), null: false\n      modify :user_id, references(:users, type: :uuid, on_delete: :nothing), null: false\n    end\n  end\n\n  def down do\n    execute \"ALTER TABLE organizations_users DROP CONSTRATAINT organizations_users_organization_id_fkey\"\n    execute \"ALTER TABLE organizations_users DROP CONSTRATAINT organizations_users_user_id_fkey\"\n\n    alter table(:organizations_users, primary_key: false) do\n      modify :organization_id, references(:organizations, type: :uuid), null: false\n      modify :user_id, references(:users, type: :uuid), null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20240505104631_organizations_users_reference2.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.OrganizationsUsersReference2 do\n  use Ecto.Migration\n\n  def up do\n    execute \"ALTER TABLE organizations_users DROP CONSTRAINT organizations_users_organization_id_fkey\"\n    execute \"ALTER TABLE organizations_users DROP CONSTRAINT organizations_users_user_id_fkey\"\n\n    alter table(:organizations_users, primary_key: false) do\n      modify :organization_id, references(:organizations, type: :uuid, on_delete: :delete_all), null: false\n      modify :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false\n    end\n  end\n\n  def down do\n    execute \"ALTER TABLE organizations_users DROP CONSTRATAINT organizations_users_organization_id_fkey\"\n    execute \"ALTER TABLE organizations_users DROP CONSTRATAINT organizations_users_user_id_fkey\"\n\n    alter table(:organizations_users, primary_key: false) do\n      modify :organization_id, references(:organizations, type: :uuid), null: false\n      modify :user_id, references(:users, type: :uuid), null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20240808195715_add_webauthn_challenge_to_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddWebauthnChallengeToUsers do\n  use Ecto.Migration\n\n  def change do\n    alter table(:users) do\n      add :webauthn_challenge, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20240820140733_add_webauthn_public_key_to_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddWebauthnPublicKeyToUsers do\n  use Ecto.Migration\n\n  def change do\n    alter table(:users) do\n      add :webauthn_public_key, :text\n      add :webauthn_registered_at, :utc_datetime_usec\n      add :webauthn_identifier, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20240820233604_add_webauthn_to_identity_providers.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddWebauthnToIdentityProviders do\n  use Ecto.Migration\n\n  def change do\n    alter table(:identity_providers) do\n      add :webauthnable, :boolean, default: false, null: false\n      add :enforce_webauthn, :boolean, default: false, null: false\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20240907103609_add_verifiable_presentations_to_backends.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddVerifiablePresentationsToBackends do\n  use Ecto.Migration\n\n  def change do\n    alter table(:backends) do\n      add :verifiable_presentations, {:array, :jsonb}, default: []\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20241017153124_add_account_type_to_users.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddAccountTypeToUsers do\n  use Ecto.Migration\n\n  def change do\n    alter table(:users) do\n      add :account_type, :string\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20241130000259_add_check_password_to_identity_providers.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.AddCheckPasswordToIdentityProviders do\n  use Ecto.Migration\n\n  def change do\n    alter table(:identity_providers) do\n      add :check_password, :boolean, null: false, default: true\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/migrations/20250302193825_remove_global_email_unique_constraint.exs",
    "content": "defmodule BorutaIdentity.Repo.Migrations.RemoveGlobalEmailUniqueConstraint do\n  use Ecto.Migration\n\n  def change do\n    execute(\"DROP INDEX users_email_index\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/priv/repo/seeds.exs",
    "content": ""
  },
  {
    "path": "apps/boruta_identity/priv/templates/choose_session/index.mustache",
    "content": "<div class=\"choose-session\">\n  <h1 class=\"ui right aligned header\">Continue ?</h1>\n  <div class=\"ui segment\">\n    <div class=\"choose-session\">\n      <a href=\"{{ delete_user_session_path }}\" class=\"ui large fluid button\">New login</a>\n      <span class=\"ui horizontal divider\">or</span>\n      <a href=\"{{ new_user_session_path }}\" class=\"ui large fluid basic button\">Continue as {{ current_user.username }}</a>\n    </div>\n  </div>\n  <a href=\"{{ delete_user_path }}\">Log out</a>{{#user_editable?}} | <a href=\"{{ edit_user_path }}\">Edit your information</a>{{/user_editable?}}\n</div>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/confirmations/new.mustache",
    "content": "<h1>Resend confirmation instructions</h1>\n\n{{^valid?}}\n<div class=\"ui error message\">\n{{#errors}}\n  <li>{{ message }}</li>\n{{/errors}}\n</div>\n{{/valid?}}\n\n<form class=\"ui form\" action=\"{{ create_user_confirmation_path }}\" method=\"POST\">\n  <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ _csrf_token }}\" />\n\n  <div class=\"field\">\n    <label for=\"user_email\">Email</label>\n    <input type=\"email\" id=\"user_email\" name=\"user[email]\" required />\n  </div>\n\n  <button class=\"ui fluid button\" type=\"submit\">Ressend confirmation instructions</button>\n\n</form>\n\n<p>\n  <a href=\"{{ new_user_session_path }}\">Log in</a> | {{#registrable?}}<a href=\"{{ new_user_registration_path }}\">Register</a> | {{/registrable?}}<a href=\"{{ new_user_reset_password_path }}\">Forgot your password?</a>\n</p>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/consents/new.mustache",
    "content": "<div class=\"ui segment\" style=\"max-width: 400px;\">\n  <h1 style=\"padding: 0 30px 30px 30px;\" class=\"ui center aligned icon header\">\n    <i class=\"circular address card icon\"></i>\n    <strong>{{ client.name }}</strong> require access to your data\n  </h1>\n  <div class=\"ui large form\">\n    <form action=\"{{ create_user_consent_path }}\" method=\"POST\">\n      <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ _csrf_token }}\" />\n      {{#scopes}}\n      <div class=\"field\">\n        <div class=\"ui checkbox\">\n          <input type=\"checkbox\" name=\"scopes[]\" id=\"{{ name }}\" value=\"{{ name }}\" checked=\"checked\">\n          <label class=\"content\" for=\"{{ name }}\">{{ label }}</label>\n        </div>\n      </div>\n      {{/scopes}}\n      <hr />\n      <button type=\"submit\" class=\"ui fluid button\">Consent</button>\n    </form>\n  </div>\n</div>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/emails/confirmation_instructions.html.mustache",
    "content": "<p>Hi {{ user.username }},</p>\n\n<p>You can confirm your account by visiting the link below:</p>\n\n<p><a href=\"{{ url }}\">Confirm your account</a></p>\n\n<hr />\n\n<p>If you didn't create an account with us, please ignore this.</p>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/emails/confirmation_instructions.txt.mustache",
    "content": "\n==============================\n\nHi {{ user.username }},\n\nYou can confirm your account by visiting the URL below:\n\n{{ url }}\n\nIf you didn't create an account with us, please ignore this.\n\n==============================\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/emails/reset_password_instructions.html.mustache",
    "content": "<p>Hi {{ user.username }},</p>\n\n<p>You can reset your password by visiting the link below:</p>\n\n<p><a href=\"{{ url }}\">Reset your password</a></p>\n\n<hr />\n\n<p>If you didn't request this change, please ignore this.</p>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/emails/reset_password_instructions.txt.mustache",
    "content": "\n==============================\n\nHi {{ user.username }},\n\nYou can reset your password by visiting the URL below:\n\n{{ url }}\n\nIf you didn't request this change, please ignore this.\n\n==============================\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/emails/tx_code.html.mustache",
    "content": "<p>Hi {{ user.username }},</p>\n\n<p>Here is your transaction code to fill in in order to obtain your verifiable credential:</p>\n\n<p><strong>{{ tx_code }}</strong></p>\n\n<hr />\n\n<p>If you didn't requested a verifiable credential, please ignore this.</p>\n\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/emails/tx_code.txt.mustache",
    "content": "\n==============================\n\nHi {{ user.username }},\n\nHere is your transaction code to fill in in order to obtain your verifiable credential:\n\n{{ tx_code }}\n\nIf you didn't requested a verifiable credential, please ignore this.\n\n==============================\n\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/errors/400.mustache",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Authorization server</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n    <style>\n    .credits {\n      position: absolute;\n      bottom: 1rem;\n      right: 1rem;\n      display: flex;\n    }\n\n    .credits img {\n      height: 1.5em;\n      margin-left: .5em;\n    }\n    </style>\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <h1 class=\"ui icon header\">\n        <i class=\"redo alternate icon\"></i>\n        <div class=\"content\">\n          Request could not be processed\n          <div class=\"sub header\">The given request could not be processed. Please retry with valid parameters.</div>\n        </div>\n      </h1>\n    </div>\n    <div class=\"ui error message\" style=\"position: absolute; top: 1em; right: 1em; left: 1em; text-align: center\">\n      {{ reason.message }}\n    </div>\n    <div class=\"credits\">\n      Powered by <img src=\"{{ boruta_logo_path }}\" />\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/errors/401.mustache",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Authorization server</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n    <style>\n    .credits {\n      position: absolute;\n      bottom: 1rem;\n      right: 1rem;\n      display: flex;\n    }\n\n    .credits img {\n      height: 1.5em;\n      margin-left: .5em;\n    }\n    </style>\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <h1 class=\"ui icon header\">\n        <i class=\"ban icon\"></i>\n        <div class=\"content\">\n          Unauthorized\n          <div class=\"sub header\">You are not authorized to access this resource.</div>\n        </div>\n      </h1>\n    </div>\n    <div class=\"ui error message\" style=\"position: absolute; top: 1em; right: 1em; left: 1em; text-align: center\">\n      {{ reason.message }}\n    </div>\n    <div class=\"credits\">\n      Powered by <img src=\"{{ boruta_logo_path }}\" />\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/errors/403.mustache",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Authorization server</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n    <style>\n    .credits {\n      position: absolute;\n      bottom: 1rem;\n      right: 1rem;\n      display: flex;\n    }\n\n    .credits img {\n      height: 1.5em;\n      margin-left: .5em;\n    }\n    </style>\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <h1 class=\"ui icon header\">\n        <i class=\"ban icon\"></i>\n        <div class=\"content\">\n          Forbidden\n          <div class=\"sub header\">You are forbidden to access this resource.</div>\n        </div>\n      </h1>\n    </div>\n    <div class=\"ui error message\" style=\"position: absolute; top: 1em; right: 1em; left: 1em; text-align: center\">\n      {{ reason.message }}\n    </div>\n    <div class=\"credits\">\n      Powered by <img src=\"{{ boruta_logo_path }}\" />\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/errors/404.mustache",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Authorization server</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n    <style>\n    .credits {\n      position: absolute;\n      bottom: 1rem;\n      right: 1rem;\n      display: flex;\n    }\n\n    .credits img {\n      height: 1.5em;\n      margin-left: .5em;\n    }\n    </style>\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <h1 class=\"ui icon header\">\n        <i class=\"question icon\"></i>\n        <div class=\"content\">\n          Page not found\n          <div class=\"sub header\">The page you requested was not found. Please contact your administrator.</div>\n        </div>\n      </h1>\n    </div>\n    <div class=\"credits\">\n      Powered by <img src=\"{{ boruta_logo_path }}\" />\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/errors/500.mustache",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Authorization server</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n    <style>\n    .credits {\n      position: absolute;\n      bottom: 1rem;\n      right: 1rem;\n      display: flex;\n    }\n\n    .credits img {\n      height: 1.5em;\n      margin-left: .5em;\n    }\n    </style>\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <h1 class=\"ui icon header\">\n        <i class=\"exclamation icon\"></i>\n        <div class=\"content\">\n          Internal server error\n          <div class=\"sub header\">An unexpected error occurred. Please contact your administrator.</div>\n        </div>\n      </h1>\n    </div>\n    <div class=\"credits\">\n      Powered by <img src=\"{{ boruta_logo_path }}\" />\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/layouts/app.mustache",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n    <link href=\"https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@500&display=swap\" rel=\"stylesheet\">\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha512-8bHTC73gkZ7rZ7vpqUQThUDhqcNFyYi2xgDgPDHc+GXVGHXq+xPjynxIopALmOPqzo9JZj0k6OqqewdGO3EsrQ==\" crossorigin=\"anonymous\" />\n    <title>Boruta · Identity provider</title>\n    <style>\n    html, body {\n      height: 100%;\n      margin: 0;\n    }\n\n    .wrapper {\n      height: 100vh;\n      display: flex;\n    }\n\n    .main.messages {\n      position: absolute;\n      top: 1rem;\n      left: 1rem;\n      right: 1rem;\n    }\n\n    header {\n      height: 100%;\n      width: 50%;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      background-size: cover;\n      background-position: center;\n      font-family: 'Source Code Pro';\n      font-size: 2.7em;\n      line-height: 1.2em;\n      border-right: 1px solid #eee;\n    }\n\n    header>div {\n      border: 5px solid black;\n      padding: .7em 1em;\n    }\n\n    main {\n      flex: 1;\n      position: relative;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: 1rem;\n    }\n\n    main>.content {\n      flex: 1;\n      max-width: 500px;\n      padding-bottom: 3.5rem;\n    }\n\n    .credits {\n      position: absolute;\n      font-size: .8em;\n      bottom: .5rem;\n      right: 1rem;\n      display: flex;\n    }\n\n    .credits img {\n      height: 1.5em;\n      margin-left: .5em;\n    }\n\n    @media (max-width: 768px) {\n      header {\n        display: none;\n      }\n      html, body {\n        height: auto;\n      }\n    }\n    </style>\n  </head>\n  <body>\n    <div class=\"wrapper\">\n      <header>\n        <div>\n          <div><strong>N</strong>o</div>\n          <div><strong>L</strong>earning</div>\n          <div><strong>P</strong>aradox</div>\n        </div>\n      </header>\n      <main role=\"main\" class=\"container\">\n        <div class=\"content\">\n          <div class=\"main messages\">\n            {{#messages}}\n            <div class=\"ui {{ type }} message\">\n              {{ content }}\n            </div>\n            {{/messages}}\n          </div>\n          {{> inner_content }}\n        </div>\n        <div class=\"credits\">\n          Powered by <img src=\"{{ boruta_logo_path }}\" />\n        </div>\n      </main>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/mfa/totp/authentication.mustache",
    "content": "<h1>TOTP authentication</h1>\n\n{{^valid?}}\n<div class=\"ui error message\">\n{{#errors}}\n  <li>{{ message }}</li>\n{{/errors}}\n</div>\n{{/valid?}}\n\n<div class=\"ui info message\">\n  Provide the TOTP code from your authenticator\n</div>\n\n<form class=\"ui massive form\" action=\"{{ create_user_session_totp_authentication_path }}\" method=\"POST\">\n  <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ _csrf_token }}\" />\n  <div class=\"field\">\n    <label for=\"totp-code\">TOTP code <em>({{ current_user.username }})</em></label>\n    <hr />\n    <input autofocus type=\"text\" id=\"totp-code\" name=\"totp[totp_code]\" placeholder=\"XXXXXX\" style=\"text-align: center;\" />\n  </div>\n\n  <button class=\"ui large fluid button\" type=\"submit\">Authenticate</button>\n</form>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/mfa/totp/registration.mustache",
    "content": "{{#user_editable?}}<a href=\"{{ edit_user_path }}\">Edit your information</a>{{/user_editable?}}\n\n<h1>Add TOTP authentication from an authenticator</h1>\n\n{{^valid?}}\n<div class=\"ui error message\">\n{{#errors}}\n  <li>{{ message }}</li>\n{{/errors}}\n</div>\n{{/valid?}}\n\n<h2>1. Scan the QR code with your authenticator</h2>\n<hr />\n<div class=\"ui center aligned segment\">\n  <img width=\"150\" src=\"data:image/svg+xml; base64, {{ base64_totp_registration_qr_code }}\" alt=\"QR code\" />\n</div>\n\n<form class=\"ui massive form\" action=\"{{ create_user_totp_registration_path }}\" method=\"POST\">\n  <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ _csrf_token }}\" />\n  <input type=\"hidden\" name=\"totp[totp_secret]\" value=\"{{ totp_secret }}\" />\n  <div class=\"field\">\n    <label for=\"totp-code\">2. Insert the TOTP code</label>\n    <hr />\n    <input autofocus type=\"text\" id=\"totp-code\" name=\"totp[totp_code]\" placeholder=\"XXXXXX\" style=\"text-align: center;\" />\n  </div>\n\n  <button class=\"ui large fluid button\" type=\"submit\">Add TOTP authenticator</button>\n</form>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/mfa/webauthn/authentication.mustache",
    "content": "<h1>Authenticate with a passkey</h1>\n\n{{^valid?}}\n<div class=\"ui error message\">\n{{#errors}}\n  <li>{{ message }}</li>\n{{/errors}}\n</div>\n{{/valid?}}\n\n<form id=\"authenticate\" action=\"{{ create_user_session_webauthn_authentication_path }}\" method=\"post\">\n  <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ _csrf_token }}\" />\n  <input type=\"hidden\" id=\"identifier\" name=\"identifier\" />\n  <input type=\"hidden\" id=\"type\" name=\"type\" />\n  <input type=\"hidden\" id=\"signature\" name=\"signature\" />\n  <input type=\"hidden\" id=\"clientData\" name=\"client_data\" />\n  <input type=\"hidden\" id=\"authenticatorData\" name=\"authenticator_data\" />\n</form>\n<span class=\"ui horizontal divider\">or</span>\n<a href=\"{{ new_user_webauthn_registration_path }}\">Register a passkey</a>\n\n<script>\n  function _arrayBufferToString( buffer ) {\n      var binary = ''\n      var bytes = new Uint8Array( buffer )\n      var len = bytes.byteLength\n      for (var i = 0; i < len; i++) {\n          binary += String.fromCharCode( bytes[ i ] )\n        }\n      return binary\n    }\n\n  const publicKeyCredentialGetOptions = {\n      challenge: Uint8Array.from(\"{{webauthn_options.challenge}}\", c => c.charCodeAt(0)),\n      allowCredentials: [{\n          id: Uint8Array.from(atob(\"{{webauthn_options.credential_id}}\"), c => c.charCodeAt(0)),\n          type: \"public-key\"\n        }],\n      timeout: 60000\n    }\n\n  const credential = navigator.credentials.get({\n      publicKey: publicKeyCredentialGetOptions\n    }).then(credential => {\n        const utf8Decoder = new TextDecoder('utf-8')\n        const decodedClientData = utf8Decoder.decode(credential.response.clientDataJSON)\n        console.log(credential.response.userHandle)\n\n        document.getElementById('identifier').value = btoa(_arrayBufferToString(credential.rawId))\n        document.getElementById('type').value = credential.type;\n        document.getElementById('clientData').value = decodedClientData\n        document.getElementById('authenticatorData').value =\n          btoa(_arrayBufferToString(credential.response.authenticatorData))\n        document.getElementById('signature').value =\n          btoa(_arrayBufferToString(credential.response.signature))\n        document.getElementById('authenticate').submit()\n      })\n</script>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/mfa/webauthn/registration.mustache",
    "content": "{{#user_editable?}}<a href=\"{{ edit_user_path }}\">Edit your information</a>{{/user_editable?}}\n\n<h1>Register a passkey</h1>\n\n{{^valid?}}\n<div class=\"ui error message\">\n{{#errors}}\n  <li>{{ message }}</li>\n{{/errors}}\n</div>\n{{/valid?}}\n\n<form id=\"register\" action=\"{{ create_user_webauthn_registration_path }}\" method=\"post\">\n  <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ _csrf_token }}\" />\n  <input type=\"hidden\" id=\"identifier\" name=\"identifier\" />\n  <input type=\"hidden\" id=\"type\" name=\"type\" />\n  <input type=\"hidden\" id=\"clientData\" name=\"client_data\" />\n  <input type=\"hidden\" id=\"attestation\" name=\"attestation\" />\n</form>\n\n<script>\n  function _arrayBufferToString( buffer ) {\n      var binary = ''\n      var bytes = new Uint8Array( buffer )\n      var len = bytes.byteLength\n      for (var i = 0; i < len; i++) {\n          binary += String.fromCharCode( bytes[ i ] )\n        }\n      return binary\n    }\n\n  const publicKeyCredentialCreationOptions = {\n      challenge: Uint8Array.from(\n          \"{{webauthn_options.challenge}}\", c => c.charCodeAt(0)),\n      rp: {\n          id: \"{{webauthn_options.rp.id}}\",\n          name: \"{{webauthn_options.rp.name}}\"\n        },\n      user: {\n          id: Uint8Array.from(\n              \"{{webauthn_options.user.id}}\", c => c.charCodeAt(0)),\n          name: \"{{webauthn_options.user.name}}\",\n          displayName: \"{{webauthn_options.user.displayName}}\"\n        },\n      pubKeyCredParams: [{alg:{{webauthn_options.publicKeyCredParams.alg}}, type: \"{{webauthn_options.publicKeyCredParams.type}}\"}],\n      authenticatorSelection: {\n          authenticatorAttachment: \"cross-platform\"\n        },\n      timeout: 60000,\n      attestation: \"direct\"\n    }\n\n  const credential = navigator.credentials.create({\n      publicKey: publicKeyCredentialCreationOptions\n    }).then(credential => {\n        const utf8Decoder = new TextDecoder('utf-8')\n        const decodedClientData = utf8Decoder.decode(credential.response.clientDataJSON)\n\n        document.getElementById('identifier').value = btoa(_arrayBufferToString(credential.rawId))\n        document.getElementById('type').value = credential.type;\n        document.getElementById('clientData').value = decodedClientData\n        document.getElementById('attestation').value =\n          btoa(_arrayBufferToString(credential.response.attestationObject))\n\n        document.getElementById('register').submit()\n      })\n</script>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/registrations/new.mustache",
    "content": "<h1>Register</h1>\n\n{{^valid?}}\n<div class=\"ui error message\">\n{{#errors}}\n  <li>{{ message }}</li>\n{{/errors}}\n</div>\n{{/valid?}}\n\n<form class=\"ui form\" action=\"{{ create_user_registration_path }}\" method=\"POST\">\n  <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ _csrf_token }}\" />\n\n  <div class=\"field\">\n    <label for=\"user_email\">Email</label>\n    <input type=\"email\" id=\"user_email\" name=\"user[email]\" />\n  </div>\n\n  <div class=\"field\">\n    <label for=\"user_password\">Password</label>\n    <input type=\"password\" id=\"user_password\" name=\"user[password]\" />\n  </div>\n\n  <button class=\"ui fluid button\" type=\"submit\">Register</button>\n</form>\n\n<p>\n  <a href=\"{{ new_user_session_path }}\">Log in</a> | <a href=\"{{ new_user_reset_password_path }}\">Forgot your password?</a>\n</p>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/reset_passwords/edit.mustache",
    "content": "<h1>Reset password</h1>\n\n{{^valid?}}\n<div class=\"ui error message\">\n{{#errors}}\n  <li>{{ message }}</li>\n{{/errors}}\n</div>\n{{/valid?}}\n\n<form class=\"ui form\" action=\"{{ update_user_reset_password_path }}\" method=\"POST\">\n  <input type=\"hidden\" name=\"_method\" value=\"put\" />\n  <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ _csrf_token }}\" />\n\n  <div class=\"field\">\n    <label for=\"user_password\">Password</label>\n    <input type=\"password\" id=\"user_password\" name=\"user[password]\" required />\n  </div>\n\n  <div class=\"field\">\n    <label for=\"user_password_confirmation\">Password confirmation</label>\n    <input type=\"password\" id=\"user_password_confirmation\" name=\"user[password_confirmation]\" required />\n  </div>\n\n  <button class=\"ui fluid button\" type=\"submit\">Reset password</button>\n</form>\n\n<p>\n  {{#registrable?}}<a href=\"{{ new_user_registration_path }}\">Register</a> | {{/registrable?}}<a href=\"{{ new_user_session_path }}\">Log in</a>\n</p>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/reset_passwords/new.mustache",
    "content": "<h1>Forgot your password?</h1>\n\n<form class=\"ui form\" action=\"{{ create_user_reset_password_path }}\" method=\"POST\">\n  <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ _csrf_token }}\" />\n\n  <div class=\"field\">\n    <label for=\"user_email\">Email</label>\n    <input type=\"email\" id=\"user_email\" name=\"user[email]\" required />\n  </div>\n\n  <button class=\"ui fluid button\" type=\"submit\">Send instructions to reset password</button>\n\n</form>\n\n<p>\n  {{#registrable?}}<a href=\"{{ new_user_registration_path }}\">Register</a> | {{/registrable?}}<a href=\"{{ new_user_session_path }}\">Log in</a>\n</p>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/sessions/new.mustache",
    "content": "<h1>Log in</h1>\n\n{{^valid?}}\n<div class=\"ui error message\">\n{{#errors}}\n  <li>{{ message }}</li>\n{{/errors}}\n</div>\n{{/valid?}}\n\n<form class=\"ui form\" action=\"{{ create_user_session_path }}\" method=\"POST\">\n  <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ _csrf_token }}\" />\n\n  <div class=\"field\">\n    <label for=\"user_email\">Email</label>\n    <input type=\"email\" id=\"user_email\" name=\"user[email]\" required />\n  </div>\n\n  <div class=\"field\">\n    <label for=\"user_password\">Password</label>\n    <input type=\"password\" id=\"user_password\" name=\"user[password]\" />\n  </div>\n\n  <div class=\"ui checkbox field\">\n    <input type=\"checkbox\" id=\"user_remember_me\" name=\"user[remember_me]\" />\n    <label for=\"user_remember_me\">Remember me</label>\n  </div>\n\n  <button class=\"ui large fluid button\" type=\"submit\">Log in</button>\n\n</form>\n\n<p>\n  {{#registrable?}}<a href=\"{{ new_user_registration_path }}\">Register</a> | {{/registrable?}}<a href=\"{{ new_user_reset_password_path }}\">Forgot your password?</a>\n</p>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/settings/credential_offer.mustache",
    "content": "<div id=\"credential-offer\">\n  <h1>Credential offer</h1>\n\n  {{^valid?}}\n  <div class=\"ui error message\">\n  {{#errors}}\n    <li>{{ message }}</li>\n  {{/errors}}\n  </div>\n  {{/valid?}}\n\n  <h2>Scan the QR code with your identity wallet</h2>\n  <hr />\n  <div class=\"ui center aligned qr-code segment\">\n    <img width=\"350\" src=\"data:image/svg+xml; base64, {{ base64_credential_offer_qr_code }}\" alt=\"QR code\" />\n  </div>\n  <div class=\"ui center aligned deeplink segment\">\n    <a class=\"ui blue fluid button\" onclick=\"window.open('{{ credential_offer_deeplink }}', 'present your credentials', 'scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,width=390,height=844,left=auto,top=auto')\">Get your verifiable credential</a>\n  </div>\n  <hr />\n  <a href=\"#\" onclick=\"window.location.reload()\" class=\"ui fluid button\">Reload</a>\n</div>\n\n<div id=\"message\" class=\"ui info message\" style=\"display: none;\">\n</div>\n\n<script>\n  var source = new EventSource('{{ issuer }}/openid/presentation_sse?code={{ code }}');\n  source.addEventListener('message', function(event) {\n      document.getElementById(\"credential-offer\").style.display = 'none'\n      document.getElementById(\"message\").style.display = 'block'\n      document.getElementById(\"message\").innerHTML = event.data\n    })\n  source.addEventListener(\"error\", console.log);\n</script>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/settings/edit_user.mustache",
    "content": "<a href=\"{{ choose_session_path }}\">Back</a>\n<h1>Edit user</h1>\n\n{{^valid?}}\n<div class=\"ui error message\">\n{{#errors}}\n  <li>{{ message }}</li>\n{{/errors}}\n</div>\n{{/valid?}}\n\n{{#current_user.totp_registered_at}}\n<div class=\"ui big right aligned segment\">\n  TOTP registered at {{ . }}\n</div>\n{{/current_user.totp_registered_at}}\n\n{{#current_user.webauthn_registered_at}}\n<div class=\"ui big right aligned segment\">\n  Passkey registered at {{ . }}\n</div>\n{{/current_user.webauthn_registered_at}}\n\n<a href=\"{{ new_user_totp_registration_path }}\" class=\"ui fluid blue button\">Register a TOTP authenticator</a>\n<hr />\n<a href=\"{{ new_user_webauthn_registration_path }}\" class=\"ui fluid blue button\">Register a Passkey</a>\n<hr />\n\n<form class=\"ui form\" action=\"{{ update_user_path }}\" method=\"POST\">\n  <input type=\"hidden\" name=\"_method\" value=\"put\" />\n  <input type=\"hidden\" name=\"_csrf_token\" value=\"{{ _csrf_token }}\" />\n\n  <div class=\"field\">\n    <label for=\"user_email\">Email</label>\n    <input type=\"email\" id=\"user_email\" name=\"user[email]\" value=\"{{ current_user.username }}\" />\n  </div>\n\n  <div class=\"field\">\n    <label for=\"current_user_password\">Current password</label>\n    <input type=\"password\" id=\"current_user_password\" name=\"user[current_password]\" />\n  </div>\n\n  <div class=\"field\">\n    <label for=\"new_user_password\">New password</label>\n    <input type=\"password\" id=\"new_user_password\" name=\"user[password]\" placeholder=\"leave blank to keep current password\" />\n  </div>\n\n  <button class=\"ui fluid button\" type=\"submit\">update</button>\n</form>\n"
  },
  {
    "path": "apps/boruta_identity/priv/templates/settings/verifiable_presentation.mustache",
    "content": "<div id=\"presentation\">\n  <h1>Credential presentation</h1>\n\n  {{^valid?}}\n  <div class=\"ui error message\">\n  {{#errors}}\n    <li>{{ message }}</li>\n  {{/errors}}\n  </div>\n  {{/valid?}}\n\n  <h2>Scan the QR code with your identity wallet</h2>\n  <hr />\n  <div class=\"ui center aligned qr-code segment\">\n    <img width=\"350\" src=\"data:image/svg+xml; base64, {{ base64_presentation_qr_code }}\" alt=\"QR code\" />\n  </div>\n  <div class=\"ui center aligned deeplink segment\">\n    <a class=\"ui blue fluid button\" onclick=\"window.open('{{ presentation_deeplink }}', 'present your credentials', 'scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,width=390,height=844,left=auto,top=auto')\">Present your credentials</a>\n  </div>\n  <hr />\n  <a href=\"#\" onclick=\"window.location.reload()\" class=\"ui fluid button\">Reload</a>\n</div>\n\n<div id=\"message\" class=\"ui info message\" style=\"display: none;\">\n</div>\n\n<script>\n  var source = new EventSource('{{ issuer }}/openid/presentation_sse?code={{ code }}');\n  source.addEventListener('authenticated', function(event) {\n      console.log(event)\n      window.location.href = event.data\n    })\n  source.addEventListener('message', function(event) {\n      document.getElementById(\"presentation\").style.display = 'none'\n      document.getElementById(\"message\").style.display = 'block'\n      document.getElementById(\"message\").innerHTML = event.data\n    })\n  source.addEventListener(\"error\", console.log);\n</script>\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity/accounts/deliveries_test.exs",
    "content": "defmodule BorutaIdentity.Accounts.DeliveriesTest do\n  use BorutaIdentity.DataCase\n\n  import BorutaIdentity.AccountsFixtures\n  import BorutaIdentity.Factory\n\n  alias BorutaIdentity.Accounts.Deliveries\n\n  describe \"deliver_user_reset_password_instructions/2\" do\n    setup do\n      {:ok, user: user_fixture(%{backend: insert(:smtp_backend)})}\n    end\n\n    test \"sends token through notification\", %{user: user} do\n      reset_password_url_fun = fn _ -> \"http://test.host\" end\n\n      assert {:ok, _email} =\n               Deliveries.deliver_user_reset_password_instructions(\n                 user.backend,\n                 user,\n                 reset_password_url_fun\n               )\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity/accounts_test.exs",
    "content": "defmodule BorutaIdentity.AccountsTest do\n  use BorutaIdentity.DataCase\n\n  import BorutaIdentity.AccountsFixtures\n  import BorutaIdentity.Factory\n  import Mox\n\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.IdentityProviderError\n  alias BorutaIdentity.Accounts.Internal\n  alias BorutaIdentity.Accounts.RegistrationError\n  alias BorutaIdentity.Accounts.ResetPasswordError\n  alias BorutaIdentity.Accounts.SessionError\n  alias BorutaIdentity.Accounts.SettingsError\n  alias BorutaIdentity.Accounts.{User, UserToken}\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.IdentityProviders.ClientIdentityProvider\n  alias BorutaIdentity.IdentityProviders.Template\n  alias BorutaIdentity.Repo\n\n  setup :set_mox_from_context\n\n  defmodule DummyRegistration do\n    @behaviour Accounts.RegistrationApplication\n\n    @impl Accounts.RegistrationApplication\n    def registration_initialized(context, template) do\n      {:registration_initialized, context, template}\n    end\n\n    @impl Accounts.RegistrationApplication\n    def user_registered(context, user, session_token) do\n      {:user_registered, context, user, session_token}\n    end\n\n    @impl Accounts.RegistrationApplication\n    def registration_failure(context, error) do\n      {:registration_failure, context, error}\n    end\n  end\n\n  defmodule DummySession do\n    @behaviour Accounts.SessionApplication\n\n    @impl Accounts.SessionApplication\n    def session_initialized(context, template) do\n      {:session_initialized, context, template}\n    end\n\n    @impl Accounts.SessionApplication\n    def user_authenticated(context, user, session_token) do\n      {:user_authenticated, context, user, session_token}\n    end\n\n    @impl Accounts.SessionApplication\n    def authentication_failure(context, error) do\n      {:authentication_failure, context, error}\n    end\n\n    @impl Accounts.SessionApplication\n    def session_deleted(context) do\n      {:session_deleted, context}\n    end\n  end\n\n  defmodule DummyConfirmation do\n    @behaviour Accounts.ConfirmationApplication\n\n    @impl Accounts.ConfirmationApplication\n    def confirmation_instructions_initialized(context, template) do\n      {:confirmation_instructions_initialized, context, template}\n    end\n\n    @impl Accounts.ConfirmationApplication\n    def confirmation_instructions_delivered(context) do\n      {:confirmation_instructions_delivered, context}\n    end\n\n    @impl Accounts.ConfirmationApplication\n    def user_confirmed(context, user) do\n      {:user_confirmed, context, user}\n    end\n\n    @impl Accounts.ConfirmationApplication\n    def user_confirmation_failure(context, error) do\n      {:user_confirmation_failure, context, error}\n    end\n  end\n\n  defmodule DummySettings do\n    @behaviour Accounts.SettingsApplication\n\n    @impl Accounts.SettingsApplication\n    def edit_user_initialized(context, user, template) do\n      {:edit_user_initialized, context, user, template}\n    end\n\n    @impl Accounts.SettingsApplication\n    def user_updated(context, user) do\n      {:user_updated, context, user}\n    end\n\n    @impl Accounts.SettingsApplication\n    def user_destroy_failure(context, error) do\n      {:user_destroy_failure, context, error}\n    end\n\n    @impl Accounts.SettingsApplication\n    def user_destroyed(context, user) do\n      {:user_destroyed, context, user}\n    end\n\n    @impl Accounts.SettingsApplication\n    def user_update_failure(context, error) do\n      {:user_update_failure, context, error}\n    end\n  end\n\n  defmodule DummyResetPasswords do\n    @behaviour Accounts.ResetPasswordApplication\n\n    @impl Accounts.ResetPasswordApplication\n    def password_instructions_initialized(context, template) do\n      {:password_instructions_initialized, context, template}\n    end\n\n    @impl Accounts.ResetPasswordApplication\n    def reset_password_instructions_delivered(context) do\n      {:reset_password_instructions_delivered, context}\n    end\n\n    @impl Accounts.ResetPasswordApplication\n    def password_reset_initialized(context, token, template) do\n      {:passsword_reseet_initialized, context, token, template}\n    end\n\n    @impl Accounts.ResetPasswordApplication\n    def password_reseted(context, user) do\n      {:password_reseted, context, user}\n    end\n\n    @impl Accounts.ResetPasswordApplication\n    def password_reset_failure(context, error) do\n      {:password_reset_failure, context, error}\n    end\n  end\n\n  describe \"Utils.client_identity_provider/1\" do\n    test \"returns an error when client_id is nil\" do\n      client_id = nil\n\n      assert Accounts.Utils.client_identity_provider(client_id) ==\n               {:error, \"Client identifier not provided.\"}\n    end\n\n    test \"returns an error when client_id is unknown\" do\n      client_id = SecureRandom.uuid()\n\n      assert Accounts.Utils.client_identity_provider(client_id) ==\n               {:error,\n                \"identity provider not configured for given OAuth client. \" <>\n                  \"Please contact your administrator.\"}\n    end\n\n    test \"returns client identity_provider\" do\n      identity_provider = BorutaIdentity.Factory.insert(:identity_provider)\n\n      %ClientIdentityProvider{client_id: client_id} =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider: identity_provider\n        )\n\n      identity_provider = Repo.preload(identity_provider, backend: :email_templates)\n\n      assert Accounts.Utils.client_identity_provider(client_id) == {:ok, identity_provider}\n    end\n  end\n\n  describe \"initialize_registration/3\" do\n    setup do\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider:\n            build(\n              :identity_provider,\n              registrable: true\n            )\n        )\n\n      {:ok, client_id: client_identity_provider.client_id}\n    end\n\n    test \"returns an error with nil client_id\" do\n      client_id = nil\n      context = :context\n\n      assert_raise IdentityProviderError, \"Client identifier not provided.\", fn ->\n        Accounts.initialize_registration(context, client_id, DummyRegistration)\n      end\n    end\n\n    test \"returns an error with unknown client_id\" do\n      client_id = SecureRandom.uuid()\n      context = :context\n\n      assert_raise IdentityProviderError,\n                   \"identity provider not configured for given OAuth client. Please contact your administrator.\",\n                   fn ->\n                     Accounts.initialize_registration(context, client_id, DummyRegistration)\n                   end\n    end\n\n    test \"returns an error if registration is not enabled for client identity provider\" do\n      %ClientIdentityProvider{client_id: client_id} = insert(:client_identity_provider)\n\n      context = :context\n\n      assert_raise IdentityProviderError,\n                   \"Feature is not enabled for client identity provider.\",\n                   fn ->\n                     Accounts.initialize_registration(context, client_id, DummyRegistration)\n                   end\n    end\n\n    test \"returns a template\", %{client_id: client_id} do\n      context = :context\n\n      assert {:registration_initialized, ^context, %Template{}} =\n               Accounts.initialize_registration(context, client_id, DummyRegistration)\n    end\n  end\n\n  describe \"register/3\" do\n    setup do\n      identity_provider = insert(:identity_provider, registrable: true)\n\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider: identity_provider\n        )\n\n      {:ok, client_id: client_identity_provider.client_id, backend: identity_provider.backend}\n    end\n\n    test \"returns an error with nil client_id\" do\n      context = :context\n      client_id = nil\n      user_params = %{}\n      confirmation_callback_fun = & &1\n\n      assert_raise IdentityProviderError, \"Client identifier not provided.\", fn ->\n        Accounts.register(\n          context,\n          client_id,\n          user_params,\n          confirmation_callback_fun,\n          DummyRegistration\n        )\n      end\n    end\n\n    test \"returns an error with unknown client_id\" do\n      context = :context\n      client_id = SecureRandom.uuid()\n      user_params = %{}\n      confirmation_callback_fun = & &1\n\n      assert_raise IdentityProviderError,\n                   \"identity provider not configured for given OAuth client. Please contact your administrator.\",\n                   fn ->\n                     Accounts.register(\n                       context,\n                       client_id,\n                       user_params,\n                       confirmation_callback_fun,\n                       DummyRegistration\n                     )\n                   end\n    end\n\n    test \"returns an error if registrations is disabled for client identity provider\" do\n      %ClientIdentityProvider{client_id: client_id} = insert(:client_identity_provider)\n      context = :context\n      user_params = %{}\n      confirmation_callback_fun = & &1\n\n      assert_raise IdentityProviderError,\n                   \"Feature is not enabled for client identity provider.\",\n                   fn ->\n                     Accounts.register(\n                       context,\n                       client_id,\n                       user_params,\n                       confirmation_callback_fun,\n                       DummyRegistration\n                     )\n                   end\n    end\n\n    test \"returns a template on error\", %{client_id: client_id} do\n      context = :context\n      user_params = %{}\n      confirmation_callback_fun = & &1\n\n      assert {:registration_failure, ^context, %RegistrationError{template: %Template{}}} =\n               Accounts.register(\n                 context,\n                 client_id,\n                 user_params,\n                 confirmation_callback_fun,\n                 DummyRegistration\n               )\n    end\n\n    test \"requires email and password to be set\", %{client_id: client_id} do\n      context = :context\n      user_params = %{}\n      confirmation_callback_fun = & &1\n\n      assert {:registration_failure, ^context, %RegistrationError{changeset: changeset}} =\n               Accounts.register(\n                 context,\n                 client_id,\n                 user_params,\n                 confirmation_callback_fun,\n                 DummyRegistration\n               )\n\n      assert %{\n               password: [\"can't be blank\"],\n               email: [\"can't be blank\"]\n             } = errors_on(changeset)\n    end\n\n    test \"validates email and password when given\", %{client_id: client_id} do\n      context = :context\n      user_params = %{email: \"not valid\", password: \"not valid\"}\n      confirmation_callback_fun = & &1\n\n      assert {:registration_failure, ^context, %RegistrationError{changeset: changeset}} =\n               Accounts.register(\n                 context,\n                 client_id,\n                 user_params,\n                 confirmation_callback_fun,\n                 DummyRegistration\n               )\n\n      assert %{\n               email: [\"must have the @ sign and no spaces\"],\n               password: [\"should be at least 12 character(s)\"]\n             } = errors_on(changeset)\n    end\n\n    test \"validates maximum values for email and password for security\", %{client_id: client_id} do\n      too_long = String.duplicate(\"too_long\", 100)\n      context = :context\n      user_params = %{email: too_long, password: too_long}\n      confirmation_callback_fun = & &1\n\n      assert {:registration_failure, ^context, %RegistrationError{changeset: changeset}} =\n               Accounts.register(\n                 context,\n                 client_id,\n                 user_params,\n                 confirmation_callback_fun,\n                 DummyRegistration\n               )\n\n      assert \"should be at most 160 character(s)\" in errors_on(changeset).email\n      assert \"should be at most 80 character(s)\" in errors_on(changeset).password\n    end\n\n    test \"validates email uniqueness\", %{client_id: client_id} do\n      email = \"test@test.test\"\n      context = :context\n      user_params = %{email: email, password: \"imaynotknowthat\"}\n      confirmation_callback_fun = & &1\n      Accounts.register(\n        context,\n        client_id,\n        user_params,\n        confirmation_callback_fun,\n        DummyRegistration\n      )\n\n      assert {:registration_failure, ^context, %RegistrationError{changeset: changeset}} =\n               Accounts.register(\n                 context,\n                 client_id,\n                 user_params,\n                 confirmation_callback_fun,\n                 DummyRegistration\n               )\n\n      assert \"has already been taken\" in errors_on(changeset).email\n\n      # Now try with the upper cased email too, to check that email case is ignored.\n      user_params = %{email: String.upcase(email), password: \"imaynotknowthat\"}\n      confirmation_callback_fun = & &1\n\n      assert {:registration_failure, ^context, %RegistrationError{changeset: changeset}} =\n               Accounts.register(\n                 context,\n                 client_id,\n                 user_params,\n                 confirmation_callback_fun,\n                 DummyRegistration\n               )\n\n      assert \"has already been taken\" in errors_on(changeset).email\n    end\n\n    test \"registers users with a hashed password\", %{client_id: client_id} do\n      email = unique_user_email()\n      context = :context\n      user_params = %{email: email, password: valid_user_password()}\n      confirmation_callback_fun = & &1\n\n      assert {:user_registered, ^context, user, session_token} =\n               Accounts.register(\n                 context,\n                 client_id,\n                 user_params,\n                 confirmation_callback_fun,\n                 DummyRegistration\n               )\n\n      assert session_token\n      assert user.username == email\n      assert is_nil(user.confirmed_at)\n      assert is_nil(user.password)\n    end\n\n    test \"registers users with metadata\", %{client_id: client_id, backend: backend} do\n      {:ok, _backend} =\n        Ecto.Changeset.change(backend, %{\n          metadata_fields: [\n            %{\"attribute_name\" => \"test\", \"user_editable\" => true},\n            %{\"attribute_name\" => \"restricted_field\", \"user_editable\" => false}\n          ]\n        })\n        |> Repo.update()\n\n      metadata = %{\"test\" => \"test value\"}\n      email = unique_user_email()\n      context = :context\n\n      user_params = %{\n        email: email,\n        password: valid_user_password(),\n        metadata: Map.put(metadata, \"restricted_field\", \"restricted\")\n      }\n\n      confirmation_callback_fun = & &1\n\n      assert {:user_registered, ^context, user, _session_token} =\n               Accounts.register(\n                 context,\n                 client_id,\n                 user_params,\n                 confirmation_callback_fun,\n                 DummyRegistration\n               )\n\n      assert user.metadata == %{\n               \"test\" => %{\"value\" => \"test value\", \"status\" => \"valid\", \"display\" => []}\n             }\n    end\n\n    test \"registers users with default organization\", %{client_id: client_id, backend: backend} do\n      {:ok, _backend} =\n        Ecto.Changeset.change(backend, %{create_default_organization: true}) |> Repo.update()\n\n      email = unique_user_email()\n      context = :context\n\n      user_params = %{\n        email: email,\n        password: valid_user_password()\n      }\n\n      confirmation_callback_fun = & &1\n\n      assert {:user_registered, ^context, %User{organizations: [organization], uid: uid},\n              _session_token} =\n               Accounts.register(\n                 context,\n                 client_id,\n                 user_params,\n                 confirmation_callback_fun,\n                 DummyRegistration\n               )\n\n      new_organization_name = \"default_#{uid}\"\n\n      assert %{organization: %{name: ^new_organization_name}} =\n               Repo.preload(organization, :organization)\n    end\n\n    @tag :skip\n    test \"delivers a confirmation mail when identity provider confirmable\"\n\n    @tag :skip\n    test \"does not deliver a confirmation mail when identity provider not confirmable\"\n  end\n\n  describe \"initialize_session/3\" do\n    setup do\n      client_identity_provider = BorutaIdentity.Factory.insert(:client_identity_provider)\n\n      {:ok, client_id: client_identity_provider.client_id}\n    end\n\n    test \"returns an error with nil client_id\" do\n      context = :context\n      client_id = nil\n\n      assert_raise IdentityProviderError, \"Client identifier not provided.\", fn ->\n        Accounts.initialize_session(\n          context,\n          client_id,\n          DummySession\n        )\n      end\n    end\n\n    test \"returns an error with unknown client_id\" do\n      context = :context\n      client_id = SecureRandom.uuid()\n\n      assert_raise IdentityProviderError,\n                   \"identity provider not configured for given OAuth client. Please contact your administrator.\",\n                   fn ->\n                     Accounts.initialize_session(\n                       context,\n                       client_id,\n                       DummySession\n                     )\n                   end\n    end\n\n    test \"returns identity provider and a template\", %{client_id: client_id} do\n      context = :context\n\n      assert {:session_initialized, ^context, %Template{type: \"new_session\"}} =\n               Accounts.initialize_session(\n                 context,\n                 client_id,\n                 DummySession\n               )\n    end\n  end\n\n  describe \"create_session/4 with an internal backend\" do\n    setup do\n      confirmable_client_identity_provider =\n        BorutaIdentity.Factory.insert(\n          :client_identity_provider,\n          identity_provider: insert(:identity_provider, confirmable: true)\n        )\n\n      no_password_client_identity_provider =\n        BorutaIdentity.Factory.insert(\n          :client_identity_provider,\n          identity_provider: insert(:identity_provider, check_password: false)\n        )\n\n      client_identity_provider = BorutaIdentity.Factory.insert(:client_identity_provider)\n\n      {:ok,\n       backend: client_identity_provider.identity_provider.backend,\n       client_id: client_identity_provider.client_id,\n       confirmable_backend: confirmable_client_identity_provider.identity_provider.backend,\n       confirmable_client_id: confirmable_client_identity_provider.client_id,\n       no_password_backend: no_password_client_identity_provider.identity_provider.backend,\n       no_password_client_id: no_password_client_identity_provider.client_id}\n    end\n\n    test \"returns an error with nil client_id\" do\n      context = :context\n      client_id = nil\n      authentication_params = %{}\n\n      assert_raise IdentityProviderError, \"Client identifier not provided.\", fn ->\n        Accounts.create_session(\n          context,\n          client_id,\n          authentication_params,\n          DummySession\n        )\n      end\n    end\n\n    test \"returns an error with unknown client_id\" do\n      context = :context\n      client_id = SecureRandom.uuid()\n      authentication_params = %{}\n\n      assert_raise IdentityProviderError,\n                   \"identity provider not configured for given OAuth client. Please contact your administrator.\",\n                   fn ->\n                     Accounts.create_session(\n                       context,\n                       client_id,\n                       authentication_params,\n                       DummySession\n                     )\n                   end\n    end\n\n    test \"returns an error with empty email\", %{client_id: client_id} do\n      context = :context\n      authentication_params = %{email: \"\"}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_session\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Invalid email or password.\"\n    end\n\n    test \"returns an error with a wrong email\", %{client_id: client_id} do\n      context = :context\n      authentication_params = %{email: \"does_not_exist\"}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_session\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Invalid email or password.\"\n    end\n\n    test \"returns an error without password\", %{client_id: client_id} do\n      %Internal.User{email: email} = insert(:internal_user)\n      context = :context\n      authentication_params = %{email: email}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_session\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Invalid email or password.\"\n    end\n\n    test \"returns an error with a wrong password\", %{client_id: client_id} do\n      %Internal.User{email: email} = insert(:internal_user)\n      context = :context\n      authentication_params = %{email: email, password: \"wrong password\"}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_session\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Invalid email or password.\"\n    end\n\n    test \"returns an error with a wrong password (confirmable)\", %{\n      confirmable_client_id: client_id\n    } do\n      %Internal.User{email: email} = insert(:internal_user)\n      context = :context\n      authentication_params = %{email: email, password: \"wrong password\"}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_session\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Invalid email or password.\"\n    end\n\n    test \"returns an error if not confirmed\", %{\n      confirmable_backend: backend,\n      confirmable_client_id: client_id\n    } do\n      %Internal.User{email: email} = insert(:internal_user, backend: backend)\n      context = :context\n      authentication_params = %{email: email, password: valid_user_password()}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_confirmation_instructions\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Email confirmation is required to authenticate.\"\n    end\n\n    test \"authenticates the user\", %{client_id: client_id, backend: backend} do\n      %Internal.User{id: uid, email: username} = insert(:internal_user, backend: backend)\n      context = :context\n      authentication_params = %{email: username, password: valid_user_password()}\n\n      assert {:user_authenticated, ^context,\n              %User{\n                username: ^username,\n                backend: ^backend,\n                uid: ^uid,\n                last_login_at: last_login_at\n              },\n              session_token} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert last_login_at\n      assert session_token\n    end\n\n    test \"authenticates the user with no password\", %{\n      no_password_client_id: client_id,\n      no_password_backend: backend\n    } do\n      context = :context\n      username = \"no_password@test.test\"\n      authentication_params = %{email: username}\n\n      assert {:user_authenticated, ^context,\n              %User{\n                username: ^username,\n                backend: ^backend,\n                last_login_at: last_login_at\n              },\n              session_token} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert last_login_at\n      assert session_token\n    end\n\n    test \"does not create multiple users accross multiple authentications\", %{\n      client_id: client_id,\n      backend: backend\n    } do\n      %Internal.User{id: uid, email: username} = insert(:internal_user, backend: backend)\n      context = :context\n      authentication_params = %{email: username, password: valid_user_password()}\n\n      assert {:user_authenticated, ^context,\n              %User{id: user_id, username: ^username, backend: ^backend, uid: ^uid},\n              session_token} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert session_token\n\n      assert {:user_authenticated, ^context,\n              %User{id: new_user_id, username: ^username, backend: ^backend, uid: ^uid},\n              session_token} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert session_token\n      assert user_id == new_user_id\n    end\n\n    @tag :skip\n    test \"returns a valid session token\"\n  end\n\n  describe \"create_session/4 with a ldap backend\" do\n    setup do\n      backend = insert(:ldap_backend)\n\n      confirmable_client_identity_provider =\n        BorutaIdentity.Factory.insert(\n          :client_identity_provider,\n          identity_provider: insert(:identity_provider, confirmable: true, backend: backend)\n        )\n\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(\n          :client_identity_provider,\n          identity_provider: insert(:identity_provider, backend: backend)\n        )\n\n      BorutaIdentity.LdapRepoMock\n      |> stub(:open, fn host, _opts ->\n        assert host == backend.ldap_host\n\n        {:ok, :ldap_pid}\n      end)\n      |> stub(:close, fn _handle ->\n        :ok\n      end)\n\n      {:ok,\n       backend: backend,\n       client_id: client_identity_provider.client_id,\n       confirmable_backend: confirmable_client_identity_provider.identity_provider.backend,\n       confirmable_client_id: confirmable_client_identity_provider.client_id}\n    end\n\n    test \"returns an error with nil client_id\" do\n      context = :context\n      client_id = nil\n      authentication_params = %{}\n\n      assert_raise IdentityProviderError, \"Client identifier not provided.\", fn ->\n        Accounts.create_session(\n          context,\n          client_id,\n          authentication_params,\n          DummySession\n        )\n      end\n    end\n\n    test \"returns an error with unknown client_id\" do\n      context = :context\n      client_id = SecureRandom.uuid()\n      authentication_params = %{}\n\n      assert_raise IdentityProviderError,\n                   \"identity provider not configured for given OAuth client. Please contact your administrator.\",\n                   fn ->\n                     Accounts.create_session(\n                       context,\n                       client_id,\n                       authentication_params,\n                       DummySession\n                     )\n                   end\n    end\n\n    test \"returns an error with empty email\", %{client_id: client_id} do\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, email ->\n        assert email == \"\"\n\n        {:error, \"user not found\"}\n      end)\n\n      context = :context\n      authentication_params = %{email: \"\"}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_session\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Invalid email or password.\"\n    end\n\n    test \"returns an error with a wrong email\", %{client_id: client_id} do\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, email ->\n        assert email == \"does_not_exist\"\n\n        {:error, \"user not found\"}\n      end)\n\n      context = :context\n      authentication_params = %{email: \"does_not_exist\"}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_session\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Invalid email or password.\"\n    end\n\n    test \"returns an error without password\", %{client_id: client_id} do\n      uid = \"ldap_uid\"\n      username = \"ldap_username\"\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, email ->\n        {:ok, {\"user_dn\", %{\"uid\" => uid, \"sn\" => email}}}\n      end)\n      |> expect(:simple_bind, fn _handle, dn, password ->\n        assert dn == \"user_dn\"\n        assert password == nil\n\n        {:error, :boom}\n      end)\n\n      context = :context\n      authentication_params = %{email: username}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_session\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Invalid email or password.\"\n    end\n\n    test \"returns an error with a wrong password\", %{client_id: client_id} do\n      uid = \"ldap_uid\"\n      username = \"ldap_username\"\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, email ->\n        {:ok, {\"user_dn\", %{\"uid\" => uid, \"sn\" => email}}}\n      end)\n      |> expect(:simple_bind, fn _handle, dn, password ->\n        assert dn == \"user_dn\"\n        assert password == \"wrong password\"\n\n        {:error, :boom}\n      end)\n\n      context = :context\n      authentication_params = %{email: username, password: \"wrong password\"}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_session\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Invalid email or password.\"\n    end\n\n    test \"returns an error with a wrong password (confirmable)\", %{\n      confirmable_client_id: client_id\n    } do\n      uid = \"ldap_uid\"\n      username = \"ldap_username\"\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, email ->\n        {:ok, {\"user_dn\", %{\"uid\" => uid, \"sn\" => email}}}\n      end)\n      |> expect(:simple_bind, fn _handle, dn, password ->\n        assert dn == \"user_dn\"\n        assert password == \"wrong password\"\n\n        {:error, :boom}\n      end)\n\n      context = :context\n      authentication_params = %{email: username, password: \"wrong password\"}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_session\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Invalid email or password.\"\n    end\n\n    test \"returns an error if not confirmed\", %{\n      confirmable_client_id: client_id\n    } do\n      uid = \"ldap_uid\"\n      username = \"ldap_username\"\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, email ->\n        {:ok, {\"user_dn\", %{\"uid\" => uid, \"sn\" => email}}}\n      end)\n      |> expect(:simple_bind, fn _handle, dn, password ->\n        assert dn == \"user_dn\"\n        assert password == valid_user_password()\n\n        :ok\n      end)\n\n      context = :context\n      authentication_params = %{email: username, password: valid_user_password()}\n\n      assert {:authentication_failure, ^context,\n              %SessionError{template: %Template{type: \"new_confirmation_instructions\"}} = error} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert error.message ==\n               \"Email confirmation is required to authenticate.\"\n    end\n\n    test \"authenticates the user\", %{client_id: client_id, backend: backend} do\n      uid = \"ldap_uid\"\n      username = \"ldap_username\"\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, email ->\n        {:ok, {\"user_dn\", %{\"uid\" => uid, \"sn\" => email}}}\n      end)\n      |> expect(:simple_bind, fn _handle, dn, password ->\n        assert dn == \"user_dn\"\n        assert password == valid_user_password()\n\n        :ok\n      end)\n\n      context = :context\n      authentication_params = %{email: username, password: valid_user_password()}\n\n      assert {:user_authenticated, ^context,\n              %User{\n                username: ^username,\n                backend: ^backend,\n                uid: ^uid,\n                last_login_at: last_login_at\n              },\n              session_token} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert last_login_at\n      assert session_token\n    end\n\n    test \"does not create multiple users accross multiple authentications\", %{\n      client_id: client_id,\n      backend: backend\n    } do\n      uid = \"ldap_uid\"\n      username = \"ldap_username\"\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, 2, fn _handle, _backend, email ->\n        {:ok, {\"user_dn\", %{\"uid\" => uid, \"sn\" => email}}}\n      end)\n      |> expect(:simple_bind, 2, fn _handle, dn, password ->\n        assert dn == \"user_dn\"\n        assert password == valid_user_password()\n\n        :ok\n      end)\n\n      context = :context\n      authentication_params = %{email: username, password: valid_user_password()}\n\n      assert {:user_authenticated, ^context,\n              %User{id: user_id, username: ^username, backend: ^backend, uid: ^uid},\n              session_token} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert session_token\n\n      assert {:user_authenticated, ^context,\n              %User{id: new_user_id, username: ^username, backend: ^backend, uid: ^uid},\n              session_token} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert session_token\n      assert user_id == new_user_id\n    end\n\n    @tag :skip\n    test \"returns a valid session token\"\n  end\n\n  describe \"delete_session/4\" do\n    setup do\n      client_identity_provider = BorutaIdentity.Factory.insert(:client_identity_provider)\n\n      {:ok,\n       backend: client_identity_provider.identity_provider.backend,\n       client_id: client_identity_provider.client_id}\n    end\n\n    test \"return a success when session does not exist\", %{client_id: client_id} do\n      context = :context\n      session_token = \"unexisting sessino\"\n\n      assert {:session_deleted, ^context} =\n               Accounts.delete_session(\n                 context,\n                 client_id,\n                 session_token,\n                 DummySession\n               )\n    end\n\n    test \"deletes session\", %{client_id: client_id, backend: backend} do\n      context = :context\n      %User{id: user_id, username: email} = user_fixture(%{backend: backend})\n      authentication_params = %{email: email, password: valid_user_password()}\n\n      assert {:user_authenticated, ^context, %User{id: ^user_id}, session_token} =\n               Accounts.create_session(\n                 context,\n                 client_id,\n                 authentication_params,\n                 DummySession\n               )\n\n      assert session_token\n      assert Repo.get_by(UserToken, token: session_token)\n\n      assert {:session_deleted, ^context} =\n               Accounts.delete_session(\n                 context,\n                 client_id,\n                 session_token,\n                 DummySession\n               )\n\n      refute Repo.get_by(UserToken, token: session_token)\n    end\n  end\n\n  @tag :skip\n  test \"initialize_password_instructions/3\"\n\n  @tag :skip\n  test \"send_reset_password_instructions/5\"\n\n  @tag :skip\n  test \"initialize_password_reset/3\"\n\n  describe \"reset_password/4 with an internal backend\" do\n    setup do\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider:\n            build(\n              :identity_provider,\n              user_editable: true\n            )\n        )\n\n      {:ok, client_id: client_identity_provider.client_id}\n    end\n\n    test \"returns an error when password token is invalid\", %{client_id: client_id} do\n      user_token = insert(:reset_password_user_token)\n\n      reset_password_params = %{\n        reset_password_token: user_token.token\n      }\n\n      assert {:password_reset_failure, :context,\n              %ResetPasswordError{\n                message: \"Given reset password token is invalid.\",\n                template: %Template{type: \"edit_reset_password\"}\n              }} =\n               Accounts.reset_password(\n                 :context,\n                 client_id,\n                 reset_password_params,\n                 DummyResetPasswords\n               )\n    end\n\n    test \"returns an error when password params are invalid\", %{client_id: client_id} do\n      user = user_fixture()\n      {token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n      {:ok, _user_token} = Repo.insert(user_token)\n\n      reset_password_params = %{\n        reset_password_token: token,\n        password: \"password\",\n        password_confirmation: \"bad password confirmation\"\n      }\n\n      assert {:password_reset_failure, :context,\n              %ResetPasswordError{\n                message: \"Could not update user password.\",\n                changeset: %Ecto.Changeset{},\n                template: %Template{type: \"edit_reset_password\"}\n              }} =\n               Accounts.reset_password(\n                 :context,\n                 client_id,\n                 reset_password_params,\n                 DummyResetPasswords\n               )\n    end\n\n    test \"returns an error with an already used token\", %{client_id: client_id} do\n      user = user_fixture()\n      {token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n      {:ok, _user_token} = Repo.insert(%{user_token | revoked_at: DateTime.utc_now()})\n      password = \"a good password\"\n\n      reset_password_params = %{\n        reset_password_token: token,\n        password: password,\n        password_confirmation: password\n      }\n\n      assert {:password_reset_failure, :context,\n              %ResetPasswordError{\n                message: \"Given reset password token is invalid.\",\n                template: %Template{type: \"edit_reset_password\"}\n              }} =\n               Accounts.reset_password(\n                 :context,\n                 client_id,\n                 reset_password_params,\n                 DummyResetPasswords\n               )\n    end\n\n    test \"resets user password\", %{client_id: client_id} do\n      %User{id: user_id} = user = user_fixture()\n      {token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n      {:ok, _user_token} = Repo.insert(user_token)\n      password = \"a good password\"\n\n      reset_password_params = %{\n        reset_password_token: token,\n        password: password,\n        password_confirmation: password\n      }\n\n      assert {:password_reseted, :context, %User{id: ^user_id}} =\n               Accounts.reset_password(\n                 :context,\n                 client_id,\n                 reset_password_params,\n                 DummyResetPasswords\n               )\n    end\n\n    test \"invalidates reset password token\", %{client_id: client_id} do\n      %User{id: user_id} = user = user_fixture()\n      {token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n      {:ok, user_token} = Repo.insert(user_token)\n      password = \"a good password\"\n\n      reset_password_params = %{\n        reset_password_token: token,\n        password: password,\n        password_confirmation: password\n      }\n\n      assert {:password_reseted, :context, %User{id: ^user_id}} =\n               Accounts.reset_password(\n                 :context,\n                 client_id,\n                 reset_password_params,\n                 DummyResetPasswords\n               )\n\n      assert %UserToken{revoked_at: revoked_at} = Repo.reload(user_token)\n      assert revoked_at\n    end\n  end\n\n  describe \"reset_password/4 with an ldap backend\" do\n    setup do\n      backend = insert(:ldap_backend)\n\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider:\n            build(\n              :identity_provider,\n              user_editable: true,\n              backend: backend\n            )\n        )\n\n      BorutaIdentity.LdapRepoMock\n      |> stub(:open, fn host, _opts ->\n        assert host == backend.ldap_host\n\n        {:ok, :ldap_pid}\n      end)\n      |> stub(:close, fn _handle ->\n        :ok\n      end)\n\n      {:ok, client_id: client_identity_provider.client_id, backend: backend}\n    end\n\n    test \"returns an error when password token is invalid\", %{client_id: client_id} do\n      user_token = insert(:reset_password_user_token)\n\n      reset_password_params = %{\n        reset_password_token: user_token.token\n      }\n\n      assert {:password_reset_failure, :context,\n              %ResetPasswordError{\n                message: \"Given reset password token is invalid.\",\n                template: %Template{type: \"edit_reset_password\"}\n              }} =\n               Accounts.reset_password(\n                 :context,\n                 client_id,\n                 reset_password_params,\n                 DummyResetPasswords\n               )\n    end\n\n    test \"returns an error when password params are invalid (no ldap user)\", %{\n      client_id: client_id,\n      backend: backend\n    } do\n      user = user_fixture(%{backend: backend})\n      {token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n      {:ok, _user_token} = Repo.insert(user_token)\n\n      reset_password_params = %{\n        reset_password_token: token,\n        password: \"password\",\n        password_confirmation: \"bad password confirmation\"\n      }\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, _email ->\n        {:ok, {\"dn\", %{\"uid\" => \"uid\", backend.ldap_user_rdn_attribute => \"username\"}}}\n      end)\n\n      assert {:password_reset_failure, :context,\n              %ResetPasswordError{\n                message: \"Password and password confirmation do not match.\",\n                changeset: nil,\n                template: %Template{type: \"edit_reset_password\"}\n              }} =\n               Accounts.reset_password(\n                 :context,\n                 client_id,\n                 reset_password_params,\n                 DummyResetPasswords\n               )\n    end\n\n    test \"returns an error when password params are invalid (ldap password error)\", %{\n      client_id: client_id,\n      backend: backend\n    } do\n      user = user_fixture(%{backend: backend})\n      {token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n      {:ok, _user_token} = Repo.insert(user_token)\n\n      reset_password_params = %{\n        reset_password_token: token,\n        password: \"password that fails on ldap\",\n        password_confirmation: \"password that fails on ldap\"\n      }\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, _email ->\n        {:ok, {\"dn\", %{\"uid\" => \"uid\", backend.ldap_user_rdn_attribute => \"username\"}}}\n      end)\n      |> expect(:simple_bind, fn _handle, _master_dn, _master_password -> :ok end)\n      |> expect(:modify_password, fn _handle, _ldap_user, _password -> {:error, \"ldap error\"} end)\n\n      assert {:password_reset_failure, :context,\n              %ResetPasswordError{\n                message: \"ldap error\",\n                changeset: nil,\n                template: %Template{type: \"edit_reset_password\"}\n              }} =\n               Accounts.reset_password(\n                 :context,\n                 client_id,\n                 reset_password_params,\n                 DummyResetPasswords\n               )\n    end\n\n    test \"returns an error with an already used token\", %{client_id: client_id, backend: backend} do\n      user = user_fixture(%{backend: backend})\n      {token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n      {:ok, _user_token} = Repo.insert(%{user_token | revoked_at: DateTime.utc_now()})\n      password = \"a good password\"\n\n      reset_password_params = %{\n        reset_password_token: token,\n        password: password,\n        password_confirmation: password\n      }\n\n      assert {:password_reset_failure, :context,\n              %ResetPasswordError{\n                message: \"Given reset password token is invalid.\",\n                template: %Template{type: \"edit_reset_password\"}\n              }} =\n               Accounts.reset_password(\n                 :context,\n                 client_id,\n                 reset_password_params,\n                 DummyResetPasswords\n               )\n    end\n\n    test \"resets user password\", %{client_id: client_id, backend: backend} do\n      %User{id: user_id} = user = user_fixture(%{backend: backend})\n      {token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n      {:ok, _user_token} = Repo.insert(user_token)\n      password = \"a good password\"\n\n      reset_password_params = %{\n        reset_password_token: token,\n        password: password,\n        password_confirmation: password\n      }\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, _email ->\n        {:ok, {\"dn\", %{\"uid\" => \"uid\", backend.ldap_user_rdn_attribute => \"username\"}}}\n      end)\n      |> expect(:simple_bind, fn _handle, _master_dn, _master_password -> :ok end)\n      |> expect(:modify_password, fn _handle, _ldap_user, _password -> :ok end)\n\n      assert {:password_reseted, :context, %User{id: ^user_id}} =\n               Accounts.reset_password(\n                 :context,\n                 client_id,\n                 reset_password_params,\n                 DummyResetPasswords\n               )\n    end\n\n    test \"invalidates reset password token\", %{client_id: client_id, backend: backend} do\n      %User{id: user_id} = user = user_fixture(%{backend: backend})\n      {token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n      {:ok, user_token} = Repo.insert(user_token)\n      password = \"a good password\"\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, _email ->\n        {:ok, {\"dn\", %{\"uid\" => \"uid\", backend.ldap_user_rdn_attribute => \"username\"}}}\n      end)\n      |> expect(:simple_bind, fn _handle, _master_dn, _master_password -> :ok end)\n      |> expect(:modify_password, fn _handle, _ldap_user, _password -> :ok end)\n\n      reset_password_params = %{\n        reset_password_token: token,\n        password: password,\n        password_confirmation: password\n      }\n\n      assert {:password_reseted, :context, %User{id: ^user_id}} =\n               Accounts.reset_password(\n                 :context,\n                 client_id,\n                 reset_password_params,\n                 DummyResetPasswords\n               )\n\n      assert %UserToken{revoked_at: revoked_at} = Repo.reload(user_token)\n      assert revoked_at\n    end\n  end\n\n  describe \"initialize_edit_user/4\" do\n    setup do\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider:\n            build(\n              :identity_provider,\n              user_editable: true\n            )\n        )\n\n      user = insert(:user)\n\n      {:ok, client_id: client_identity_provider.client_id, user: user}\n    end\n\n    test \"returns an error with nil client_id\", %{user: user} do\n      client_id = nil\n      context = :context\n\n      assert_raise IdentityProviderError, \"Client identifier not provided.\", fn ->\n        Accounts.initialize_edit_user(context, client_id, user, DummySettings)\n      end\n    end\n\n    test \"returns an error with unknown client_id\", %{user: user} do\n      client_id = SecureRandom.uuid()\n      context = :context\n\n      assert_raise IdentityProviderError,\n                   \"identity provider not configured for given OAuth client. Please contact your administrator.\",\n                   fn ->\n                     Accounts.initialize_edit_user(context, client_id, user, DummySettings)\n                   end\n    end\n\n    test \"returns an error if registration is not enabled for client identity provider\", %{\n      user: user\n    } do\n      %ClientIdentityProvider{client_id: client_id} = insert(:client_identity_provider)\n\n      context = :context\n\n      assert_raise IdentityProviderError,\n                   \"Feature is not enabled for client identity provider.\",\n                   fn ->\n                     Accounts.initialize_edit_user(context, client_id, user, DummySettings)\n                   end\n    end\n\n    test \"returns a template\", %{client_id: client_id, user: user} do\n      context = :context\n\n      assert {:edit_user_initialized, ^context, ^user, %Template{}} =\n               Accounts.initialize_edit_user(context, client_id, user, DummySettings)\n    end\n  end\n\n  describe \"update_user/5 with internal backend\" do\n    setup do\n      backend = insert(:backend)\n\n      identity_provider =\n        build(\n          :identity_provider,\n          user_editable: true,\n          backend: backend\n        )\n\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider: identity_provider\n        )\n\n      user = user_fixture(%{backend: backend})\n\n      {:ok, client_id: client_identity_provider.client_id, user: user, backend: backend}\n    end\n\n    test \"returns an error with unexisting user\", %{client_id: client_id} do\n      user = %User{username: \"unexisting\"}\n      confirmation_url_fun = fn -> \"\" end\n\n      assert {:user_update_failure, :context,\n              %SettingsError{message: \"User not found.\", template: %Template{type: \"edit_user\"}}} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{},\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n\n    test \"returns an error with a bad current password\", %{client_id: client_id, user: user} do\n      confirmation_url_fun = fn -> \"\" end\n\n      assert {:user_update_failure, :context,\n              %SettingsError{\n                message: \"Invalid user password.\",\n                template: %Template{type: \"edit_user\"}\n              }} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{current_password: \"bad password\"},\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n\n    test \"returns an error with bad update parameters\", %{client_id: client_id, user: user} do\n      confirmation_url_fun = fn -> \"\" end\n\n      assert {:user_update_failure, :context,\n              %SettingsError{\n                message: \"Could not update user with given params.\",\n                changeset: %Ecto.Changeset{},\n                template: %Template{type: \"edit_user\"}\n              }} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{current_password: valid_user_password(), email: \"\"},\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n\n    test \"updates user\", %{client_id: client_id, user: user} do\n      updated_email = \"updated@email.test\"\n      confirmation_url_fun = fn -> \"\" end\n\n      assert {:user_updated, :context, %User{username: ^updated_email}} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{current_password: valid_user_password(), email: updated_email},\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n\n    test \"updates user with metadata\", %{client_id: client_id, backend: backend} do\n      user =\n        user_fixture(%{\n          backend: backend,\n          metadata: %{\"other\" => %{\"value\" => \"other\", \"status\" => \"valid\", \"display\" => []}}\n        })\n\n      {:ok, _backend} =\n        Ecto.Changeset.change(backend, %{\n          metadata_fields: [\n            %{\"attribute_name\" => \"other\", \"user_editable\" => false},\n            %{\"attribute_name\" => \"test\", \"user_editable\" => true}\n          ]\n        })\n        |> Repo.update()\n\n      metadata = %{\"test\" => \"test value\"}\n      updated_email = \"updated@email.test\"\n      confirmation_url_fun = fn -> \"\" end\n\n      assert {:user_updated, :context,\n              %User{\n                username: ^updated_email,\n                metadata: %{\n                  \"test\" => %{\"value\" => \"test value\", \"status\" => \"valid\", \"display\" => []},\n                  \"other\" => %{\"value\" => \"other\", \"status\" => \"valid\", \"display\" => []}\n                }\n              }} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{\n                   current_password: valid_user_password(),\n                   email: updated_email,\n                   metadata: metadata\n                 },\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n\n    test \"updates user with filtered metadata\", %{\n      client_id: client_id,\n      user: user,\n      backend: backend\n    } do\n      {:ok, _backend} =\n        Ecto.Changeset.change(backend, %{\n          metadata_fields: [\n            %{\"attribute_name\" => \"test\", \"user_editable\" => true},\n            %{\"attribute_name\" => \"restricted_field\", \"user_editable\" => false}\n          ]\n        })\n        |> Repo.update()\n\n      {:ok, user} =\n        Ecto.Changeset.change(user, %{metadata: %{\"restricted_field\" => \"restricted\"}})\n        |> Repo.update()\n\n      metadata = %{\"test\" => \"test value\"}\n      updated_email = \"updated@email.test\"\n      confirmation_url_fun = fn -> \"\" end\n\n      assert {:user_updated, :context, %User{username: ^updated_email}} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{\n                   current_password: valid_user_password(),\n                   email: updated_email,\n                   metadata:\n                     metadata\n                     |> Map.put(\"filtered\", true)\n                     |> Map.put(\"restricted_field\", \"update restricted\")\n                 },\n                 confirmation_url_fun,\n                 DummySettings\n               )\n\n      assert %User{\n               metadata: %{\n                 \"restricted_field\" => %{\"status\" => \"valid\", \"value\" => \"restricted\"},\n                 \"test\" => %{\"status\" => \"valid\", \"value\" => \"test value\"}\n               }\n             } = Repo.reload(user)\n    end\n\n    @tag :skip\n    test \"unconfirms user\"\n  end\n\n  @tag :skip\n  describe \"update_user/5 with ldap backend\" do\n    setup do\n      backend = insert(:ldap_backend)\n\n      identity_provider =\n        build(\n          :identity_provider,\n          user_editable: true,\n          backend: backend\n        )\n\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider: identity_provider\n        )\n\n      user = user_fixture(%{backend: backend})\n\n      BorutaIdentity.LdapRepoMock\n      |> stub(:open, fn host, _opts ->\n        assert host == backend.ldap_host\n\n        {:ok, :ldap_pid}\n      end)\n      |> stub(:close, fn _handle ->\n        :ok\n      end)\n\n      {:ok, client_id: client_identity_provider.client_id, backend: backend, user: user}\n    end\n\n    test \"returns an error with unexisting user\", %{client_id: client_id} do\n      user = %User{username: \"unexisting\"}\n      confirmation_url_fun = fn -> \"\" end\n\n      assert {:user_update_failure, :context,\n              %SettingsError{message: \"User not found.\", template: %Template{type: \"edit_user\"}}} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{},\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n\n    test \"returns an error with a bad current password (no ldap user)\", %{\n      client_id: client_id,\n      user: user\n    } do\n      confirmation_url_fun = fn -> \"\" end\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, _email ->\n        {:error, \"ldap error\"}\n      end)\n\n      assert {:user_update_failure, :context,\n              %SettingsError{\n                message: \"ldap error\",\n                template: %Template{type: \"edit_user\"}\n              }} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{current_password: \"bad password\"},\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n\n    test \"returns an error with a bad current password (ldap password error)\", %{\n      client_id: client_id,\n      backend: backend,\n      user: user\n    } do\n      confirmation_url_fun = fn -> \"\" end\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, _email ->\n        {:ok, {\"dn\", %{\"uid\" => \"uid\", backend.ldap_user_rdn_attribute => \"username\"}}}\n      end)\n      |> expect(:simple_bind, fn _handle, _dn, _password -> {:error, \"ldap error\"} end)\n\n      assert {:user_update_failure, :context,\n              %SettingsError{\n                message: \"Authentication failure.\",\n                template: %Template{type: \"edit_user\"}\n              }} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{current_password: \"bad password\"},\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n\n    test \"returns an error with bad update parameters\", %{\n      client_id: client_id,\n      user: user,\n      backend: backend\n    } do\n      confirmation_url_fun = fn -> \"\" end\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, _email ->\n        {:ok, {\"dn\", %{\"uid\" => \"uid\", backend.ldap_user_rdn_attribute => \"username\"}}}\n      end)\n      |> expect(:simple_bind, fn _handle, _dn, _password -> :ok end)\n      |> expect(:simple_bind, fn _handle, _master_dn, _master_password -> :ok end)\n      |> expect(:modify, fn _handle, _backend, _user, \"\" -> {:error, \"ldap error\"} end)\n\n      assert {:user_update_failure, :context,\n              %SettingsError{\n                message: \"ldap error\",\n                template: %Template{type: \"edit_user\"}\n              }} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{current_password: valid_user_password(), email: \"\"},\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n\n    test \"updates user\", %{client_id: client_id, user: user, backend: backend} do\n      updated_email = \"updated@email.test\"\n      confirmation_url_fun = fn -> \"\" end\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, _email ->\n        {:ok, {\"dn\", %{\"uid\" => \"uid\", backend.ldap_user_rdn_attribute => \"username\"}}}\n      end)\n      |> expect(:simple_bind, fn _handle, _dn, _password -> :ok end)\n      |> expect(:simple_bind, fn _handle, _master_dn, _master_password -> :ok end)\n      |> expect(:modify, fn _handle, _backend, _user, ^updated_email -> :ok end)\n\n      assert {:user_updated, :context, %User{username: ^updated_email}} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{current_password: valid_user_password(), email: updated_email},\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n\n    test \"updates user with metadata\", %{client_id: client_id, user: user, backend: backend} do\n      {:ok, _backend} =\n        Ecto.Changeset.change(backend, %{\n          metadata_fields: [%{\"attribute_name\" => \"test\", \"user_editable\" => true}]\n        })\n        |> Repo.update()\n\n      metadata = %{\"test\" => \"test value\"}\n      updated_email = \"updated@email.test\"\n      confirmation_url_fun = fn -> \"\" end\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, _email ->\n        {:ok, {\"dn\", %{\"uid\" => \"uid\", backend.ldap_user_rdn_attribute => \"username\"}}}\n      end)\n      |> expect(:simple_bind, fn _handle, _dn, _password -> :ok end)\n      |> expect(:simple_bind, fn _handle, _master_dn, _master_password -> :ok end)\n      |> expect(:modify, fn _handle, _backend, _user, ^updated_email -> :ok end)\n\n      assert {:user_updated, :context,\n              %User{\n                username: ^updated_email,\n                metadata: %{\"test\" => %{\"value\" => \"test value\", \"status\" => \"valid\"}}\n              }} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{\n                   current_password: valid_user_password(),\n                   email: updated_email,\n                   metadata: metadata\n                 },\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n\n    test \"updates user with filtered metadata\", %{\n      client_id: client_id,\n      user: user,\n      backend: backend\n    } do\n      Ecto.Changeset.change(backend, %{\n        metadata_fields: [\n          %{\"attribute_name\" => \"test\", \"user_editable\" => true},\n          %{\"attribute_name\" => \"restricted_field\", \"user_editable\" => false}\n        ]\n      })\n      |> Repo.update()\n\n      metadata = %{\"test\" => \"test value\"}\n      updated_email = \"updated@email.test\"\n      confirmation_url_fun = fn -> \"\" end\n\n      BorutaIdentity.LdapRepoMock\n      |> expect(:search, fn _handle, _backend, _email ->\n        {:ok, {\"dn\", %{\"uid\" => \"uid\", backend.ldap_user_rdn_attribute => \"username\"}}}\n      end)\n      |> expect(:simple_bind, fn _handle, _dn, _password -> :ok end)\n      |> expect(:simple_bind, fn _handle, _master_dn, _master_password -> :ok end)\n      |> expect(:modify, fn _handle, _backend, _user, ^updated_email -> :ok end)\n\n      assert {:user_updated, :context,\n              %User{\n                username: ^updated_email,\n                metadata: %{\"test\" => %{\"value\" => \"test value\", \"status\" => \"valid\"}}\n              }} =\n               Accounts.update_user(\n                 :context,\n                 client_id,\n                 user,\n                 %{\n                   current_password: valid_user_password(),\n                   email: updated_email,\n                   metadata:\n                     metadata\n                     |> Map.put(\"filtered\", true)\n                     |> Map.put(\"restricted_field\", \"restricted\")\n                 },\n                 confirmation_url_fun,\n                 DummySettings\n               )\n    end\n  end\n\n  describe \"get_user_by_email/1\" do\n    test \"does not return the user if the email does not exist\" do\n      refute Accounts.get_user_by_email(Backend.default!(), \"unknown@example.com\")\n    end\n\n    test \"returns the user if the email exists\" do\n      %{id: id} = user = user_fixture()\n      assert %User{id: ^id} = Accounts.get_user_by_email(user.backend, user.username)\n    end\n  end\n\n  describe \"get_user_by_session_token/1\" do\n    setup do\n      user = user_fixture()\n      token = BorutaIdentityWeb.ConnCase.generate_user_session_token(user)\n      %{user: user, token: token}\n    end\n\n    test \"returns user by token\", %{user: user, token: token} do\n      assert session_user = Accounts.get_user_by_session_token(token)\n      assert session_user.id == user.id\n    end\n\n    test \"does not return user for invalid token\" do\n      refute Accounts.get_user_by_session_token(\"oops\")\n    end\n\n    test \"does not return user for expired token\", %{token: token} do\n      {1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])\n      refute Accounts.get_user_by_session_token(token)\n    end\n  end\n\n  describe \"send_confirmation_instructions/5\" do\n    test \"returns a success\" do\n      identity_provider = BorutaIdentity.Factory.insert(:identity_provider, confirmable: true)\n\n      %ClientIdentityProvider{client_id: client_id} =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider: identity_provider\n        )\n\n      context = :context\n\n      confirmation_params = %{\n        email: \"unknown@test.test\"\n      }\n\n      confirmation_url_fun = & &1\n\n      assert Accounts.send_confirmation_instructions(\n               context,\n               client_id,\n               confirmation_params,\n               confirmation_url_fun,\n               DummyConfirmation\n             ) == {\n               :confirmation_instructions_delivered,\n               context\n             }\n    end\n\n    @tag :skip\n    test \"delivers confirmation instructions when user is known\"\n  end\n\n  @tag :skip\n  test \"confirm_user/4\"\n\n  @tag :skip\n  test \"initialize_consent/4\"\n\n  describe \"inspect/2\" do\n    test \"does not include password\" do\n      refute inspect(%User{password: \"123456\"}) =~ \"password: \\\"123456\\\"\"\n    end\n  end\n\n  describe \"get_user_scopes/1\" do\n    test \"returns an empty list\" do\n      user = user_fixture()\n\n      assert Accounts.get_user_scopes(user.id) == []\n    end\n\n    test \"returns authorized scopes\" do\n      user = user_fixture()\n      user_scope = user_scopes_fixture(user)\n\n      scope_id = user_scope.scope_id\n\n      assert [%Boruta.Oauth.Scope{id: ^scope_id, name: \"name\"}] =\n               Accounts.get_user_scopes(user.id)\n    end\n  end\n\n  @tag :skip\n  test \"consent/5\"\n\n  describe \"destroy_user/4\" do\n    setup do\n      backend = insert(:backend)\n\n      identity_provider =\n        build(\n          :identity_provider,\n          user_editable: true,\n          backend: backend\n        )\n\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider: identity_provider\n        )\n\n      user = user_fixture(%{backend: backend})\n\n      {:ok, client_id: client_identity_provider.client_id, user: user, backend: backend}\n    end\n\n    test \"returns an error\", %{client_id: client_id} do\n      assert {:user_destroy_failure, :context,\n                %SettingsError{\n                  message: \"User could not be deleted, please contact an administrator.\"\n                }} = Accounts.destroy_user(\n               :context,\n               client_id,\n               %User{uid: SecureRandom.uuid()},\n               DummySettings\n             )\n    end\n\n    test \"destroys user\", %{client_id: client_id, user: user} do\n      assert {:user_destroyed, :context, %User{}} =\n               Accounts.destroy_user(\n                 :context,\n                 client_id,\n                 user,\n                 DummySettings\n               )\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity/admin_test.exs",
    "content": "defmodule BorutaIdentity.AdminTest do\n  use BorutaIdentity.DataCase\n\n  import BorutaIdentity.AccountsFixtures\n  import BorutaIdentity.Factory\n\n  alias BorutaIdentity.Accounts.Internal\n  alias BorutaIdentity.Accounts.LdapError\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.UserAuthorizedScope\n  alias BorutaIdentity.Admin\n  alias BorutaIdentity.Repo\n\n  describe \"update_user_authorized_scopes/2\" do\n    test \"returns an error on duplicates\" do\n      user = insert(:user)\n\n      assert {:error, %Ecto.Changeset{}} =\n               Admin.update_user_authorized_scopes(user, [\n                 %{\"name\" => \"test\"},\n                 %{\"name\" => \"test\"}\n               ])\n    end\n\n    test \"stores user scopes\" do\n      scope = Boruta.Factory.insert(:scope, name: \"test\")\n      user = insert(:user)\n\n      user_id = user.id\n      scope_id = scope.id\n\n      {:ok,\n       %User{\n         authorized_scopes: [\n           %UserAuthorizedScope{\n             scope_id: ^scope_id,\n             user_id: ^user_id\n           }\n         ]\n       }} = Admin.update_user_authorized_scopes(user, [%{\"id\" => scope.id}])\n\n      assert [%{scope_id: ^scope_id, user_id: ^user_id}] = Repo.all(UserAuthorizedScope)\n    end\n  end\n\n  describe \"list_users/0\" do\n    test \"returns an empty list\" do\n      assert Admin.list_users() == %Scrivener.Page{\n               entries: [],\n               page_number: 1,\n               page_size: 12,\n               total_entries: 0,\n               total_pages: 1\n             }\n    end\n\n    test \"returns paginated users\" do\n      user = insert(:user) |> Repo.preload([:authorized_scopes, :roles, :organizations])\n\n      assert Admin.list_users() == %Scrivener.Page{\n               entries: [user],\n               page_number: 1,\n               page_size: 12,\n               total_entries: 1,\n               total_pages: 1\n             }\n    end\n  end\n\n  describe \"search_users/0\" do\n    test \"returns an empty search\" do\n      assert Admin.search_users(\"query\") == %Scrivener.Page{\n               entries: [],\n               page_number: 1,\n               page_size: 12,\n               total_entries: 0,\n               total_pages: 1\n             }\n    end\n\n    test \"returns user search\" do\n      _other = insert(:user) |> Repo.preload(:authorized_scopes)\n\n      user =\n        insert(:user, username: \"match\")\n        |> Repo.preload([:authorized_scopes, :roles, :organizations])\n\n      assert Admin.search_users(\"match\") == %Scrivener.Page{\n               entries: [user],\n               page_number: 1,\n               page_size: 12,\n               total_entries: 1,\n               total_pages: 1\n             }\n    end\n  end\n\n  describe \"delete_user/1 with internal backend\" do\n    test \"returns an error\" do\n      assert Admin.delete_user(Ecto.UUID.generate()) == {:error, :not_found}\n    end\n\n    test \"returns deleted user\" do\n      %User{id: user_id, uid: user_uid} = user_fixture(%{}, \"internal\")\n      assert {:ok, %User{id: ^user_id}} = Admin.delete_user(user_id)\n      refute Repo.get(User, user_id)\n      refute Repo.get(Internal.User, user_uid)\n    end\n  end\n\n  describe \"delete_user/1 with federated backend\" do\n    test \"returns an error\" do\n      assert Admin.delete_user(Ecto.UUID.generate()) == {:error, :not_found}\n    end\n\n    test \"returns deleted user\" do\n      %User{id: user_id} = user_fixture(%{}, \"federated\")\n      assert {:ok, %User{id: ^user_id}} = Admin.delete_user(user_id)\n      refute Repo.get(User, user_id)\n    end\n  end\n\n  @tag :skip\n  test \"delete_user/1 with ldap backend\"\n\n  describe \"create_user/2 with an internal backend\" do\n    setup do\n      backend = insert(:backend)\n\n      {:ok, backend: backend}\n    end\n\n    test \"returns an error when data is invalid\", %{backend: backend} do\n      params = %{}\n\n      assert {:error, %Ecto.Changeset{}} = Admin.create_user(backend, params)\n    end\n\n    test \"creates a user\", %{backend: backend} do\n      params = %{username: \"test@created.email\", password: \"a valid password\"}\n\n      assert {:ok, %User{}} = Admin.create_user(backend, params)\n    end\n\n    test \"creates a user with metadata\", %{backend: backend} do\n      metadata_field = %{\n        \"attribute_name\" => \"attribute_test\"\n      }\n\n      {:ok, backend} =\n        Ecto.Changeset.change(backend, %{\n          metadata_fields: [\n            metadata_field\n          ]\n        })\n        |> Repo.update()\n\n      params = %{\n        username: \"test@created.email\",\n        password: \"a valid password\",\n        metadata: %{\n          \"attribute_test\" => %{\n            \"value\" => \"attribute_test value\",\n            \"status\" => \"valid\"\n          }\n        }\n      }\n\n      assert {:ok,\n              %User{\n                metadata: %{\n                  \"attribute_test\" => %{\n                    \"value\" => \"attribute_test value\",\n                    \"status\" => \"valid\"\n                  }\n                }\n              }} = Admin.create_user(backend, params)\n    end\n\n    test \"returns an error with invalid metadata\", %{backend: backend} do\n      metadata_field = %{\n        \"attribute_name\" => \"attribute_test\"\n      }\n\n      {:ok, backend} =\n        Ecto.Changeset.change(backend, %{\n          metadata_fields: [\n            metadata_field\n          ]\n        })\n        |> Repo.update()\n\n      params = %{\n        username: \"test@created.email\",\n        password: \"a valid password\",\n        metadata: %{\n          \"attribute_test\" => %{\n            \"value\" => \"attribute_test value\"\n          }\n        }\n      }\n\n      assert Enum.empty?(Repo.all(Internal.User))\n      assert_raise Ecto.InvalidChangesetError, fn ->\n        Admin.create_user(backend, params) == {:error, %Ecto.Changeset{}}\n      end\n    end\n\n    test \"creates a user with a group\", %{backend: backend} do\n      params = %{\n        username: \"test@created.email\",\n        password: \"a valid password\",\n        group: \"group\"\n      }\n\n      assert {:ok,\n              %User{\n                group: \"group\"\n              }} = Admin.create_user(backend, params)\n    end\n\n    test \"creates a user with a default organization\", %{backend: backend} do\n      {:ok, backend} =\n        Ecto.Changeset.change(backend, %{create_default_organization: true}) |> Repo.update()\n\n      params = %{\n        username: \"test@created.email\",\n        password: \"a valid password\"\n      }\n\n      assert {:ok,\n              %User{\n                uid: uid,\n                organizations: [organization]\n              }} = Admin.create_user(backend, params)\n\n      new_organization_name = \"default_#{uid}\"\n\n      assert %{organization: %{name: ^new_organization_name}} =\n               Repo.preload(organization, :organization)\n    end\n\n    @tag :skip\n    test \"creates a user with roles\"\n  end\n\n  describe \"create_user/2 with a ldap backend\" do\n    setup do\n      backend = insert(:ldap_backend)\n\n      {:ok, backend: backend}\n    end\n\n    test \"raises an error\", %{backend: backend} do\n      params = %{}\n\n      assert_raise LdapError, fn ->\n        Admin.create_user(backend, params)\n      end\n    end\n\n    @tag :skip\n    test \"creates a user\"\n\n    @tag :skip\n    test \"creates a user with roles\"\n  end\n\n  describe \"update_user/2 with an internal backend\" do\n    setup do\n      user = user_fixture()\n\n      {:ok, user: user}\n    end\n\n    test \"updates user with metadata\", %{user: user} do\n      {:ok, _backend} =\n        Ecto.Changeset.change(user.backend, %{metadata_fields: [%{attribute_name: \"test\"}]})\n        |> Repo.update()\n\n      metadata = %{\n        \"attribute_test\" => %{\n          \"value\" => \"attribute_test value\",\n          \"status\" => \"valid\"\n        }\n      }\n\n      user_params = %{metadata: metadata}\n\n      assert {:ok, %User{metadata: ^metadata}} = Admin.update_user(user, user_params)\n    end\n\n    test \"returns an error with invalid metadata\", %{user: user} do\n      {:ok, _backend} =\n        Ecto.Changeset.change(user.backend, %{metadata_fields: [%{attribute_name: \"test\"}]})\n        |> Repo.update()\n\n      metadata = %{\n        \"attribute_test\" => %{\n          \"value\" => \"attribute_test value\"\n        }\n      }\n\n      user_params = %{metadata: metadata}\n\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [metadata: {\"Required property status was not present. at #\", []}]\n              }} = Admin.update_user(user, user_params)\n    end\n\n    test \"returns an error if group is not unique\", %{user: user} do\n      {:ok, _backend} =\n        Ecto.Changeset.change(user.backend, %{metadata_fields: [%{attribute_name: \"test\"}]})\n        |> Repo.update()\n\n      user_params = %{group: \"group group\"}\n\n      assert {:error, %Ecto.Changeset{errors: [group: {\"must be unique\", []}]}} =\n               Admin.update_user(user, user_params)\n    end\n\n    test \"updates user with a group\", %{user: user} do\n      {:ok, _backend} =\n        Ecto.Changeset.change(user.backend, %{metadata_fields: [%{attribute_name: \"test\"}]})\n        |> Repo.update()\n\n      user_params = %{group: \"group\"}\n\n      assert {:ok, %User{group: \"group\"}} = Admin.update_user(user, user_params)\n    end\n\n    @tag :skip\n    test \"updates user with roles\"\n  end\n\n  @tag :skip\n  test \"update_user_roles/2\"\n\n  @tag :skip\n  test \"update_user_authorized_scopes/2\"\n\n  @tag :skip\n  test \"create_raw_user/2\"\n\n  @tag :skip\n  test \"import_users/3\"\n\n  @tag :skip\n  test \"delete_user_authorized_scopes_by_id/1\"\n\n  describe \"roles\" do\n    alias BorutaIdentity.Accounts.Role\n\n    import BorutaIdentity.AdminFixtures\n\n    @invalid_attrs %{name: nil}\n\n    test \"list_roles/0 returns all roles\" do\n      role = role_fixture()\n      assert Admin.list_roles() == [role]\n    end\n\n    test \"get_role!/1 returns the role with given id\" do\n      role = role_fixture()\n      assert Admin.get_role!(role.id) == role\n    end\n\n    test \"create_role/1 with valid data creates a role\" do\n      valid_attrs = %{name: \"some name\"}\n\n      assert {:ok, %Role{} = role} = Admin.create_role(valid_attrs)\n      assert role.name == \"some name\"\n    end\n\n    test \"create_role/1 with scopes creates a role\" do\n      scope = Boruta.Factory.insert(:scope)\n      valid_attrs = %{name: \"some name\", scopes: [%{id: scope.id, name: scope.name}]}\n\n      assert {:ok, %Role{} = role} = Admin.create_role(valid_attrs)\n      assert role.name == \"some name\"\n      scope_id = scope.id\n      scope_name = scope.name\n      assert [%{id: ^scope_id, name: ^scope_name}] = role.scopes\n    end\n\n    test \"create_role/1 with invalid data returns error changeset\" do\n      assert {:error, %Ecto.Changeset{}} = Admin.create_role(@invalid_attrs)\n    end\n\n    test \"update_role/2 with valid data updates the role\" do\n      role = role_fixture()\n      update_attrs = %{name: \"some updated name\"}\n\n      assert {:ok, %Role{} = role} = Admin.update_role(role, update_attrs)\n      assert role.name == \"some updated name\"\n    end\n\n    test \"update_role/2 with scopes updates a role\" do\n      role = role_fixture()\n      scope = Boruta.Factory.insert(:scope)\n      update_attrs = %{name: \"some updated name\", scopes: [%{id: scope.id, name: scope.name}]}\n\n      assert {:ok, %Role{} = role} = Admin.update_role(role, update_attrs)\n      assert role.name == \"some updated name\"\n      scope_id = scope.id\n      scope_name = scope.name\n      assert [%{id: ^scope_id, name: ^scope_name}] = role.scopes\n    end\n\n    test \"update_role/2 with invalid data returns error changeset\" do\n      role = role_fixture()\n      assert {:error, %Ecto.Changeset{}} = Admin.update_role(role, @invalid_attrs)\n      assert role == Admin.get_role!(role.id)\n    end\n\n    test \"delete_role/1 deletes the role\" do\n      role = role_fixture()\n      assert {:ok, %Role{}} = Admin.delete_role(role)\n      assert_raise Ecto.NoResultsError, fn -> Admin.get_role!(role.id) end\n    end\n\n    test \"change_role/1 returns a role changeset\" do\n      role = role_fixture()\n      assert %Ecto.Changeset{} = Admin.change_role(role)\n    end\n  end\n\n  defmodule OrganizationsTest do\n    use BorutaIdentity.DataCase, async: true\n\n    alias BorutaIdentity.Organizations.Organization\n\n    describe \"list_organizations/0\" do\n      test \"returns an empty set\" do\n        assert Enum.empty?(Admin.list_organizations())\n      end\n\n      test \"returns paginated organizations\" do\n        organization = insert(:organization)\n\n        assert Admin.list_organizations() == %Scrivener.Page{\n                 page_number: 1,\n                 page_size: 500,\n                 total_entries: 1,\n                 total_pages: 1,\n                 entries: [organization]\n               }\n      end\n    end\n\n    describe \"get_organization/1\" do\n      test \"returns nil\" do\n        assert Admin.get_organization(\"bad id\") == nil\n      end\n\n      test \"returns nil with an unkown uuid\" do\n        organization_id = SecureRandom.uuid()\n\n        assert Admin.get_organization(organization_id) == nil\n      end\n\n      test \"returns an organization\" do\n        %Organization{id: organization_id} = organization = insert(:organization)\n\n        assert Admin.get_organization(organization_id) == organization\n      end\n    end\n\n    describe \"create_organization/1\" do\n      test \"returns an error with invalid params\" do\n        organization_params = %{name: nil}\n        assert {:error, %Ecto.Changeset{}} = Admin.create_organization(organization_params)\n      end\n\n      test \"creates and organization\" do\n        organization_params = %{name: \"Organization\"}\n\n        assert {:ok, %Organization{name: \"Organization\"}} =\n                 Admin.create_organization(organization_params)\n      end\n\n      test \"creates and organization with label\" do\n        organization_params = %{name: \"Organization\", label: \"label\"}\n\n        assert {:ok, %Organization{name: \"Organization\", label: \"label\"}} =\n                 Admin.create_organization(organization_params)\n      end\n    end\n\n    describe \"update_organization/2\" do\n      test \"returns an error with unkown organization\" do\n        organization = build(:organization)\n\n        organization_params = %{name: nil}\n\n        assert {:error, %Ecto.Changeset{}} =\n                 Admin.update_organization(organization, organization_params)\n      end\n\n      test \"returns an error with invalid params\" do\n        organization = insert(:organization)\n\n        organization_params = %{name: nil}\n\n        assert {:error, %Ecto.Changeset{}} =\n                 Admin.update_organization(organization, organization_params)\n      end\n\n      test \"updates an organization\" do\n        organization = insert(:organization)\n\n        organization_params = %{name: \"updated\"}\n\n        assert {:ok, %Organization{name: \"updated\"}} =\n                 Admin.update_organization(organization, organization_params)\n\n        assert %Organization{name: \"updated\"} = Repo.reload(organization)\n      end\n\n      test \"updates an organization with label\" do\n        organization = insert(:organization)\n\n        organization_params = %{name: \"updated\", label: \"updated\"}\n\n        assert {:ok, %Organization{name: \"updated\", label: \"updated\"}} =\n                 Admin.update_organization(organization, organization_params)\n\n        assert %Organization{name: \"updated\"} = Repo.reload(organization)\n      end\n    end\n\n    describe \"delete_organization/1\" do\n      test \"returns an error with unkown organization\" do\n        organization_id = SecureRandom.uuid()\n\n        assert {:error, :not_found} = Admin.delete_organization(organization_id)\n      end\n\n      test \"deletes an organization\" do\n        %Organization{id: organization_id} = insert(:organization)\n\n        assert {:ok, %Organization{}} = Admin.delete_organization(organization_id)\n\n        assert Repo.get(Organization, organization_id) == nil\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity/configuration_test.exs",
    "content": "defmodule BorutaIdentity.ConfigurationTest do\n  use BorutaIdentity.DataCase\n\n  import BorutaIdentity.Factory\n\n  alias BorutaIdentity.Configuration\n  alias BorutaIdentity.Configuration.ErrorTemplate\n  alias BorutaIdentity.Repo\n\n  describe \"get_error_template!/1\" do\n    test \"returns nil with unexisting template\" do\n      assert_raise Ecto.NoResultsError, fn ->\n        Configuration.get_error_template!(:unexisting) == nil\n      end\n    end\n\n    test \"returns default template\" do\n      template = Configuration.get_error_template!(400)\n\n      assert template == ErrorTemplate.default_template(400)\n    end\n\n    test \"returns error template\" do\n      template =\n        insert(:error_template,\n          content: \"custom registration template\"\n        )\n\n      assert Configuration.get_error_template!(400) == template\n    end\n  end\n\n  describe \"upsert_error_template/1\" do\n    test \"inserts with a default template\" do\n      template = Configuration.get_error_template!(400)\n\n      assert {:ok, template} = Configuration.upsert_error_template(template, %{content: \"new content\"})\n\n      assert Repo.reload(template)\n    end\n\n    test \"updates with an existing template\" do\n      template = insert(:error_template)\n\n      assert {:ok, template} = Configuration.upsert_error_template(template, %{content: \"new content\"})\n\n      assert Repo.reload(template)\n    end\n  end\n\n  describe \"delete_error_template!/2\" do\n    test \"raises an error with unexisting template\" do\n      assert_raise Ecto.NoResultsError, fn ->\n        Configuration.delete_error_template!(:unexisting)\n      end\n    end\n\n    test \"returns an error if template is default\" do\n      assert_raise Ecto.NoResultsError, fn ->\n        Configuration.delete_error_template!(400)\n      end\n    end\n\n    test \"deletes and returns error template\" do\n      template =\n        insert(:error_template,\n          content: \"custom registration template\"\n        )\n\n      default_template = ErrorTemplate.default_template(400)\n\n      reseted_template =\n        Configuration.delete_error_template!(400)\n\n      assert reseted_template.default == true\n      assert reseted_template.type == \"400\"\n      assert reseted_template.content == default_template.content\n\n      assert Repo.get_by(ErrorTemplate, id: template.id) == nil\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity/federated_accounts_test.exs",
    "content": "defmodule BorutaIdentity.FederatedAccountsTest do\n  use BorutaIdentity.DataCase\n\n  alias BorutaIdentity.Accounts.IdentityProviderError\n  alias BorutaIdentity.Accounts.SessionError\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.FederatedAccounts\n\n  defmodule DummyFederatedSessions do\n    @behaviour BorutaIdentity.Accounts.FederatedSessionApplication\n\n    @impl BorutaIdentity.Accounts.FederatedSessionApplication\n    def user_authenticated(context, user, session_token) do\n      {:user_authenticated, context, user, session_token}\n    end\n\n    @impl BorutaIdentity.Accounts.FederatedSessionApplication\n    def authentication_failure(context, error) do\n      {:authentication_failure, context, error}\n    end\n  end\n\n  describe \"create_federated_session/4\" do\n    setup do\n      federated_backend = BorutaIdentity.Factory.insert(:federated_backend)\n\n      identity_provider =\n        BorutaIdentity.Factory.insert(:identity_provider, backend: federated_backend)\n\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(\n          :client_identity_provider,\n          identity_provider: identity_provider\n        )\n\n      bypass = Bypass.open(port: 7878)\n      Bypass.up(bypass)\n\n      {:ok,\n       bypass: bypass, client_id: client_identity_provider.client_id, backend: federated_backend}\n    end\n\n    test \"returns an error if client id is unknown\" do\n      context = :context\n      access_token = \"access_token\"\n\n      assert_raise IdentityProviderError, fn ->\n        FederatedAccounts.create_federated_session(\n          context,\n          \"unknown\",\n          \"unknown\",\n          access_token,\n          DummyFederatedSessions\n        )\n      end\n    end\n\n    test \"returns an error if federated server is unknown\", %{client_id: client_id} do\n      context = :context\n      access_token = \"access_token\"\n\n      assert {:authentication_failure, ^context,\n              %SessionError{message: \"Could not fetch associated federated server.\"}} =\n               FederatedAccounts.create_federated_session(\n                 context,\n                 client_id,\n                 \"unknown\",\n                 access_token,\n                 DummyFederatedSessions\n               )\n    end\n\n    test \"returns an error if code fails\", %{\n      client_id: client_id,\n      backend: backend,\n      bypass: bypass\n    } do\n      federated_server = List.first(backend.federated_servers)\n      context = :context\n      code = \"code\"\n      error = \"error\"\n\n      Bypass.stub(bypass, \"POST\", federated_server[\"token_path\"], fn conn ->\n        Plug.Conn.resp(conn, 400, Jason.encode!(%{error: error}))\n      end)\n\n      assert {:authentication_failure, ^context,\n              %SessionError{message: message}} =\n               FederatedAccounts.create_federated_session(\n                 context,\n                 client_id,\n                 federated_server[\"name\"],\n                 code,\n                 DummyFederatedSessions\n               )\n      assert message =~ ~r/#{error}/\n    end\n\n    test \"returns an error if userinfo fails\", %{\n      client_id: client_id,\n      backend: backend,\n      bypass: bypass\n    } do\n      federated_server = List.first(backend.federated_servers)\n      context = :context\n      code = \"code\"\n\n      Bypass.stub(bypass, \"POST\", federated_server[\"token_path\"], fn conn ->\n        Plug.Conn.resp(conn, 200, Jason.encode!(%{access_token: \"access_token\"}))\n      end)\n\n      Bypass.stub(bypass, \"GET\", federated_server[\"userinfo_path\"], fn conn ->\n        Plug.Conn.resp(conn, 401, \"\")\n      end)\n\n      assert {:authentication_failure, ^context,\n              %SessionError{message: \"Could not fetch user information.\"}} =\n               FederatedAccounts.create_federated_session(\n                 context,\n                 client_id,\n                 federated_server[\"name\"],\n                 code,\n                 DummyFederatedSessions\n               )\n    end\n\n    test \"creates user session\", %{client_id: client_id, backend: backend, bypass: bypass} do\n      federated_server = List.first(backend.federated_servers)\n      context = :context\n      code = \"code\"\n      sub = \"sub\"\n\n      Bypass.stub(bypass, \"POST\", federated_server[\"token_path\"], fn conn ->\n        Plug.Conn.resp(conn, 200, Jason.encode!(%{access_token: \"access_token\"}))\n      end)\n\n      Bypass.stub(bypass, \"GET\", federated_server[\"userinfo_path\"], fn conn ->\n        Plug.Conn.resp(conn, 200, Jason.encode!(%{sub: sub}))\n      end)\n\n      assert {:user_authenticated, ^context, %User{uid: ^sub}, _session_token} =\n               FederatedAccounts.create_federated_session(\n                 context,\n                 client_id,\n                 federated_server[\"name\"],\n                 code,\n                 DummyFederatedSessions\n               )\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity/identity_providers/backend_test.exs",
    "content": "defmodule BorutaIdentity.IdentityProviders.BackendTest do\n  use BorutaIdentity.DataCase\n\n  import BorutaIdentity.Factory\n\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  describe \"federated_login_url/2\" do\n    test \"returns an empty string\" do\n      backend = insert(:backend)\n\n      assert Backend.federated_login_url(backend, \"inexistant\") == \"\"\n    end\n\n    test \"returns login url\" do\n      federated_servers = [\n        %{\n          \"name\" => \"name\",\n          \"client_id\" => \"client_id\",\n          \"client_secret\" => \"client_secret\",\n          \"base_url\" => \"https://host.test\",\n          \"authorize_path\" => \"/authorize\",\n          \"token_path\" => \"/token\",\n          \"scope\" => \"openid email\"\n        }\n      ]\n      backend = insert(:backend, federated_servers: federated_servers)\n\n      assert Backend.federated_login_url(backend, \"name\") == \"https://host.test/authorize?client_id=client_id&redirect_uri=http%3A%2F%2Flocalhost%3A4003%2Fbackends%2F#{backend.id}%2Fname%2Fcallback&response_type=code&scope=openid+email\"\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity/identity_providers/identity_provider_test.exs",
    "content": "defmodule BorutaIdentity.IdentityProviders.IdentityProviderTest do\n  use BorutaIdentity.DataCase\n\n  import BorutaIdentity.Factory\n\n  alias BorutaIdentity.Accounts.Ldap\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.IdentityProviders.Template\n  alias BorutaIdentity.Repo\n\n  describe \"template/2\" do\n    setup do\n      identity_provider = insert(:identity_provider, templates: [])\n\n      identity_provider_with_template =\n        insert(:identity_provider,\n          templates: [\n            build(:new_registration_template, content: \"custom new registration template\")\n          ]\n        )\n\n      {:ok,\n       identity_provider: identity_provider,\n       identity_provider_with_template: identity_provider_with_template}\n    end\n\n    test \"returns nil\", %{identity_provider: identity_provider} do\n      assert IdentityProvider.template(identity_provider, :unexisting) == nil\n    end\n\n    test \"returns default template\", %{identity_provider: identity_provider} do\n      assert IdentityProvider.template(identity_provider, :new_registration) ==\n               %{\n                 Template.default_template(:new_registration)\n                 | identity_provider_id: identity_provider.id,\n                   identity_provider: identity_provider\n               }\n    end\n\n    test \"returns identity provider template\", %{\n      identity_provider_with_template: identity_provider\n    } do\n      assert IdentityProvider.template(identity_provider, :new_registration) ==\n               List.first(identity_provider.templates)\n               |> Repo.preload(identity_provider: [:backend, :templates])\n    end\n  end\n\n  describe \"check_feature/2\" do\n    setup do\n      identity_provider = insert(:identity_provider)\n\n      {:ok, identity_provider: identity_provider}\n    end\n\n    test \"returns an error if feature is not supported\", %{identity_provider: identity_provider} do\n      assert IdentityProvider.check_feature(identity_provider, :not_supported) ==\n               {:error, \"This provider does not support this feature.\"}\n    end\n\n    test \"returns an error if identity provider disabled the feature\", %{\n      identity_provider: identity_provider\n    } do\n      identity_provider = %{identity_provider | authenticable: false}\n\n      assert IdentityProvider.check_feature(identity_provider, :initialize_session) ==\n               {:error, \"Feature is not enabled for client identity provider.\"}\n    end\n\n    test \"returns an error if identity provider backend does not support the feature\", %{\n      identity_provider: identity_provider\n    } do\n      identity_provider = %{\n        identity_provider\n        | registrable: true,\n          backend: insert(:backend, type: Atom.to_string(Ldap))\n      }\n\n      assert IdentityProvider.check_feature(identity_provider, :register) ==\n               {:error, \"Feature is not enabled for identity provider backend implementation.\"}\n    end\n\n    test \"returns ok if feature is supported\", %{identity_provider: identity_provider} do\n      assert IdentityProvider.check_feature(identity_provider, :initialize_session) ==\n               :ok\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity/identity_providers_test.exs",
    "content": "defmodule BorutaIdentity.IdentityProvidersTest do\n  use BorutaIdentity.DataCase\n\n  import BorutaIdentity.Factory\n  import Mox\n\n  alias BorutaIdentity.Accounts.EmailTemplate\n  alias BorutaIdentity.Accounts.Ldap\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.IdentityProviders.ClientIdentityProvider\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.IdentityProviders.Template\n  alias BorutaIdentity.Repo\n\n  setup :set_mox_global\n\n  describe \"identity_providers\" do\n    setup do\n      backend = insert(:backend)\n\n      {:ok, backend: backend}\n    end\n\n    @valid_attrs %{name: \"some name\", backend_id: nil}\n    @update_attrs %{name: \"some updated name\"}\n    @invalid_attrs %{name: nil}\n\n    def identity_provider_fixture(attrs \\\\ %{}) do\n      insert(:identity_provider, Map.merge(@valid_attrs, attrs))\n      |> Repo.preload(backend: :email_templates)\n    end\n\n    test \"list_identity_providers/0 returns all identity_providers\" do\n      identity_provider = identity_provider_fixture()\n      assert IdentityProviders.list_identity_providers() == [identity_provider]\n    end\n\n    test \"get_identity_provider!/1 returns the identity_provider with given id\" do\n      identity_provider = identity_provider_fixture()\n      assert IdentityProviders.get_identity_provider!(identity_provider.id) == identity_provider\n    end\n\n    test \"create_identity_provider/1 with valid data creates a identity_provider\", %{\n      backend: backend\n    } do\n      assert {:ok, %IdentityProvider{} = identity_provider} =\n               IdentityProviders.create_identity_provider(%{@valid_attrs | backend_id: backend.id})\n\n      assert identity_provider.name == \"some name\"\n    end\n\n    test \"create_identity_provider/1 with valid data (with a new template) creates a identity_provider\",\n         %{backend: backend} do\n      templates_attrs = %{templates: [%{type: \"new_registration\", content: \"test content\"}]}\n\n      assert {:ok, %IdentityProvider{} = identity_provider} =\n               IdentityProviders.create_identity_provider(\n                 Map.merge(%{@valid_attrs | backend_id: backend.id}, templates_attrs)\n               )\n\n      assert [%Template{type: \"new_registration\", content: \"test content\"}] =\n               identity_provider.templates\n    end\n\n    test \"create_identity_provider/1 with invalid data returns error changeset\" do\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [\n                  name: {\"can't be blank\", [validation: :required]},\n                  backend_id: {\"can't be blank\", [validation: :required]}\n                ]\n              }} = IdentityProviders.create_identity_provider(@invalid_attrs)\n    end\n\n    test \"create_identity_provider/1 with invalid data (unique name) returns error changeset\", %{\n      backend: backend\n    } do\n      identity_provider_fixture()\n\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [\n                  name:\n                    {\"has already been taken\",\n                     [constraint: :unique, constraint_name: \"identity_providers_name_index\"]}\n                ]\n              }} =\n               IdentityProviders.create_identity_provider(%{@valid_attrs | backend_id: backend.id})\n    end\n\n    test \"update_identity_provider/2 with valid data updates the identity_provider\" do\n      identity_provider = identity_provider_fixture()\n\n      assert {:ok, %IdentityProvider{} = identity_provider} =\n               IdentityProviders.update_identity_provider(identity_provider, @update_attrs)\n\n      assert identity_provider.name == \"some updated name\"\n    end\n\n    test \"update_identity_provider/1 with valid data (with an existing template) creates a identity_provider\" do\n      identity_provider = identity_provider_fixture()\n      template = insert(:template, identity_provider: identity_provider)\n\n      templates_attrs = %{\n        templates: [%{id: template.id, type: \"new_registration\", content: \"test content\"}]\n      }\n\n      assert {:ok, %IdentityProvider{} = identity_provider} =\n               IdentityProviders.update_identity_provider(identity_provider, templates_attrs)\n\n      template_id = template.id\n\n      assert [\n               %Template{\n                 id: ^template_id,\n                 type: \"new_registration\",\n                 content: \"test content\"\n               }\n             ] = identity_provider.templates\n    end\n\n    test \"update_identity_provider/1 with valid data (with an existing template, delete_if_exists) creates a identity_provider\" do\n      identity_provider = identity_provider_fixture()\n      insert(:template, identity_provider: identity_provider)\n\n      templates_attrs = %{\n        templates: [%{type: \"new_registration\", content: \"test content\"}]\n      }\n\n      assert {:ok, %IdentityProvider{} = identity_provider} =\n               IdentityProviders.update_identity_provider(identity_provider, templates_attrs)\n\n      assert [\n               %Template{\n                 type: \"new_registration\",\n                 content: \"test content\"\n               }\n             ] = identity_provider.templates\n    end\n\n    test \"update_identity_provider/2 with invalid data returns error changeset\" do\n      identity_provider = identity_provider_fixture()\n\n      assert {:error, %Ecto.Changeset{}} =\n               IdentityProviders.update_identity_provider(identity_provider, @invalid_attrs)\n\n      assert identity_provider == IdentityProviders.get_identity_provider!(identity_provider.id)\n    end\n\n    test \"update_identity_provider/2 with invalid data (unique name) returns error changeset\", %{\n      backend: backend\n    } do\n      identity_provider_fixture()\n      identity_provider = identity_provider_fixture(%{name: \"other\"})\n\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [\n                  name:\n                    {\"has already been taken\",\n                     [constraint: :unique, constraint_name: \"identity_providers_name_index\"]}\n                ]\n              }} =\n               IdentityProviders.update_identity_provider(identity_provider, %{\n                 @valid_attrs\n                 | backend_id: backend.id\n               })\n\n      assert identity_provider == IdentityProviders.get_identity_provider!(identity_provider.id)\n    end\n\n    test \"delete_identity_provider/1 deletes the identity_provider\" do\n      identity_provider = identity_provider_fixture()\n\n      assert {:ok, %IdentityProvider{}} =\n               IdentityProviders.delete_identity_provider(identity_provider)\n\n      assert_raise Ecto.NoResultsError, fn ->\n        IdentityProviders.get_identity_provider!(identity_provider.id)\n      end\n    end\n\n    test \"delete_identity_provider/1 returns an error when associated to a client\" do\n      identity_provider = identity_provider_fixture()\n      insert(:client_identity_provider, identity_provider: identity_provider)\n\n      assert {:error, %Ecto.Changeset{errors: [client_identity_providers: {_message, []}]}} =\n               IdentityProviders.delete_identity_provider(identity_provider)\n    end\n\n    test \"change_identity_provider/1 returns a identity_provider changeset\" do\n      identity_provider = identity_provider_fixture()\n      assert %Ecto.Changeset{} = IdentityProviders.change_identity_provider(identity_provider)\n    end\n  end\n\n  describe \"upsert_client_identity_provider/2\" do\n    test \"inserts client identity provider\" do\n      %IdentityProvider{id: identity_provider_id} = insert(:identity_provider)\n      client_id = SecureRandom.uuid()\n\n      assert {:ok,\n              %ClientIdentityProvider{\n                client_id: ^client_id,\n                identity_provider_id: ^identity_provider_id\n              }} =\n               IdentityProviders.upsert_client_identity_provider(client_id, identity_provider_id)\n    end\n\n    test \"updates client identity provider\" do\n      %ClientIdentityProvider{client_id: client_id} = insert(:client_identity_provider)\n\n      %IdentityProvider{id: new_identity_provider_id} = insert(:identity_provider)\n\n      assert {:ok,\n              %ClientIdentityProvider{\n                client_id: ^client_id,\n                identity_provider_id: ^new_identity_provider_id\n              }} =\n               IdentityProviders.upsert_client_identity_provider(\n                 client_id,\n                 new_identity_provider_id\n               )\n    end\n  end\n\n  describe \"remove_client_identity_provider/2\" do\n    test \"remove client identity provider\" do\n      client_id = SecureRandom.uuid()\n\n      client_identity_provider =\n        insert(:client_identity_provider, client_id: client_id) |> Repo.reload()\n\n      assert {:ok, ^client_identity_provider} =\n               IdentityProviders.remove_client_identity_provider(client_id)\n\n      assert_raise Ecto.NoResultsError, fn ->\n        Repo.get!(ClientIdentityProvider, client_identity_provider.id)\n      end\n    end\n\n    test \"returns nil when not exists\" do\n      client_id = SecureRandom.uuid()\n\n      assert IdentityProviders.remove_client_identity_provider(client_id) == {:ok, nil}\n    end\n  end\n\n  describe \"get_identity_provider_by_client_id/1\" do\n    test \"returns nil with nil\" do\n      assert IdentityProviders.get_identity_provider_by_client_id(nil) == nil\n    end\n\n    test \"returns nil with a raw string\" do\n      assert IdentityProviders.get_identity_provider_by_client_id(\"bad_id\") == nil\n    end\n\n    test \"returns nil with a random uuid\" do\n      assert IdentityProviders.get_identity_provider_by_client_id(SecureRandom.uuid()) == nil\n    end\n\n    test \"returns client's identity provider\" do\n      %ClientIdentityProvider{client_id: client_id, identity_provider: identity_provider} =\n        insert(:client_identity_provider)\n\n      identity_provider = Repo.preload(identity_provider, backend: :email_templates)\n\n      assert IdentityProviders.get_identity_provider_by_client_id(client_id) == identity_provider\n    end\n  end\n\n  describe \"get_identity_provider_template!/2\" do\n    test \"raises an error with unexisting identity provider\" do\n      identity_provider_id = SecureRandom.uuid()\n\n      assert_raise Ecto.NoResultsError, fn ->\n        IdentityProviders.get_identity_provider_template!(identity_provider_id, :unexisting)\n      end\n    end\n\n    test \"raises an error with unexisting template\" do\n      identity_provider_id = insert(:identity_provider).id\n\n      assert_raise Ecto.NoResultsError, fn ->\n        IdentityProviders.get_identity_provider_template!(identity_provider_id, :unexisting)\n      end\n    end\n\n    test \"returns default template\" do\n      identity_provider = insert(:identity_provider, templates: [])\n\n      template =\n        IdentityProviders.get_identity_provider_template!(identity_provider.id, :new_registration)\n\n      assert template == %{\n               Template.default_template(:new_registration)\n               | identity_provider_id: identity_provider.id,\n                 identity_provider: identity_provider,\n                 layout: IdentityProvider.template(identity_provider, :layout)\n             }\n    end\n\n    test \"returns identity provider template with a layout\" do\n      template =\n        build(:new_registration_template,\n          content: \"custom registration template\"\n        )\n\n      %IdentityProvider{templates: [template]} =\n        identity_provider = insert(:identity_provider, templates: [template])\n\n      assert IdentityProviders.get_identity_provider_template!(\n               identity_provider.id,\n               :new_registration\n             ) ==\n               %{\n                 template\n                 | layout: IdentityProvider.template(identity_provider, :layout),\n                   identity_provider: identity_provider\n               }\n    end\n  end\n\n  describe \"upsert_template/2\" do\n    test \"inserts with a default template\" do\n      identity_provider = insert(:identity_provider)\n\n      template =\n        IdentityProviders.get_identity_provider_template!(identity_provider.id, :new_registration)\n\n      assert {:ok, template} =\n               IdentityProviders.upsert_template(template, %{content: \"new content\"})\n\n      assert Repo.reload(template)\n    end\n\n    test \"updates with an existing template\" do\n      identity_provider = insert(:identity_provider)\n      template = insert(:new_registration_template, identity_provider: identity_provider)\n\n      assert {:ok, template} =\n               IdentityProviders.upsert_template(template, %{content: \"new content\"})\n\n      assert Repo.reload(template)\n    end\n  end\n\n  describe \"delete_identity_provider_template!/2\" do\n    test \"raises an error with unexisting identity provider\" do\n      identity_provider_id = SecureRandom.uuid()\n\n      assert_raise Ecto.NoResultsError, fn ->\n        IdentityProviders.delete_identity_provider_template!(identity_provider_id, :unexisting)\n      end\n    end\n\n    test \"raises an error with unexisting template\" do\n      identity_provider_id = insert(:identity_provider).id\n\n      assert_raise Ecto.NoResultsError, fn ->\n        IdentityProviders.delete_identity_provider_template!(identity_provider_id, :unexisting)\n      end\n    end\n\n    test \"returns an error if template is default\" do\n      identity_provider = insert(:identity_provider, templates: [])\n\n      assert_raise Ecto.NoResultsError, fn ->\n        IdentityProviders.delete_identity_provider_template!(\n          identity_provider.id,\n          :new_registration\n        )\n      end\n    end\n\n    test \"returns identity provider template with a layout\" do\n      template =\n        build(:new_registration_template,\n          content: \"custom registration template\"\n        )\n\n      %IdentityProvider{templates: [template]} =\n        identity_provider = insert(:identity_provider, templates: [template])\n\n      default_template = %{\n        Template.default_template(:new_registration)\n        | identity_provider_id: identity_provider.id\n      }\n\n      reseted_template =\n        IdentityProviders.delete_identity_provider_template!(\n          identity_provider.id,\n          :new_registration\n        )\n\n      assert reseted_template.default == true\n      assert reseted_template.type == \"new_registration\"\n      assert reseted_template.content == default_template.content\n\n      assert Repo.get_by(Template, id: template.id) == nil\n    end\n  end\n\n  describe \"backends\" do\n    import BorutaIdentity.IdentityProvidersFixtures\n\n    @invalid_attrs %{name: nil, type: \"bad type\"}\n\n    test \"list_backends/0 returns all backends\" do\n      backend = backend_fixture()\n      assert IdentityProviders.list_backends() |> Enum.member?(backend)\n    end\n\n    test \"get_backend!/1 returns the backend with given id\" do\n      backend = backend_fixture()\n      assert IdentityProviders.get_backend!(backend.id) == backend\n    end\n\n    @tag :skip\n    test \"get_backend_roles/1\"\n\n    test \"create_backend/1 with valid data creates a backend\" do\n      valid_attrs = %{name: \"some name\", type: \"Elixir.BorutaIdentity.Accounts.Internal\"}\n\n      assert {:ok, %Backend{} = backend} = IdentityProviders.create_backend(valid_attrs)\n      assert backend.name == \"some name\"\n      assert backend.type == \"Elixir.BorutaIdentity.Accounts.Internal\"\n    end\n\n    test \"create_backend/1 with valid argon2 password hashing opts creates a backend\" do\n      valid_attrs = %{\n        name: \"some name\",\n        type: \"Elixir.BorutaIdentity.Accounts.Internal\",\n        password_hashing_alg: \"argon2\",\n        password_hashing_opts: %{\n          \"salt_len\" => 16,\n          \"t_cost\" => 8,\n          \"m_cost\" => 16,\n          \"parallelism\" => 2,\n          \"format\" => \"encoded\",\n          \"hashlen\" => 32,\n          \"argon2_type\" => 2\n        }\n      }\n\n      assert {:ok, %Backend{} = backend} = IdentityProviders.create_backend(valid_attrs)\n      assert backend.name == \"some name\"\n      assert backend.type == \"Elixir.BorutaIdentity.Accounts.Internal\"\n      assert backend.password_hashing_alg == \"argon2\"\n\n      assert backend.password_hashing_opts == %{\n               \"argon2_type\" => 2,\n               \"format\" => \"encoded\",\n               \"hashlen\" => 32,\n               \"m_cost\" => 16,\n               \"parallelism\" => 2,\n               \"salt_len\" => 16,\n               \"t_cost\" => 8\n             }\n    end\n\n    test \"create_backend/1 with valid bcrypt password hashing opts creates a backend\" do\n      valid_attrs = %{\n        name: \"some name\",\n        type: \"Elixir.BorutaIdentity.Accounts.Internal\",\n        password_hashing_alg: \"bcrypt\",\n        password_hashing_opts: %{\n          \"log_rounds\" => 12,\n          \"legacy\" => false\n        }\n      }\n\n      assert {:ok, %Backend{} = backend} = IdentityProviders.create_backend(valid_attrs)\n      assert backend.name == \"some name\"\n      assert backend.type == \"Elixir.BorutaIdentity.Accounts.Internal\"\n      assert backend.password_hashing_alg == \"bcrypt\"\n      assert backend.password_hashing_opts == %{\"legacy\" => false, \"log_rounds\" => 12}\n    end\n\n    test \"create_backend/1 set as default will override other backends default attribute\" do\n      other_backend = backend_fixture(%{is_default: true})\n\n      valid_attrs = %{\n        name: \"some name\",\n        type: \"Elixir.BorutaIdentity.Accounts.Internal\",\n        is_default: true\n      }\n\n      assert {:ok, %Backend{} = backend} = IdentityProviders.create_backend(valid_attrs)\n      assert backend.is_default\n      refute Repo.reload!(other_backend).is_default\n    end\n\n    test \"create_backend/1 with invalid argon2 password hashing opts returns an error changeset\" do\n      valid_attrs = %{\n        name: \"some name\",\n        type: \"Elixir.BorutaIdentity.Accounts.Internal\",\n        password_hashing_alg: \"argon2\",\n        password_hashing_opts: %{\n          \"salt_len\" => true,\n          \"t_cost\" => true,\n          \"m_cost\" => true,\n          \"parallelism\" => true,\n          \"format\" => true,\n          \"hashlen\" => true,\n          \"argon2_type\" => true\n        }\n      }\n\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected Number but got Boolean. at #/t_cost\", []},\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected Number but got Boolean. at #/salt_len\", []},\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected Number but got Boolean. at #/parallelism\", []},\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected Number but got Boolean. at #/m_cost\", []},\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected Number but got Boolean. at #/hashlen\", []},\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected String but got Boolean. at #/format\", []},\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected Number but got Boolean. at #/argon2_type\", []}\n                ]\n              }} = IdentityProviders.create_backend(valid_attrs)\n    end\n\n    test \"create_backend/1 with invalid pbkdf2 password hashing opts returns an error changeset\" do\n      valid_attrs = %{\n        name: \"some name\",\n        type: \"Elixir.BorutaIdentity.Accounts.Internal\",\n        password_hashing_alg: \"pbkdf2\",\n        password_hashing_opts: %{\n          \"salt_len\" => true,\n          \"format\" => true,\n          \"digest\" => true,\n          \"length\" => true\n        }\n      }\n\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected Number but got Boolean. at #/salt_len\", []},\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected Number but got Boolean. at #/length\", []},\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected String but got Boolean. at #/format\", []},\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected String but got Boolean. at #/digest\", []}\n                ]\n              }} = IdentityProviders.create_backend(valid_attrs)\n    end\n\n    test \"create_backend/1 with invalid bcrypt password hashing opts returns an error changeset\" do\n      valid_attrs = %{\n        name: \"some name\",\n        type: \"Elixir.BorutaIdentity.Accounts.Internal\",\n        password_hashing_alg: \"bcrypt\",\n        password_hashing_opts: %{\n          \"log_rounds\" => true,\n          \"legacy\" => \"invalid\"\n        }\n      }\n\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected Number but got Boolean. at #/log_rounds\", []},\n                  password_hashing_opts:\n                    {\"Type mismatch. Expected Boolean but got String. at #/legacy\", []}\n                ]\n              }} = IdentityProviders.create_backend(valid_attrs)\n    end\n\n    test \"create_backend/1 with invalid data returns error changeset\" do\n      assert {:error, %Ecto.Changeset{}} = IdentityProviders.create_backend(@invalid_attrs)\n    end\n\n    test \"create_backend/1 with invalid metadata_fields returns an error changeset\" do\n      attrs = Map.put(@valid_attrs, :metadata_fields, [%{\"valid\" => false}])\n      assert {:error, %Ecto.Changeset{errors: errors}} = IdentityProviders.create_backend(attrs)\n      assert errors[:metadata_fields]\n\n      attrs = Map.put(@valid_attrs, :metadata_fields, %{\"valid\" => false})\n      assert {:error, %Ecto.Changeset{errors: errors}} = IdentityProviders.create_backend(attrs)\n      assert errors[:metadata_fields]\n    end\n\n    test \"create_backend/1 with valid metadata_fields creates a backend\" do\n      metadata_fields = [%{\"attribute_name\" => \"attribute value\"}]\n      attrs = Map.put(@valid_attrs, :metadata_fields, metadata_fields)\n\n      assert {:ok, backend} = IdentityProviders.create_backend(attrs)\n\n      assert backend.metadata_fields == metadata_fields\n    end\n\n    test \"create_backend/1 with invalid federated_servers returns an error changeset\" do\n      federated_servers = [%{}]\n      attrs = Map.put(@valid_attrs, :federated_servers, federated_servers)\n\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [\n                  federated_servers:\n                    {\"Required properties name, client_id, client_secret, base_url were not present. at #\",\n                     []}\n                ]\n              }} = IdentityProviders.create_backend(attrs)\n    end\n\n    test \"create_backend/1 with valid federated_servers creates a backend\" do\n      federated_servers = [\n        %{\n          \"name\" => \"name\",\n          \"client_id\" => \"client_id\",\n          \"client_secret\" => \"client_secret\",\n          \"base_url\" => \"https://host.test\",\n          \"userinfo_path\" => \"/userinfo\",\n          \"authorize_path\" => \"/authorize\",\n          \"token_path\" => \"/token\"\n        }\n      ]\n\n      attrs = Map.put(@valid_attrs, :federated_servers, federated_servers)\n\n      assert {:ok, backend} = IdentityProviders.create_backend(attrs)\n\n      assert backend.federated_servers == federated_servers\n    end\n\n    test \"update_backend/2 with valid data updates the backend\" do\n      backend = backend_fixture()\n      update_attrs = %{name: \"some updated name\"}\n\n      assert {:ok, %Backend{} = backend} = IdentityProviders.update_backend(backend, update_attrs)\n      assert backend.name == \"some updated name\"\n    end\n\n    test \"update_backend/2 stop associated ldap connection pool\" do\n      backend = insert(:ldap_backend)\n      update_attrs = %{ldap_pool_size: 3}\n\n      BorutaIdentity.LdapRepoMock\n      |> stub(:open, fn host, _opts ->\n        assert host == backend.ldap_host\n\n        {:ok, SecureRandom.uuid()}\n      end)\n\n      {:ok, ldap_pool_pid} = Ldap.start_link(backend)\n\n      assert {:ok, %Backend{}} = IdentityProviders.update_backend(backend, update_attrs)\n      refute Process.alive?(ldap_pool_pid)\n    end\n\n    test \"update_backend/2 cannot remove default\" do\n      backend = backend_fixture(%{is_default: true})\n      update_attrs = %{name: \"some updated name\", is_default: false}\n\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [is_default: {\"There must be at least one default backend.\", []}]\n              }} = IdentityProviders.update_backend(backend, update_attrs)\n    end\n\n    test \"update_backend/2 other backends default attribute\" do\n      other_backend = backend_fixture(%{is_default: true})\n      backend = backend_fixture()\n      update_attrs = %{name: \"some updated name\", is_default: true}\n\n      assert {:ok, %Backend{} = backend} = IdentityProviders.update_backend(backend, update_attrs)\n      assert backend.is_default\n      refute Repo.reload!(other_backend).is_default\n    end\n\n    test \"update_backend/2 with invalid data returns error changeset\" do\n      backend = backend_fixture()\n\n      assert {:error, %Ecto.Changeset{}} =\n               IdentityProviders.update_backend(backend, @invalid_attrs)\n\n      assert backend == IdentityProviders.get_backend!(backend.id)\n    end\n\n    test \"delete_backend/1 deletes the backend\" do\n      backend = backend_fixture()\n      assert {:ok, %Backend{}} = IdentityProviders.delete_backend(backend)\n      assert_raise Ecto.NoResultsError, fn -> IdentityProviders.get_backend!(backend.id) end\n    end\n\n    test \"delete_backend/1 can't delete a default backend\" do\n      assert {:error,\n              %Ecto.Changeset{\n                errors: [is_default: {\"Deleting a default backend is prohibited.\", []}]\n              }} = IdentityProviders.delete_backend(Backend.default!())\n\n      assert Backend.default!()\n    end\n\n    test \"delete_backend/1 stop the associated ldap connection pool\" do\n      backend = insert(:ldap_backend)\n\n      BorutaIdentity.LdapRepoMock\n      |> stub(:open, fn host, _opts ->\n        assert host == backend.ldap_host\n\n        {:ok, SecureRandom.uuid()}\n      end)\n\n      {:ok, ldap_pool_pid} = Ldap.start_link(backend)\n\n      assert {:ok, %Backend{}} = IdentityProviders.delete_backend(backend)\n      refute Process.alive?(ldap_pool_pid)\n      assert_raise Ecto.NoResultsError, fn -> IdentityProviders.get_backend!(backend.id) end\n    end\n\n    test \"change_backend/1 returns a backend changeset\" do\n      backend = backend_fixture()\n      assert %Ecto.Changeset{} = IdentityProviders.change_backend(backend)\n    end\n  end\n\n  describe \"get_backend_email_template!/2\" do\n    test \"raises an error with unexisting identity provider\" do\n      backend_id = SecureRandom.uuid()\n\n      assert_raise Ecto.NoResultsError, fn ->\n        IdentityProviders.get_backend_email_template!(backend_id, :unexisting)\n      end\n    end\n\n    test \"raises an error with unexisting template\" do\n      backend_id = insert(:backend).id\n\n      assert_raise Ecto.NoResultsError, fn ->\n        IdentityProviders.get_backend_email_template!(backend_id, :unexisting)\n      end\n    end\n\n    test \"returns default template\" do\n      backend = insert(:backend, email_templates: [])\n\n      template =\n        IdentityProviders.get_backend_email_template!(backend.id, :reset_password_instructions)\n\n      assert template == %{\n               EmailTemplate.default_template(:reset_password_instructions)\n               | backend_id: backend.id,\n                 backend: backend\n             }\n    end\n\n    test \"returns backend email template with a layout\" do\n      template =\n        build(:reset_password_instructions_email_template,\n          txt_content: \"custom reset password instructions template\"\n        )\n\n      %Backend{email_templates: [template]} =\n        backend = insert(:backend, email_templates: [template])\n\n      assert IdentityProviders.get_backend_email_template!(\n               backend.id,\n               :reset_password_instructions\n             ) ==\n               %{template | backend: backend}\n    end\n  end\n\n  describe \"upsert_email_template/2\" do\n    test \"inserts with a default template\" do\n      backend = insert(:backend)\n\n      template =\n        IdentityProviders.get_backend_email_template!(backend.id, :reset_password_instructions)\n\n      assert {:ok, template} =\n               IdentityProviders.upsert_email_template(template, %{txt_content: \"new txt content\"})\n\n      assert Repo.reload(template)\n    end\n\n    test \"updates with an existing template\" do\n      backend = insert(:backend)\n      template = insert(:reset_password_instructions_email_template, backend: backend)\n\n      assert {:ok, template} =\n               IdentityProviders.upsert_email_template(template, %{txt_content: \"new content\"})\n\n      assert Repo.reload(template)\n    end\n  end\n\n  describe \"delete_email_template!/2\" do\n    test \"raises an error with unexisting identity provider\" do\n      backend_id = SecureRandom.uuid()\n\n      assert_raise Ecto.NoResultsError, fn ->\n        IdentityProviders.delete_email_template!(backend_id, :unexisting)\n      end\n    end\n\n    test \"raises an error with unexisting template\" do\n      backend_id = insert(:backend).id\n\n      assert_raise Ecto.NoResultsError, fn ->\n        IdentityProviders.delete_email_template!(backend_id, :unexisting)\n      end\n    end\n\n    test \"returns an error if template is default\" do\n      backend = insert(:backend, email_templates: [])\n\n      assert_raise Ecto.NoResultsError, fn ->\n        IdentityProviders.delete_email_template!(\n          backend.id,\n          :reset_password_instructions\n        )\n      end\n    end\n\n    test \"returns identity provider template with a layout\" do\n      template =\n        build(:reset_password_instructions_email_template,\n          txt_content: \"custom registration template\"\n        )\n\n      %Backend{email_templates: [template]} =\n        backend = insert(:backend, email_templates: [template])\n\n      default_template = %{\n        EmailTemplate.default_template(:reset_password_instructions)\n        | backend_id: backend.id\n      }\n\n      reseted_template =\n        IdentityProviders.delete_email_template!(\n          backend.id,\n          :reset_password_instructions\n        )\n\n      assert reseted_template.default == true\n      assert reseted_template.type == \"reset_password_instructions\"\n      assert reseted_template.txt_content == default_template.txt_content\n\n      assert Repo.get_by(EmailTemplate, id: template.id) == nil\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity/resource_owners_test.exs",
    "content": "defmodule BorutaIdentity.ResourceOwnersTest do\n  use BorutaIdentity.DataCase\n\n  import BorutaIdentity.AccountsFixtures\n\n  alias Boruta.Ecto.Admin\n  alias Boruta.Oauth.ResourceOwner\n  alias BorutaIdentity.Accounts.UserRole\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.Organizations.OrganizationUser\n  alias BorutaIdentity.Repo\n  alias BorutaIdentity.ResourceOwners\n\n  doctest BorutaIdentity\n\n  @valid_username unique_user_email()\n  @valid_password valid_user_password()\n\n  describe \"get_by/1\" do\n    test \"returns an user by username\" do\n      username = @valid_username\n\n      user =\n        user_fixture(%{\n          email: username,\n          password: @valid_password,\n          backend: Backend.default!()\n        })\n\n      {:ok, result} = ResourceOwners.get_by(username: username)\n\n      user_id = user.id\n\n      assert %ResourceOwner{sub: ^user_id, username: ^username, extra_claims: %{user: _user}} =\n               result\n    end\n\n    test \"returns an user by sub\" do\n      user =\n        user_fixture(%{\n          email: @valid_username,\n          password: @valid_password,\n          backend: Backend.default!()\n        })\n\n      {:ok, result} = ResourceOwners.get_by(sub: user.id, scope: \"\")\n\n      user_id = user.id\n      user_username = user.username\n      assert %ResourceOwner{sub: ^user_id, username: ^user_username} = result\n    end\n\n    test \"returns nil when username do not exists\" do\n      user_fixture(%{\n        email: @valid_username,\n        password: @valid_password,\n        backend: Backend.default!()\n      })\n\n      assert ResourceOwners.get_by(username: \"other\") == {:error, \"Invalid username or password.\"}\n    end\n  end\n\n  describe \"#check_password/2\" do\n    test \"returns ok if password match\" do\n      username = @valid_username\n      backend = Backend.default!()\n\n      user =\n        user_fixture(%{\n          email: username,\n          password: @valid_password,\n          backend: backend\n        })\n\n      {:ok, impl_user} =\n        apply(Backend.implementation(backend), :get_user, [backend, %{email: username}])\n\n      resource_owner = %ResourceOwner{\n        sub: user.id,\n        username: user.username,\n        extra_claims: %{user: impl_user}\n      }\n\n      assert ResourceOwners.check_password(resource_owner, @valid_password) == :ok\n    end\n\n    test \"returns an error if password do not match\" do\n      user =\n        user_fixture(%{\n          email: @valid_username,\n          password: @valid_password,\n          backend: Backend.default!()\n        })\n\n      resource_owner = %ResourceOwner{sub: user.id}\n\n      assert ResourceOwners.check_password(resource_owner, \"wrong password\") ==\n               {:error, \"Invalid username or password.\"}\n    end\n  end\n\n  describe \"authorized_scopes/1\" do\n    test \"returns an empty array\" do\n      user = user_fixture(%{backend: Backend.default!()})\n      resource_owner = %ResourceOwner{sub: user.id}\n      assert ResourceOwners.authorized_scopes(resource_owner) == []\n    end\n\n    test \"return user associated scopes with authorized scopes\" do\n      %{id: id} = user = user_fixture(%{backend: Backend.default!()})\n      {:ok, scope} = Admin.create_scope(%{name: \"scope:scope\"})\n      insert(:user_authorized_scope, user_id: id, scope_id: scope.id)\n\n      resource_owner = %ResourceOwner{sub: user.id}\n\n      name = scope.name\n      assert [%Boruta.Oauth.Scope{name: ^name}] = ResourceOwners.authorized_scopes(resource_owner)\n    end\n\n    test \"return user associated scopes with roles\" do\n      %{id: id} = user = user_fixture(%{backend: Backend.default!()})\n      {:ok, scope} = Admin.create_scope(%{name: \"scope:scope\"})\n      role = insert(:role)\n      insert(:role_scope, role_id: role.id, scope_id: scope.id)\n      insert(:user_role, user_id: id, role_id: role.id)\n\n      resource_owner = %ResourceOwner{sub: user.id}\n\n      name = scope.name\n      assert [%Boruta.Oauth.Scope{name: ^name}] = ResourceOwners.authorized_scopes(resource_owner)\n    end\n  end\n\n  describe \"claims/2\" do\n    test \"returns user roles with profile in scope\" do\n      user = user_fixture()\n      role = BorutaIdentity.Factory.insert(:role)\n\n      Repo.insert(%UserRole{user_id: user.id, role_id: role.id})\n\n      role_name = role.name\n      assert %{\"roles\" => [^role_name]} = ResourceOwners.claims(%ResourceOwner{sub: user.id}, \"profile\")\n    end\n\n    test \"returns user organizations with profile in scope\" do\n      user = user_fixture()\n      organization = BorutaIdentity.Factory.insert(:organization)\n\n      Repo.insert(%OrganizationUser{user_id: user.id, organization_id: organization.id})\n\n      organization_id = organization.id\n      organization_name = organization.name\n      organization_label = organization.label\n\n      assert %{\n               \"organizations\" => [\n                 %{\n                   \"id\" => ^organization_id,\n                   \"name\" => ^organization_name,\n                   \"label\" => ^organization_label\n                 }\n               ]\n             } = ResourceOwners.claims(%ResourceOwner{sub: user.id}, \"profile\")\n    end\n  end\n\n  describe \"metadata/2\" do\n    test \"returns user metadata\" do\n      user = user_fixture()\n\n      {:ok, backend} =\n        Ecto.Changeset.change(user.backend, %{\n          metadata_fields: [%{\"attribute_name\" => \"metadata\"}]\n        })\n        |> Repo.update()\n\n      user = %{user | backend: backend}\n\n      {:ok, user} =\n        Ecto.Changeset.change(user, %{metadata: %{\"metadata\" => \"true\"}}) |> Repo.update()\n\n      assert %{\"metadata\" => \"true\"} = ResourceOwners.metadata(user, \"\")\n    end\n\n    test \"filters user metadata\" do\n      user = user_fixture()\n\n      {:ok, backend} =\n        Ecto.Changeset.change(user.backend, %{\n          metadata_fields: [%{\"attribute_name\" => \"metadata\"}]\n        })\n        |> Repo.update()\n\n      user = %{user | backend: backend}\n\n      {:ok, user} =\n        Ecto.Changeset.change(user, %{metadata: %{\"filtered\" => \"true\", \"metadata\" => \"true\"}})\n        |> Repo.update()\n\n      assert %{\"metadata\" => \"true\"} = ResourceOwners.metadata(user, \"\")\n    end\n\n    test \"filters user metadata according to scopes\" do\n      user = user_fixture()\n\n      {:ok, backend} =\n        Ecto.Changeset.change(user.backend, %{\n          metadata_fields: [\n            %{\"attribute_name\" => \"without_scopes\"},\n            %{\"attribute_name\" => \"test_scope\", \"scopes\" => [\"test\"]},\n            %{\"attribute_name\" => \"other_scope\", \"scopes\" => [\"other\"]}\n          ]\n        })\n        |> Repo.update()\n\n      user = %{user | backend: backend}\n\n      {:ok, user} =\n        Ecto.Changeset.change(user, %{\n          metadata: %{\"without_scopes\" => \"true\", \"test_scope\" => \"true\", \"other_scope\" => \"true\"}\n        })\n        |> Repo.update()\n\n      assert %{\"without_scopes\" => \"true\", \"test_scope\" => \"true\"} =\n               ResourceOwners.metadata(user, \"test\")\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity/totp_test.exs",
    "content": "defmodule BorutaIdentity.TotpTest do\n  defmodule DummyTotpRegistrationApplication do\n    @behaviour BorutaIdentity.TotpRegistrationApplication\n\n    @impl BorutaIdentity.TotpRegistrationApplication\n    def totp_registration_initialized(context, totp_secret, template) do\n      {:totp_registration_initialized, context, totp_secret, template}\n    end\n\n    @impl BorutaIdentity.TotpRegistrationApplication\n    def totp_registration_error(context, error) do\n      {:totp_registration_error, context, error}\n    end\n\n    @impl BorutaIdentity.TotpRegistrationApplication\n    def totp_registration_success(context, user) do\n      {:totp_registration_success, context, user}\n    end\n  end\n\n  defmodule DummyTotpAuthenticationApplication do\n    @behaviour BorutaIdentity.TotpAuthenticationApplication\n\n    @impl BorutaIdentity.TotpAuthenticationApplication\n    def totp_initialized(context, template) do\n      {:totp_initialized, context, template}\n    end\n\n    @impl BorutaIdentity.TotpAuthenticationApplication\n    def totp_not_required(context) do\n      {:totp_not_required, context}\n    end\n\n    @impl BorutaIdentity.TotpAuthenticationApplication\n    def totp_registration_missing(context) do\n      {:totp_registration_missing, context}\n    end\n\n    @impl BorutaIdentity.TotpAuthenticationApplication\n    def totp_authenticated(context, current_user) do\n      {:totp_authenticated, context, current_user}\n    end\n\n    @impl BorutaIdentity.TotpAuthenticationApplication\n    def totp_authentication_failure(context, error) do\n      {:totp_authentication_failure, context, error}\n    end\n  end\n\n  defmodule HotpTest do\n    use ExUnit.Case\n\n    alias BorutaIdentity.Totp.Hotp\n\n    test \"returns an htop given empty params\" do\n      assert Hotp.generate_hotp(\"\", 0) == \"328482\"\n    end\n\n    test \"returns an hotp with RFC examples\" do\n      assert Hotp.generate_hotp(\"12345678901234567890\", 0) == \"755224\"\n      assert Hotp.generate_hotp(\"12345678901234567890\", 1) == \"287082\"\n      assert Hotp.generate_hotp(\"12345678901234567890\", 2) == \"359152\"\n      assert Hotp.generate_hotp(\"12345678901234567890\", 3) == \"969429\"\n      assert Hotp.generate_hotp(\"12345678901234567890\", 4) == \"338314\"\n      assert Hotp.generate_hotp(\"12345678901234567890\", 5) == \"254676\"\n      assert Hotp.generate_hotp(\"12345678901234567890\", 6) == \"287922\"\n      assert Hotp.generate_hotp(\"12345678901234567890\", 7) == \"162583\"\n      assert Hotp.generate_hotp(\"12345678901234567890\", 8) == \"399871\"\n      assert Hotp.generate_hotp(\"12345678901234567890\", 9) == \"520489\"\n    end\n  end\n\n  defmodule AdminTest do\n    use ExUnit.Case\n\n    alias BorutaIdentity.Totp.Admin\n\n    describe \"generate_totp/1\" do\n      test \"returns an error with a non base32 encoded secret\" do\n        assert :error = Admin.generate_totp(\"not base64 encoded secret\")\n      end\n\n      test \"returns a totp\" do\n        secret = Base.encode32(\"secret\", padding: false)\n\n        # TODO test only the presence until we have a timestamp provider\n        assert Admin.generate_totp(secret) |> String.length() == 6\n      end\n    end\n\n    describe \"check_totp/2\" do\n      test \"returns an error when totp invalid\" do\n        secret = Base.encode32(\"secret\", padding: false)\n\n        assert {:error, \"Given TOTP is invalid.\"} = Admin.check_totp(\"invalid\", secret)\n      end\n\n      test \"returns an error when secret is bad encoded invalid\" do\n        secret = \"bad encoding\"\n\n        assert {:error, \"Given TOTP is invalid.\"} = Admin.check_totp(\"whatever\", secret)\n      end\n    end\n\n    describe \"generate_secret/0\" do\n      test \"return a random secret\" do\n        # secret is hashed from an uuid\n        assert {:ok, _} =\n                 Admin.generate_secret()\n                 |> Base.decode32!(padding: false)\n                 |> Ecto.UUID.cast()\n      end\n    end\n  end\n\n  use BorutaIdentity.DataCase\n\n  alias BorutaIdentity.Accounts.IdentityProviderError\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Totp\n\n  describe \"initialize_totp_registration/3\" do\n    setup do\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider:\n            build(\n              :identity_provider,\n              totpable: true\n            )\n        )\n\n      {:ok, client_id: client_identity_provider.client_id}\n    end\n\n    test \"raises an error\", %{client_id: client_id} do\n      assert_raise BorutaIdentity.TotpError, fn ->\n        Totp.initialize_totp_registration(\n          :context,\n          client_id,\n          false,\n          %User{totp_registered_at: DateTime.utc_now()},\n          DummyTotpRegistrationApplication\n        )\n      end\n    end\n\n    test \"returns a secret and the registration template when not registered\", %{client_id: client_id} do\n      assert {:totp_registration_initialized, :context, totp_secret, template} =\n               Totp.initialize_totp_registration(\n                 :context,\n                 client_id,\n                 false,\n                 %User{},\n                 DummyTotpRegistrationApplication\n               )\n\n      # secret is hashed from an uuid\n      assert {:ok, _} =\n               totp_secret\n               |> Base.decode32!(padding: false)\n               |> Ecto.UUID.cast()\n\n      assert Regex.match?(\n               ~r/Add TOTP authentication from an authenticator/,\n               template.content\n             )\n    end\n\n    test \"returns a secret and the registration template when totp authenticated\", %{client_id: client_id} do\n      assert {:totp_registration_initialized, :context, totp_secret, template} =\n               Totp.initialize_totp_registration(\n                 :context,\n                 client_id,\n                 true,\n                 %User{},\n                 DummyTotpRegistrationApplication\n               )\n\n      # secret is hashed from an uuid\n      assert {:ok, _} =\n               totp_secret\n               |> Base.decode32!(padding: false)\n               |> Ecto.UUID.cast()\n\n      assert Regex.match?(\n               ~r/Add TOTP authentication from an authenticator/,\n               template.content\n             )\n    end\n  end\n\n  describe \"register_totp/4\" do\n    setup do\n      client_identity_provider =\n        BorutaIdentity.Factory.insert(:client_identity_provider,\n          identity_provider:\n            build(\n              :identity_provider,\n              totpable: true\n            )\n        )\n\n      user = BorutaIdentity.Factory.insert(:user)\n\n      {:ok, client_id: client_identity_provider.client_id, user: user}\n    end\n\n    test \"returns an error with registration template if bad secret format\", %{\n      client_id: client_id,\n      user: current_user\n    } do\n      totp_params = %{\n        totp_code: \"totp_code\",\n        totp_secret: \"bad_totp_secret\"\n      }\n\n      assert {:totp_registration_error, :context, error} =\n               Totp.register_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 totp_params,\n                 DummyTotpRegistrationApplication\n               )\n\n      assert error.message == \"Given TOTP is invalid.\"\n\n      assert Regex.match?(\n               ~r/Add TOTP authentication from an authenticator/,\n               error.template.content\n             )\n    end\n\n    test \"returns an error with registration template if totp is invalid (bad code)\", %{\n      client_id: client_id,\n      user: current_user\n    } do\n      totp_params = %{\n        totp_code: \"totp_code\",\n        totp_secret: Totp.Admin.generate_secret()\n      }\n\n      assert {:totp_registration_error, :context, error} =\n               Totp.register_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 totp_params,\n                 DummyTotpRegistrationApplication\n               )\n\n      assert error.message == \"Given TOTP is invalid.\"\n\n      assert Regex.match?(\n               ~r/Add TOTP authentication from an authenticator/,\n               error.template.content\n             )\n    end\n\n    # NOTE can be a flaky test, mind about time provider\n    test \"successes when TOTP is valid\", %{\n      client_id: client_id,\n      user: current_user\n    } do\n      secret = Totp.Admin.generate_secret()\n\n      totp_params = %{\n        totp_code: Totp.Admin.generate_totp(secret),\n        totp_secret: secret\n      }\n\n      assert {:totp_registration_success, :context, user} =\n               Totp.register_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 totp_params,\n                 DummyTotpRegistrationApplication\n               )\n\n      assert user.totp_registered_at\n      assert user.totp_secret == secret\n    end\n  end\n\n  describe \"initialize_totp/4\" do\n    setup do\n      client_id =\n        for totpable <- [true, false], enforce_totp <- [true, false] do\n          current_client_id =\n            BorutaIdentity.Factory.insert(:client_identity_provider,\n              identity_provider:\n                build(\n                  :identity_provider,\n                  totpable: totpable,\n                  enforce_totp: enforce_totp\n                )\n            ).client_id\n\n          label =\n            case {totpable, enforce_totp} do\n              {true, true} -> :totpable_enforce_totp\n              {true, false} -> :totpable\n              {false, true} -> :enforce_totp\n              {false, false} -> :basic\n            end\n\n          {label, current_client_id}\n        end\n        |> Enum.into(%{})\n\n      user = BorutaIdentity.Factory.insert(:user)\n\n      registered_user =\n        BorutaIdentity.Factory.insert(:user, totp_registered_at: DateTime.utc_now())\n\n      {:ok, client_id: client_id, user: user, registered_user: registered_user}\n    end\n\n    test \"returns not required if identity provider is basic\", %{\n      client_id: %{basic: client_id},\n      user: current_user\n    } do\n      assert {:totp_not_required, :context} =\n               Totp.initialize_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 DummyTotpAuthenticationApplication\n               )\n    end\n\n    test \"returns registration missing if identity provider enforces totp\", %{\n      client_id: %{enforce_totp: client_id},\n      user: current_user\n    } do\n      assert {:totp_registration_missing, :context} =\n               Totp.initialize_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 DummyTotpAuthenticationApplication\n               )\n    end\n\n    test \"returns authentication template if identity provider enforces totp and user registred\",\n         %{\n           client_id: %{enforce_totp: client_id},\n           registered_user: current_user\n         } do\n      assert {:totp_initialized, :context, template} =\n               Totp.initialize_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 DummyTotpAuthenticationApplication\n               )\n\n      assert Regex.match?(\n               ~r/Provide the TOTP code from your authenticator/,\n               template.content\n             )\n    end\n\n    test \"returns not required if identity provider is totpable\", %{\n      client_id: %{totpable: client_id},\n      user: current_user\n    } do\n      assert {:totp_not_required, :context} =\n               Totp.initialize_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 DummyTotpAuthenticationApplication\n               )\n    end\n\n    test \"returns authentication template if identity provider is totpable and user registered\",\n         %{\n           client_id: %{totpable: client_id},\n           registered_user: current_user\n         } do\n      assert {:totp_initialized, :context, template} =\n               Totp.initialize_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 DummyTotpAuthenticationApplication\n               )\n\n      assert Regex.match?(\n               ~r/Provide the TOTP code from your authenticator/,\n               template.content\n             )\n    end\n\n    test \"returns registration missing if identity provider totpable and enforces totp\", %{\n      client_id: %{totpable_enforce_totp: client_id},\n      user: current_user\n    } do\n      assert {:totp_registration_missing, :context} =\n               Totp.initialize_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 DummyTotpAuthenticationApplication\n               )\n    end\n\n    test \"returns authentication template if identity provider is totpable, enforces totp and user registered\",\n         %{\n           client_id: %{totpable_enforce_totp: client_id},\n           registered_user: current_user\n         } do\n      assert {:totp_initialized, :context, template} =\n               Totp.initialize_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 DummyTotpAuthenticationApplication\n               )\n\n      assert Regex.match?(\n               ~r/Provide the TOTP code from your authenticator/,\n               template.content\n             )\n    end\n  end\n\n  describe \"authenticate_totp/5\" do\n    setup do\n      client_id =\n        for totpable <- [true, false], enforce_totp <- [true, false] do\n          current_client_id =\n            BorutaIdentity.Factory.insert(:client_identity_provider,\n              identity_provider:\n                build(\n                  :identity_provider,\n                  totpable: totpable,\n                  enforce_totp: enforce_totp\n                )\n            ).client_id\n\n          label =\n            case {totpable, enforce_totp} do\n              {true, true} -> :totpable_enforce_totp\n              {true, false} -> :totpable\n              {false, true} -> :enforce_totp\n              {false, false} -> :basic\n            end\n\n          {label, current_client_id}\n        end\n        |> Enum.into(%{})\n\n      user = BorutaIdentity.Factory.insert(:user)\n\n      registered_user =\n        BorutaIdentity.Factory.insert(:user,\n          totp_registered_at: DateTime.utc_now(),\n          totp_secret: Totp.Admin.generate_secret()\n        )\n\n      {:ok, client_id: client_id, user: user, registered_user: registered_user}\n    end\n\n    test \"raises an error if identity provider is basic\", %{\n      client_id: %{basic: client_id},\n      user: current_user\n    } do\n      totp_params = %{}\n\n      assert_raise IdentityProviderError, fn ->\n        Totp.authenticate_totp(\n          :context,\n          client_id,\n          current_user,\n          totp_params,\n          DummyTotpAuthenticationApplication\n        )\n      end\n    end\n\n    test \"raises an error if identity provider enforces totp\", %{\n      client_id: %{enforce_totp: client_id},\n      user: current_user\n    } do\n      totp_params = %{}\n\n      assert_raise IdentityProviderError, fn ->\n        Totp.authenticate_totp(\n          :context,\n          client_id,\n          current_user,\n          totp_params,\n          DummyTotpAuthenticationApplication\n        )\n      end\n    end\n\n    test \"returns not required if identity provider is totpable\", %{\n      client_id: %{totpable: client_id},\n      user: current_user\n    } do\n      totp_params = %{}\n\n      assert {:totp_not_required, :context} =\n               Totp.authenticate_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 totp_params,\n                 DummyTotpAuthenticationApplication\n               )\n    end\n\n    test \"returns authentication failure if identity provider is totpable, and user registered\",\n         %{\n           client_id: %{totpable: client_id},\n           registered_user: current_user\n         } do\n      totp_params = %{\n        totp_code: \"bad code\"\n      }\n\n      assert {:totp_authentication_failure, :context, error} =\n               Totp.authenticate_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 totp_params,\n                 DummyTotpAuthenticationApplication\n               )\n\n      assert error.message == \"Given TOTP is invalid.\"\n\n      assert Regex.match?(\n               ~r/Provide the TOTP code from your authenticator/,\n               error.template.content\n             )\n    end\n\n    test \"authenticates if identity provider is totpable, user registered and valid totp\",\n         %{\n           client_id: %{totpable: client_id},\n           registered_user: current_user\n         } do\n      totp_params = %{\n        totp_code: Totp.Admin.generate_totp(current_user.totp_secret)\n      }\n\n      assert {:totp_authenticated, :context, %User{}} =\n               Totp.authenticate_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 totp_params,\n                 DummyTotpAuthenticationApplication\n               )\n    end\n\n    test \"returns registration missing if identity provider totpable and enforces totp\", %{\n      client_id: %{totpable_enforce_totp: client_id},\n      user: current_user\n    } do\n      totp_params = %{}\n\n      assert {:totp_registration_missing, :context} =\n               Totp.authenticate_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 totp_params,\n                 DummyTotpAuthenticationApplication\n               )\n    end\n\n    test \"returns an error if identity provider is totpable, enforces totp and user registered\",\n         %{\n           client_id: %{totpable_enforce_totp: client_id},\n           registered_user: current_user\n         } do\n      totp_params = %{}\n\n      assert {:totp_authentication_failure, :context, error} =\n               Totp.authenticate_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 totp_params,\n                 DummyTotpAuthenticationApplication\n               )\n\n      assert error.message == \"Given TOTP is invalid.\"\n\n      assert Regex.match?(\n               ~r/Provide the TOTP code from your authenticator/,\n               error.template.content\n             )\n    end\n\n    test \"authenticates if identity provider is totpable, enforces totp, user registered and totp valid\",\n         %{\n           client_id: %{totpable_enforce_totp: client_id},\n           registered_user: current_user\n         } do\n      totp_params = %{\n        totp_code: Totp.Admin.generate_totp(current_user.totp_secret)\n      }\n\n      assert {:totp_authenticated, :context, %User{}} =\n               Totp.authenticate_totp(\n                 :context,\n                 client_id,\n                 current_user,\n                 totp_params,\n                 DummyTotpAuthenticationApplication\n               )\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity/webauthn_test.exs",
    "content": "defmodule BorutaIdentity.WebauthnTest do\n  use BorutaIdentity.DataCase\n\n  import BorutaIdentity.Factory\n\n  alias BorutaIdentity.Webauthn\n\n  describe \"options/2\" do\n    test \"returns webauthn options\" do\n      user = insert(:user)\n\n      assert {:ok, %Webauthn.Options{\n        user: webauthn_user,\n        challenge: challenge,\n        publicKeyCredParams: %{alg: -7, type: \"public-key\"},\n        rp: %{id: \"localhost\"}\n      }} = Webauthn.options(user, true)\n\n      assert challenge\n      assert webauthn_user[:id] == user.id\n      assert webauthn_user[:displayName] == user.username\n    end\n  end\n\n  @tag :skip\n  test \"registration\"\n\n  @tag :skip\n  test \"authentication\"\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/concerns/authenticable_test.exs",
    "content": "defmodule BorutaIdentityWeb.AuthenticableTest do\n  use BorutaIdentityWeb.ConnCase, async: true\n\n  import BorutaIdentity.AccountsFixtures\n\n  alias BorutaIdentityWeb.Authenticable\n\n  setup %{conn: conn} do\n    conn =\n      conn\n      |> Map.replace!(:secret_key_base, BorutaIdentityWeb.Endpoint.config(:secret_key_base))\n      |> init_test_session(%{})\n\n    %{user: user_fixture(), conn: conn}\n  end\n\n  describe \"after_sign_in_path\" do\n    setup :with_a_request\n\n    test \"returns root path\", %{conn: conn} do\n      conn = Plug.Conn.fetch_query_params(conn)\n\n      assert Authenticable.after_sign_in_path(conn) == \"/\"\n    end\n\n    test \"returns session stored path if provided\", %{conn: conn, request: request} do\n      conn = %{conn|query_params: %{\"request\" => request}}\n\n      assert Authenticable.after_sign_in_path(conn) == \"/user_return_to\"\n    end\n  end\n\n  @tag :skip\n  test \"store_user_session/2\"\n\n  @tag :skip\n  test \"after_sign_in_path/2\"\n\n  @tag :skip\n  test \"after_registration_path/2\"\n\n  @tag :skip\n  test \"after_sign_out_path/2\"\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/controllers/choose_session_controller_test.exs",
    "content": "defmodule BorutaIdentityWeb.ChooseSessionControllerTest do\n  use BorutaIdentityWeb.ConnCase\n\n  import BorutaIdentity.AccountsFixtures\n\n  alias BorutaIdentity.Repo\n\n  describe \"GET /choose_session\" do\n    setup :with_a_request\n\n    test \"renders choose session template\", %{conn: conn, request: request} do\n      conn =\n        conn\n        |> log_in(user_fixture())\n        |> get(Routes.choose_session_path(conn, :index, %{request: request}))\n\n      assert html_response(conn, 200) =~ \"Continue ?\"\n    end\n\n    test \"redirect to log in if identity provider disabled choose_session\", %{\n      conn: conn,\n      identity_provider: identity_provider,\n      request: request\n    } do\n      identity_provider |> Ecto.Changeset.change(choose_session: false) |> Repo.update()\n      conn =\n        conn\n        |> log_in(user_fixture())\n        |> get(Routes.choose_session_path(conn, :index, %{request: request}))\n\n      assert redirected_to(conn) =~ Routes.user_session_path(conn, :new, request: request)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/controllers/page_controller_test.exs",
    "content": "defmodule BorutaIdentityWeb.PageControllerTest do\n  use BorutaIdentityWeb.ConnCase\n\n  # test \"GET /\", %{conn: conn} do\n  #   conn = get(conn, \"/\")\n  #   assert html_response(conn, 200) =~ \"Welcome to Phoenix!\"\n  # end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/controllers/totp_controller_test.exs",
    "content": "defmodule BorutaIdentityWeb.TotpControllerTest do\n  use BorutaIdentityWeb.ConnCase, async: true\n\n  import BorutaIdentity.AccountsFixtures\n  import BorutaIdentityWeb.Authenticable,\n    only: [\n      get_user_session: 1\n    ]\n\n  alias BorutaIdentity.Accounts.IdentityProviderError\n  alias BorutaIdentity.Repo\n  alias BorutaIdentity.Totp\n\n  setup :with_a_request\n\n  setup %{identity_provider: identity_provider} do\n    {:ok, user} =\n      user_fixture(%{backend: identity_provider.backend})\n      |> Ecto.Changeset.change(confirmed_at: DateTime.utc_now())\n      |> Repo.update()\n\n    %{user: user}\n  end\n\n  describe \"GET /users/totp_registration\" do\n    test \"redirects to log in\", %{\n      conn: conn,\n      request: request\n    } do\n      conn =\n        conn\n        |> get(Routes.totp_path(conn, :new, request: request))\n\n      assert redirected_to(conn) =~ Routes.user_session_path(conn, :new)\n    end\n\n    test \"raises if identity provider not totpable\", %{\n      conn: conn,\n      request: request,\n      user: user\n    } do\n      assert_raise IdentityProviderError, fn ->\n        conn\n        |> log_in(user)\n        |> get(Routes.totp_path(conn, :new, request: request))\n      end\n    end\n\n    test \"renders totp registration template\", %{\n      identity_provider: identity_provider,\n      conn: conn,\n      request: request,\n      user: user\n    } do\n      Ecto.Changeset.change(identity_provider, %{totpable: true}) |> Repo.update!()\n\n      conn = log_in(conn, user)\n      conn = conn\n        |> put_session(\n          :totp_authenticated,\n          %{conn |> fetch_session() |> get_user_session() => true}\n        )\n        |> get(Routes.totp_path(conn, :new, request: request))\n\n      assert html_response(conn, 200) =~ \"Add TOTP authentication from an authenticator\"\n    end\n  end\n\n  describe \"POST /users/totp_registration\" do\n    test \"redirects to log in\", %{\n      conn: conn,\n      request: request\n    } do\n      conn =\n        conn\n        |> post(Routes.totp_path(conn, :register, request: request))\n\n      assert redirected_to(conn) =~ Routes.user_session_path(conn, :new)\n    end\n\n    test \"raises if identity provider not totpable\", %{\n      conn: conn,\n      request: request,\n      user: user\n    } do\n      assert_raise IdentityProviderError, fn ->\n        conn\n        |> log_in(user)\n        |> post(Routes.totp_path(conn, :register, request: request), %{\"totp\" => %{}})\n      end\n    end\n\n    test \"renders registration template with error with invalid code\", %{\n      identity_provider: identity_provider,\n      conn: conn,\n      request: request,\n      user: user\n    } do\n      Ecto.Changeset.change(identity_provider, %{totpable: true}) |> Repo.update!()\n\n      totp_params = %{\n        \"totp_code\" => \"bad code\",\n        \"totp_secret\" => \"bad secret\"\n      }\n\n      conn =\n        conn\n        |> log_in(user)\n        |> post(Routes.totp_path(conn, :register, request: request), %{\"totp\" => totp_params})\n\n      assert html_response(conn, 422) =~ \"Add TOTP authentication from an authenticator\"\n      assert html_response(conn, 422) =~ \"Given TOTP is invalid.\"\n    end\n\n    test \"redirects to chosse session with valid code\", %{\n      identity_provider: identity_provider,\n      conn: conn,\n      request: request,\n      user: user\n    } do\n      Ecto.Changeset.change(identity_provider, %{totpable: true}) |> Repo.update!()\n\n      secret = Totp.Admin.generate_secret()\n\n      totp_params = %{\n        \"totp_code\" => Totp.Admin.generate_totp(secret),\n        \"totp_secret\" => secret\n      }\n\n      conn =\n        conn\n        |> log_in(user)\n        |> post(Routes.totp_path(conn, :register, request: request), %{\"totp\" => totp_params})\n\n      assert redirected_to(conn) =~ \"/user_return_to\"\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/controllers/user_confirmation_controller_test.exs",
    "content": "defmodule BorutaIdentityWeb.UserConfirmationControllerTest do\n  use BorutaIdentityWeb.ConnCase, async: true\n\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.Deliveries\n  alias BorutaIdentity.Repo\n\n  import BorutaIdentity.AccountsFixtures\n\n  setup :with_a_request\n  setup %{identity_provider: identity_provider}do\n    %{user: user_fixture(%{backend: identity_provider.backend})}\n  end\n\n  describe \"GET /users/confirm\" do\n    test \"renders the confirmation page\", %{conn: conn, request: request} do\n      conn = get(conn, Routes.user_confirmation_path(conn, :new, request: request))\n      response = html_response(conn, 200)\n      assert response =~ \"<h1>Resend confirmation instructions</h1>\"\n    end\n  end\n\n  describe \"POST /users/confirm\" do\n    @tag :capture_log\n    test \"sends a new confirmation token\", %{conn: conn, user: user, request: request} do\n      conn =\n        post(conn, Routes.user_confirmation_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => user.username}\n        })\n\n      assert redirected_to(conn) == Routes.user_session_path(conn, :new, request: request)\n      assert get_flash(conn, :info) =~ \"If your email is in our system\"\n      assert Repo.get_by!(Accounts.UserToken, user_id: user.id).context == \"confirm\"\n    end\n\n    test \"does not send confirmation token if account is confirmed\", %{\n      conn: conn,\n      request: request\n    } do\n      {:ok, user} = user_fixture() |> Ecto.Changeset.change(confirmed_at: DateTime.utc_now()) |> Repo.update()\n\n      conn =\n        post(conn, Routes.user_confirmation_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => user.username}\n        })\n\n      assert redirected_to(conn) == Routes.user_session_path(conn, :new, request: request)\n      assert get_flash(conn, :info) =~ \"If your email is in our system\"\n      refute Repo.get_by(Accounts.UserToken, user_id: user.id)\n    end\n\n    test \"does not send confirmation token if email is invalid\", %{conn: conn, request: request} do\n      conn =\n        post(conn, Routes.user_confirmation_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => \"unknown@example.com\"}\n        })\n\n      assert redirected_to(conn) == Routes.user_session_path(conn, :new, request: request)\n      assert get_flash(conn, :info) =~ \"If your email is in our system\"\n      assert Repo.all(Accounts.UserToken) == []\n    end\n  end\n\n  describe \"GET /users/confirm/:token\" do\n    test \"confirms the given token once\", %{conn: conn, user: user, request: request} do\n      confirmation_url_fun = fn _ -> \"http://test.host\" end\n      {:ok, token} = Deliveries.deliver_user_confirmation_instructions(user.backend, user, confirmation_url_fun)\n\n      confirm_conn =\n        get(conn, Routes.user_confirmation_path(conn, :confirm, token, request: request))\n\n      assert redirected_to(confirm_conn) == Routes.user_session_path(conn, :new, request: request)\n      assert get_flash(confirm_conn, :info) =~ \"Account confirmed successfully\"\n      assert Accounts.get_user(user.id).confirmed_at\n      refute get_session(confirm_conn, :user_token)\n\n      # When not logged in\n      signed_out_conn =\n        get(conn, Routes.user_confirmation_path(conn, :confirm, token, request: request))\n\n      assert redirected_to(signed_out_conn) ==\n               Routes.user_session_path(signed_out_conn, :new, request: request)\n\n      assert get_flash(signed_out_conn, :error) =~\n               \"Account confirmation token is invalid or it has expired\"\n    end\n\n    test \"redirects if user is already confirmed\", %{conn: conn, request: request} do\n      user_fixture() |> Ecto.Changeset.change(confirmed_at: DateTime.utc_now()) |> Repo.update()\n\n      signed_in_conn =\n        conn\n        |> get(Routes.user_confirmation_path(conn, :confirm, \"unused_token\", request: request))\n\n      assert redirected_to(signed_in_conn) == Routes.user_session_path(conn, :new, request: request)\n      assert get_flash(signed_in_conn, :error) =~ \"Account confirmation token is invalid or it has expired\"\n    end\n\n    test \"does not confirm email with invalid token\", %{conn: conn, user: user, request: request} do\n      conn = get(conn, Routes.user_confirmation_path(conn, :confirm, \"oops\", request: request))\n      assert redirected_to(conn) == Routes.user_session_path(conn, :new, request: request)\n      assert get_flash(conn, :error) =~ \"Account confirmation token is invalid or it has expired\"\n      refute Accounts.get_user(user.id).confirmed_at\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/controllers/user_consent_controller_test.exs",
    "content": "defmodule BorutaIdentityWeb.UserConsentControllerTest do\n  use BorutaIdentityWeb.ConnCase\n\n  import BorutaIdentity.AccountsFixtures\n\n  describe \"GET /consent\" do\n    setup :with_a_request\n\n    test \"renders consent form\", %{\n      conn: conn,\n      request: request,\n      requested_scope: scope\n    } do\n      conn =\n        conn\n        |> log_in(user_fixture())\n        |> get(Routes.user_consent_path(conn, :index, %{request: request}))\n\n      assert html_response(conn, 200) =~ \"Scope from request\"\n      assert html_response(conn, 200) =~ scope.name\n    end\n  end\n\n  describe \"POST /consent\" do\n    setup :with_a_request\n\n    test \"redirects to after sign in path with valid params\", %{conn: conn, request: request} do\n      conn =\n        conn\n        |> log_in(user_fixture())\n        |> post(Routes.user_consent_path(conn, :consent, %{request: request}), %{})\n\n      assert redirected_to(conn) == \"/user_return_to\"\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/controllers/user_registration_controller_test.exs",
    "content": "defmodule BorutaIdentityWeb.UserRegistrationControllerTest do\n  use BorutaIdentityWeb.ConnCase, async: true\n\n  import BorutaIdentity.AccountsFixtures\n\n  alias BorutaIdentity.Repo\n\n  describe \"GET /users/register\" do\n    setup :with_a_request\n\n    test \"renders registration page\", %{conn: conn, request: request} do\n      conn = get(conn, Routes.user_registration_path(conn, :new, request: request))\n      response = html_response(conn, 200)\n      assert response =~ \"<h1>Register</h1>\"\n    end\n\n    test \"redirects if already logged in\", %{conn: conn, request: request} do\n      conn =\n        conn\n        |> log_in(user_fixture())\n        |> get(Routes.user_registration_path(conn, :new, request: request))\n\n      assert redirected_to(conn) == \"/user_return_to\"\n    end\n  end\n\n  describe \"POST /users/register\" do\n    setup :with_a_request\n\n    @tag :capture_log\n    test \"creates account and ask for confirmation in if confirmable\", %{conn: conn, request: request} do\n      email = unique_user_email()\n\n      conn =\n        post(conn, Routes.user_registration_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => email, \"password\" => valid_user_password()}\n        })\n\n      response = html_response(conn, 200)\n      assert response =~ \"<h1>Resend confirmation instructions</h1>\"\n      assert response =~ \"Email confirmation is required to authenticate.\"\n    end\n\n    @tag :capture_log\n    test \"creates account and logs the user in if not confirmable\", %{identity_provider: identity_provider, conn: conn, request: request} do\n      Ecto.Changeset.change(identity_provider, confirmable: false) |> Repo.update()\n      email = unique_user_email()\n\n      conn =\n        post(conn, Routes.user_registration_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => email, \"password\" => valid_user_password()}\n        })\n\n      assert get_session(conn, :user_token)\n      assert redirected_to(conn) =~ \"/user_return_to\"\n    end\n\n    test \"render errors for invalid data\", %{conn: conn, request: request} do\n      conn =\n        post(conn, Routes.user_registration_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => \"with spaces\", \"password\" => \"too short\"}\n        })\n\n      response = html_response(conn, 200)\n      assert response =~ \"<h1>Register</h1>\"\n      assert response =~ \"must have the @ sign and no spaces\"\n      assert response =~ \"should be at least 12 character\"\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/controllers/user_reset_password_controller_test.exs",
    "content": "defmodule BorutaIdentityWeb.UserResetPasswordControllerTest do\n  use BorutaIdentityWeb.ConnCase, async: false\n\n  import BorutaIdentity.AccountsFixtures\n  import Swoosh.TestAssertions\n\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.UserToken\n  alias BorutaIdentity.Repo\n\n  setup :set_swoosh_global\n  setup :with_a_request\n\n  setup %{identity_provider: identity_provider} do\n    {:ok, user: user_fixture(%{backend: identity_provider.backend})}\n  end\n\n  describe \"GET /users/reset_password\" do\n    test \"renders the reset password page\", %{conn: conn, request: request} do\n      conn = get(conn, Routes.user_reset_password_path(conn, :new, request: request))\n      response = html_response(conn, 200)\n      assert response =~ \"<h1>Forgot your password?</h1>\"\n    end\n  end\n\n  describe \"POST /users/reset_password\" do\n    @tag :capture_log\n    test \"sends a new reset password token\", %{conn: conn, user: user, request: request} do\n      conn =\n        post(conn, Routes.user_reset_password_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => user.username}\n        })\n\n      assert redirected_to(conn) == Routes.user_session_path(conn, :new, request: request)\n      assert get_flash(conn, :info) =~ \"If your email is in our system\"\n\n      user_token = Repo.get_by!(Accounts.UserToken, user_id: user.id)\n      assert user_token.context == \"reset_password\"\n      assert_email_sent(text_body: ~r/reset your password/)\n    end\n\n    test \"does not send reset password token if email is invalid\", %{conn: conn, request: request} do\n      conn =\n        post(conn, Routes.user_reset_password_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => \"unknown@example.com\"}\n        })\n\n      assert redirected_to(conn) == Routes.user_session_path(conn, :new, request: request)\n      assert get_flash(conn, :info) =~ \"If your email is in our system\"\n      assert Repo.all(Accounts.UserToken) == []\n\n      refute_email_sent()\n    end\n  end\n\n  describe \"GET /users/reset_password/:token\" do\n    setup %{user: user} do\n      {encoded_token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n      {:ok, _user_token} = Repo.insert(user_token)\n\n      {:ok, token: encoded_token}\n    end\n\n    test \"renders reset password\", %{conn: conn, token: token, request: request} do\n      conn = get(conn, Routes.user_reset_password_path(conn, :edit, token, request: request))\n      assert html_response(conn, 200) =~ \"<h1>Reset password</h1>\"\n    end\n\n    test \"does not render reset password with invalid token\", %{conn: conn, request: request} do\n      conn = get(conn, Routes.user_reset_password_path(conn, :edit, \"oops\", request: request))\n\n      response = html_response(conn, 200)\n\n      assert response =~ \"<h1>Reset password</h1>\"\n      assert response =~ \"Given reset password token is invalid.\"\n    end\n  end\n\n  describe \"PUT /users/reset_password/:token\" do\n    setup %{user: user} do\n      {encoded_token, user_token} = UserToken.build_email_token(user, \"reset_password\")\n      {:ok, _user_token} = Repo.insert(user_token)\n\n      {:ok, token: encoded_token}\n    end\n\n    test \"resets password once\", %{conn: conn, user: user, token: token, request: request, identity_provider: identity_provider} do\n      conn =\n        put(conn, Routes.user_reset_password_path(conn, :update, token, request: request), %{\n          \"user\" => %{\n            \"password\" => \"new valid password\",\n            \"password_confirmation\" => \"new valid password\"\n          }\n        })\n\n      assert redirected_to(conn) == Routes.user_session_path(conn, :new, request: request)\n      refute get_session(conn, :user_token)\n      assert get_flash(conn, :info) =~ \"Password reset successfully\"\n      assert {:ok, user} = Accounts.Internal.get_user(identity_provider.backend, %{email: user.username})\n\n      assert {:ok, _user} =\n               Accounts.Internal.check_user_against(\n                 identity_provider.backend,\n                 user,\n                 %{password: \"new valid password\"}\n               )\n    end\n\n    test \"does not reset password on invalid data\", %{conn: conn, token: token, request: request} do\n      conn =\n        put(conn, Routes.user_reset_password_path(conn, :update, token, request: request), %{\n          \"user\" => %{\n            \"password\" => \"too short\",\n            \"password_confirmation\" => \"does not match\"\n          }\n        })\n\n      response = html_response(conn, 200)\n\n      assert response =~ \"<h1>Reset password</h1>\"\n      assert response =~ \"should be at least 12 character(s)\"\n      assert response =~ \"does not match password\"\n    end\n\n    test \"does not reset password with invalid token\", %{conn: conn, request: request} do\n      conn = put(conn, Routes.user_reset_password_path(conn, :update, \"oops\", request: request))\n\n      response = html_response(conn, 200)\n\n      assert response =~ \"<h1>Reset password</h1>\"\n      assert response =~ \"Given reset password token is invalid.\"\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/controllers/user_session_controller_test.exs",
    "content": "defmodule BorutaIdentityWeb.UserSessionControllerTest do\n  use BorutaIdentityWeb.ConnCase, async: true\n\n  import BorutaIdentity.AccountsFixtures\n\n  alias BorutaIdentity.Repo\n\n  setup :with_a_request\n\n  setup %{identity_provider: identity_provider} do\n    {:ok, user} =\n      user_fixture(%{backend: identity_provider.backend})\n      |> Ecto.Changeset.change(confirmed_at: DateTime.utc_now())\n      |> Repo.update()\n\n    %{user: user}\n  end\n\n  describe \"GET /users/log_in\" do\n    test \"renders log in page\", %{conn: conn, request: request} do\n      conn = get(conn, Routes.user_session_path(conn, :new, request: request))\n      response = html_response(conn, 200)\n      assert response =~ \"<h1>Log in</h1>\"\n    end\n\n    test \"redirects if already logged in\", %{conn: conn, user: user, request: request} do\n      conn = conn |> log_in(user) |> get(Routes.user_session_path(conn, :new, request: request))\n      assert redirected_to(conn) == \"/user_return_to\"\n    end\n  end\n\n  describe \"POST /users/log_in\" do\n    test \"logs the user in with remember me\", %{conn: conn, user: user, request: request} do\n      conn =\n        post(conn, Routes.user_session_path(conn, :create, request: request), %{\n          \"user\" => %{\n            \"email\" => user.username,\n            \"password\" => valid_user_password(),\n            \"remember_me\" => \"true\"\n          }\n        })\n\n      assert conn.resp_cookies[\"_boruta_identity_web_user_remember_me\"]\n      assert redirected_to(conn) =~ \"/\"\n    end\n\n    test \"returns unauthorized when not confirmed\", %{\n      conn: conn,\n      request: request,\n      identity_provider: identity_provider\n    } do\n      user = user_fixture(%{backend: identity_provider.backend})\n\n      conn =\n        post(conn, Routes.user_session_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => user.username, \"password\" => valid_user_password()}\n        })\n\n      response = html_response(conn, 401)\n      assert response =~ \"<h1>Resend confirmation instructions</h1>\"\n      assert response =~ \"Email confirmation is required to authenticate.\"\n    end\n\n    test \"returns unauthorized with invalid credentials\", %{\n      conn: conn,\n      user: user,\n      request: request\n    } do\n      conn =\n        post(conn, Routes.user_session_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => user.username, \"password\" => \"invalid_password\"}\n        })\n\n      response = html_response(conn, 401)\n      assert response =~ \"<h1>Log in</h1>\"\n      assert response =~ \"Invalid email or password\"\n    end\n\n    test \"logs the user in\", %{conn: conn, user: user, request: request} do\n      conn =\n        post(conn, Routes.user_session_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => user.username, \"password\" => valid_user_password()}\n        })\n\n      assert get_session(conn, :user_token)\n      assert redirected_to(conn) == \"/user_return_to\"\n    end\n\n    test \"logs the user in with totp identity provider\", %{\n      conn: conn,\n      user: user,\n      request: request,\n      identity_provider: identity_provider\n    } do\n      Ecto.Changeset.change(identity_provider, %{totpable: true}) |> Repo.update()\n\n      conn =\n        post(conn, Routes.user_session_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => user.username, \"password\" => valid_user_password()}\n        })\n\n      assert redirected_to(conn) == \"/user_return_to\"\n    end\n\n    test \"returns totp template with totp identity provider and totp registered user\", %{\n      conn: conn,\n      user: user,\n      request: request,\n      identity_provider: identity_provider\n    } do\n      Ecto.Changeset.change(identity_provider, %{totpable: true}) |> Repo.update()\n\n      Ecto.Changeset.change(user, %{\n        totp_registered_at: DateTime.utc_now()\n      })\n      |> Repo.update()\n\n      conn =\n        post(conn, Routes.user_session_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => user.username, \"password\" => valid_user_password()}\n        })\n\n      assert html_response(conn, 200) =~ ~r/TOTP authentication/\n    end\n\n    test \"returns totp template with totp enforced identity provider and totp registered user\", %{\n      conn: conn,\n      user: user,\n      request: request,\n      identity_provider: identity_provider\n    } do\n      Ecto.Changeset.change(identity_provider, %{\n        enforce_totp: true,\n        totpable: true\n      })\n      |> Repo.update()\n\n      Ecto.Changeset.change(user, %{\n        totp_registered_at: DateTime.utc_now()\n      })\n      |> Repo.update()\n\n      conn =\n        post(conn, Routes.user_session_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => user.username, \"password\" => valid_user_password()}\n        })\n\n      assert html_response(conn, 200) =~ ~r/TOTP authentication/\n    end\n\n    test \"redirects to totp registration with totp enforced identity provider\", %{\n      conn: conn,\n      user: user,\n      request: request,\n      identity_provider: identity_provider\n    } do\n      Ecto.Changeset.change(identity_provider, %{\n        enforce_totp: true,\n        totpable: true\n      })\n      |> Repo.update()\n\n      conn =\n        post(conn, Routes.user_session_path(conn, :create, request: request), %{\n          \"user\" => %{\"email\" => user.username, \"password\" => valid_user_password()}\n        })\n\n      assert redirected_to(conn) =~ Routes.totp_path(conn, :new)\n    end\n  end\n\n  describe \"GET /users/totp_authenticate\" do\n    test \"redirects to login\", %{\n      conn: conn,\n      request: request\n    } do\n      conn =\n        conn\n        |> get(Routes.user_session_path(conn, :initialize_totp, request: request))\n\n      assert redirected_to(conn) =~ Routes.user_session_path(conn, :new)\n    end\n\n    test \"returns totp template with totp enforced identity provider and logged in user\",\n         %{\n           conn: conn,\n           user: user,\n           request: request,\n           identity_provider: identity_provider\n         } do\n      Ecto.Changeset.change(identity_provider, %{totpable: true, enforce_totp: true})\n      |> Repo.update()\n\n      conn =\n        conn\n        |> log_in(user)\n        |> get(Routes.user_session_path(conn, :initialize_totp, request: request))\n\n      assert redirected_to(conn) =~ Routes.totp_path(conn, :new)\n    end\n\n    test \"returns totp template with totp enforced identity provider and totp logged in registered user\",\n         %{\n           conn: conn,\n           user: user,\n           request: request,\n           identity_provider: identity_provider\n         } do\n      Ecto.Changeset.change(identity_provider, %{\n        totpable: true,\n        enforce_totp: true\n      })\n      |> Repo.update()\n\n      Ecto.Changeset.change(user, %{\n        totp_registered_at: DateTime.utc_now()\n      })\n      |> Repo.update()\n\n      conn =\n        conn\n        |> log_in(user)\n        |> get(Routes.user_session_path(conn, :initialize_totp, request: request))\n\n      assert html_response(conn, 200) =~ ~r/TOTP authentication/\n    end\n\n    test \"returns totp template with totp identity provider and totp logged in registered user\",\n         %{\n           conn: conn,\n           user: user,\n           request: request,\n           identity_provider: identity_provider\n         } do\n      Ecto.Changeset.change(identity_provider, %{totpable: true}) |> Repo.update()\n\n      Ecto.Changeset.change(user, %{\n        totp_registered_at: DateTime.utc_now()\n      })\n      |> Repo.update()\n\n      conn =\n        conn\n        |> log_in(user)\n        |> get(Routes.user_session_path(conn, :initialize_totp, request: request))\n\n      assert html_response(conn, 200) =~ ~r/TOTP authentication/\n    end\n  end\n\n  describe \"GET /users/log_out\" do\n    test \"logs the user out\", %{conn: conn, user: user, request: request} do\n      conn =\n        conn |> log_in(user) |> get(Routes.user_session_path(conn, :delete, request: request))\n\n      assert redirected_to(conn) == Routes.user_session_path(conn, :new, request: request)\n      assert get_flash(conn, :info) =~ \"Logged out successfully\"\n    end\n\n    test \"succeeds even if the user is not logged in\", %{conn: conn, request: request} do\n      conn = get(conn, Routes.user_session_path(conn, :delete, request: request))\n      assert redirected_to(conn) == Routes.user_session_path(conn, :new, request: request)\n      assert get_flash(conn, :info) =~ \"Logged out successfully\"\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/controllers/user_settings_controller_test.exs",
    "content": "defmodule BorutaIdentityWeb.UserSettingsControllerTest do\n  use BorutaIdentityWeb.ConnCase\n\n  import BorutaIdentity.AccountsFixtures\n\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Repo\n\n  setup :register_and_log_in\n\n  describe \"GET /users/settings\" do\n    setup :with_a_request\n\n    test \"renders settings page\", %{conn: conn, request: request} do\n      conn = get(conn, Routes.user_settings_path(conn, :edit, request: request))\n      response = html_response(conn, 200)\n      assert response =~ \"<h1>Edit user</h1>\"\n    end\n\n    test \"redirects if user is not logged in\", %{request: request} do\n      conn = build_conn()\n      conn = get(conn, Routes.user_settings_path(conn, :edit, request: request))\n      assert redirected_to(conn) == Routes.user_session_path(conn, :new, request: request)\n    end\n  end\n\n  describe \"PUT /users/settings\" do\n    setup :with_a_request\n\n    setup %{identity_provider: identity_provider, user: user} do\n      {:ok, _identity_provider} =\n        identity_provider\n        |> Ecto.Changeset.change(%{backend_id: user.backend.id})\n        |> Repo.update()\n\n      :ok\n    end\n\n    @tag :skip\n    test \"render errors when data is invalid\"\n\n    test \"updates an user with metadata\", %{conn: conn, request: request, user: user} do\n      {:ok, _backend} = Ecto.Changeset.change(user.backend, %{metadata_fields: [%{\"attribute_name\" => \"test\", \"user_editable\" => true}]}) |> Repo.update()\n      conn =\n        put(conn, Routes.user_settings_path(conn, :update, request: request), %{\n          \"user\" => %{\n            \"current_password\" => valid_user_password(),\n            \"metadata\" => %{\"test\" => \"test value\"}\n          }\n        })\n\n      assert redirected_to(conn, 302) == Routes.user_settings_path(conn, :edit, request: request)\n\n      assert %User{metadata: %{\"test\" => %{\"value\" => \"test value\", \"status\" => \"valid\"}}} = Repo.reload(user)\n    end\n\n    test \"updates an user without metadata (do not override)\", %{\n      conn: conn,\n      request: request,\n      user: user\n    } do\n      {:ok, _user} =\n        Ecto.Changeset.change(user, %{metadata: %{\"test\" => \"test value\"}}) |> Repo.update()\n\n      conn =\n        put(conn, Routes.user_settings_path(conn, :update, request: request), %{\n          \"user\" => %{\n            \"current_password\" => valid_user_password()\n          }\n        })\n\n      assert redirected_to(conn, 302) == Routes.user_settings_path(conn, :edit, request: request)\n\n      assert %User{metadata: %{\"test\" => \"test value\"}} = Repo.reload(user)\n    end\n  end\n\n  describe \"POST /users/destroy\" do\n    setup :with_a_request\n\n    test \"redirects to log in\", %{conn: conn, request: request} do\n      conn = post(conn, Routes.user_settings_path(conn, :destroy, request: request))\n      assert redirected_to(conn) =~ ~r\"/users/log_in\"\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/plugs/sessions_test.exs",
    "content": "defmodule BorutaIdentityWeb.SessionsTest do\n  use BorutaIdentityWeb.ConnCase, async: true\n\n  import BorutaIdentity.AccountsFixtures\n\n  alias BorutaIdentityWeb.Sessions\n\n  @remember_me_cookie \"_boruta_identity_web_user_remember_me\"\n\n  setup %{conn: conn} do\n    conn =\n      conn\n      |> Map.replace!(:secret_key_base, BorutaIdentityWeb.Endpoint.config(:secret_key_base))\n      |> init_test_session(%{})\n      |> Plug.Conn.fetch_query_params()\n\n    %{user: user_fixture(), conn: conn}\n  end\n\n  describe \"fetch_current_user/2\" do\n    test \"authenticates user from session\", %{conn: conn, user: user} do\n      user_token = generate_user_session_token(user)\n      conn = conn |> put_session(:user_token, user_token) |> Sessions.fetch_current_user([])\n      assert conn.assigns.current_user.id == user.id\n    end\n\n    test \"does not authenticate if data is missing\", %{conn: conn, user: user} do\n      _ = generate_user_session_token(user)\n      conn = Sessions.fetch_current_user(conn, [])\n      refute get_session(conn, :user_token)\n      refute conn.assigns.current_user\n    end\n\n    test \"authenticates user from cookies\", %{conn: conn, user: user} do\n      logged_in_conn =\n        conn\n        |> fetch_cookies()\n        |> log_in(user, %{\"user\" => %{\"remember_me\" => \"true\"}})\n\n      user_token = logged_in_conn.cookies[@remember_me_cookie]\n      %{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]\n\n      conn =\n        conn\n        |> put_req_cookie(@remember_me_cookie, signed_token)\n        |> Sessions.fetch_current_user([])\n\n      assert get_session(conn, :user_token) == user_token\n      assert conn.assigns.current_user.id == user.id\n    end\n  end\n\n  describe \"redirect_if_user_is_authenticated/2\" do\n    test \"redirects if user is authenticated\", %{conn: conn, user: user} do\n      conn = conn |> assign(:current_user, user) |> Sessions.redirect_if_user_is_authenticated([])\n      assert conn.halted\n      assert redirected_to(conn) == \"/\"\n    end\n\n    test \"does not redirect if user is not authenticated\", %{conn: conn} do\n      conn = Sessions.redirect_if_user_is_authenticated(conn, [])\n      refute conn.halted\n      refute conn.status\n    end\n  end\n\n  describe \"require_authenticated_user/2\" do\n    test \"redirects if user is not authenticated\", %{conn: conn} do\n      conn = conn |> fetch_flash() |> Sessions.require_authenticated_user([])\n      assert conn.halted\n      assert redirected_to(conn) == Routes.user_session_path(conn, :new)\n      assert get_flash(conn, :error) == \"You must log in to access this page.\"\n    end\n\n    test \"does not redirect if user is authenticated\", %{conn: conn, user: user} do\n      conn = conn |> assign(:current_user, user) |> Sessions.require_authenticated_user([])\n      refute conn.halted\n      refute conn.status\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/views/error_view_test.exs",
    "content": "defmodule BorutaIdentityWeb.ErrorViewTest do\n  use BorutaIdentityWeb.ConnCase, async: true\n\n  # Bring render/3 and render_to_string/3 for testing custom views\n  import Phoenix.View\n\n  test \"renders 404.html\" do\n    assert render_to_string(BorutaIdentityWeb.ErrorView, \"404.html\", []) =~ \"Page not found\"\n  end\n\n  test \"renders 500.html\" do\n    assert render_to_string(BorutaIdentityWeb.ErrorView, \"500.html\", []) =~ \"Internal server error\"\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/views/layout_view_test.exs",
    "content": "defmodule BorutaIdentityWeb.LayoutViewTest do\n  use BorutaIdentityWeb.ConnCase, async: true\n\n  # When testing helpers, you may want to import Phoenix.HTML and\n  # use functions such as safe_to_string() to convert the helper\n  # result into an HTML string.\n  # import Phoenix.HTML\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/boruta_identity_web/views/page_view_test.exs",
    "content": "defmodule BorutaIdentityWeb.PageViewTest do\n  use BorutaIdentityWeb.ConnCase, async: true\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/support/boruta_factory.ex",
    "content": "defmodule Boruta.Factory do\n  @moduledoc false\n\n  use ExMachina.Ecto, repo: BorutaAuth.Repo\n\n  alias Boruta.Ecto\n\n  def client_factory do\n    %Ecto.Client{\n      secret: SecureRandom.urlsafe_base64(),\n      redirect_uris: [\"https://redirect.uri/oauth2-redirect-path\"],\n      access_token_ttl: 3600,\n      authorization_code_ttl: 60,\n      private_key:\n        \"-----BEGIN RSA PRIVATE KEY-----\\nMIIEowIBAAKCAQEA1PaP/gbXix5itjRCaegvI/B3aFOeoxlwPPLvfLHGA4QfDmVO\\nf8cU8OuZFAYzLArW3PnnwWWy39nVJOx42QRVGCGdUCmV7shDHRsr86+2DlL7pwUa\\n9QyHsTj84fAJn2Fv9h9mqrIvUzAtEYRlGFvjVTGCwzEullpsB0GJafopUTFby8Wd\\nSq3dGLJBB1r+Q8QtZnAxxvolhwOmYkBkkidefmm48X7hFXL2cSJm2G7wQyinOey/\\nU8xDZ68mgTakiqS2RtjnFD0dnpBl5CYTe4s6oZKEyFiFNiW4KkR1GVjsKwY9oC2t\\npyQ0AEUMvk9T9VdIltSIiAvOKlwFzL49cgwZDwIDAQABAoIBAG0dg/upL8k1IWiv\\n8BNphrXIYLYQmiiBQTPJWZGvWIC2sl7i40yvCXjDjiRnZNK9HwgL94XtALCXYRFR\\nJD41bRA3MO5A0HSPIWwJXwS10/cU56HVCNHjwKa6Rz/QiG2kNASMZEMzlvHtrjna\\ndx36/sjI3HH8gh1BaTZyiuDE72SMkPbL838jfL1YY9uJ0u6hWFDbdn3sqPfJ6Cnz\\n1cu0piT35nkilnIGCNYA0i3lyMeo4XrdXaAJdN9nnqbCi5ewQWqaHbrIIY5LTgzJ\\nYlOr3IiecyokFxHCbULXle60u0KqXYgBHmlQJJr1Dj4c9AkQmefjC2jRMlhOrIzo\\nIkIUeMECgYEA+MNLB+w6vv1ogqzM3M1OLt6bziWJCn+XkziuMrCiY9KeDD+S70+E\\nhfbhM5RjCE3wxC/k59039laT973BmdMHxrDd2zSjOFmCIORv5yrD5oBHMaMZcwuQ\\n45Xisi4aoQoOhyznSnjo/RjeQB7qEDzXFznLLNT79HzqyAtCWD3UIu8CgYEA2yik\\n9FKl7HJEY94D2K6vNh1AHGnkwIQC72pXzlUrVuwQYngj6/Gkhw8ayFBApHfwVCXj\\no9rDYPdNrrAs0Zz0JsiJp6bOCEKCrMYE16UiejUUAg/OZ5eg6+3m3/iWatkzLUuK\\n1LIkVBJlEyY0uPuAaBF0V0VleNvfCGhVYOn46+ECgYAUD4OsduNh5YOZDiBTKgdF\\nBlSgMiyz+QgbKjX6Bn6B+EkgibvqqonwV7FffHbkA40H9SjLfe52YhL6poXHRtpY\\nroillcAX2jgBOQrBJJS5sNyM5y81NNiRUdP/NHKXS/1R71ATlF6NkoTRvOx5NL7P\\ns6xryB0tYSl5ylamUQ4bZwKBgHF6FB9mA//wErVbKcayfIqajq2nrwh30kVBXQG7\\nW9uAE+PIrWDoF/bOvWFnHHGMoOYRUFNxXKUCqDiBhFNs34aNY6lpV1kzhxIK3ksC\\neF2qyhdfM9Kz0mEXJ+pkfw4INNWJPfNv4hueArPtnnMB1rUMBJ+DkU0JG+zwiPTL\\ncVZBAoGBAM6kOsh5KGn3aI83g9ZO0TrKLXXFotxJt31Wu11ydj9K33/Qj3UXcxd4\\nJPXr600F0DkLeUKBob6BALeHFWcrSz5FGLGRqdRxdv+L6g18WH5m2xEs7o6M6e5I\\nIhyUC60ZewJ2M8rV4KgCJJdZE2kENlSgjU92IDVPT9Oetrc7hQJd\\n-----END RSA PRIVATE KEY-----\\n\\n\",\n      public_key:\n        \"-----BEGIN RSA PUBLIC KEY-----\\nMIIBCgKCAQEA1PaP/gbXix5itjRCaegvI/B3aFOeoxlwPPLvfLHGA4QfDmVOf8cU\\n8OuZFAYzLArW3PnnwWWy39nVJOx42QRVGCGdUCmV7shDHRsr86+2DlL7pwUa9QyH\\nsTj84fAJn2Fv9h9mqrIvUzAtEYRlGFvjVTGCwzEullpsB0GJafopUTFby8WdSq3d\\nGLJBB1r+Q8QtZnAxxvolhwOmYkBkkidefmm48X7hFXL2cSJm2G7wQyinOey/U8xD\\nZ68mgTakiqS2RtjnFD0dnpBl5CYTe4s6oZKEyFiFNiW4KkR1GVjsKwY9oC2tpyQ0\\nAEUMvk9T9VdIltSIiAvOKlwFzL49cgwZDwIDAQAB\\n-----END RSA PUBLIC KEY-----\\n\\n\"\n    }\n  end\n\n  def scope_factory do\n    %Ecto.Scope{\n      name: SecureRandom.hex(10),\n      public: false\n    }\n  end\n\n  def token_factory do\n    %Ecto.Token{\n      client: build(:client),\n      type: \"access_token\",\n      value: Boruta.TokenGenerator.generate(),\n      expires_at: :os.system_time(:seconds) + 10\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/support/boruta_identity_factory.ex",
    "content": "defmodule BorutaIdentity.Factory do\n  @moduledoc false\n\n  use ExMachina.Ecto, repo: BorutaIdentity.Repo\n\n  alias BorutaIdentity.Accounts.Consent\n  alias BorutaIdentity.Accounts.EmailTemplate\n  alias BorutaIdentity.Accounts.Internal\n  alias BorutaIdentity.Accounts.Role\n  alias BorutaIdentity.Accounts.RoleScope\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.UserAuthorizedScope\n  alias BorutaIdentity.Accounts.UserRole\n  alias BorutaIdentity.Accounts.UserToken\n  alias BorutaIdentity.Configuration.ErrorTemplate\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.IdentityProviders.ClientIdentityProvider\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.IdentityProviders.Template\n  alias BorutaIdentity.Organizations.Organization\n\n  # @password \"hello world!\"\n  @hashed_password \"$argon2id$v=19$m=131072,t=8,p=4$9lPv7KsJogno0FlnhaRQXA$TeTY9FYjR1HJtZzg+N1z0oDC+0Mn7buPpOMhDP+M2Ik\"\n\n  def user_factory do\n    %User{\n      username: \"user#{System.unique_integer()}@example.com\",\n      uid: SecureRandom.hex(),\n      backend: insert(:backend)\n    }\n  end\n\n  def user_authorized_scope_factory do\n    %UserAuthorizedScope{}\n  end\n\n  def user_role_factory do\n    %UserRole{}\n  end\n\n  def reset_password_user_token_factory do\n    user = build(:user)\n\n    %UserToken{\n      token: SecureRandom.hex(64),\n      context: \"reset_password\",\n      sent_to: user.username,\n      user: user\n    }\n  end\n\n  def internal_user_factory do\n    %Internal.User{\n      email: \"user#{System.unique_integer()}@example.com\",\n      hashed_password: @hashed_password,\n      backend: build(:backend)\n    }\n  end\n\n  def consent_factory do\n    %Consent{\n      client_id: SecureRandom.uuid(),\n      scopes: []\n    }\n  end\n\n  def client_identity_provider_factory do\n    %ClientIdentityProvider{\n      client_id: SecureRandom.uuid(),\n      identity_provider: build(:identity_provider)\n    }\n  end\n\n  def identity_provider_factory do\n    %IdentityProvider{\n      name: sequence(:name, &\"identity provider #{&1}\"),\n      backend: build(:backend)\n    }\n  end\n\n  def backend_factory do\n    %Backend{\n      name: \"backend name\",\n      type: \"Elixir.BorutaIdentity.Accounts.Internal\"\n    }\n  end\n\n  def federated_backend_factory do\n    %Backend{\n      name: \"backend name\",\n      type: \"Elixir.BorutaIdentity.Accounts.Internal\",\n      federated_servers: [%{\n        \"name\" => \"federated\",\n        \"client_id\" => \"client_id\",\n        \"client_secret\" => \"client_secret\",\n        \"base_url\" => \"http://localhost:7878\",\n        \"token_path\" => \"/token_path\",\n        \"authorize_path\" => \"/authorize_path\",\n        \"userinfo_path\" => \"/userinfo_path\",\n      }]\n    }\n  end\n\n  def ldap_backend_factory do\n    %Backend{\n      name: \"backend name\",\n      type: \"Elixir.BorutaIdentity.Accounts.Ldap\",\n      ldap_pool_size: 2,\n      ldap_host: \"ldpa.test\",\n      ldap_user_rdn_attribute: \"sn\",\n      ldap_base_dn: \"dc=ldap,dc=test\",\n      ldap_ou: \"\"\n    }\n  end\n\n  def smtp_backend_factory do\n    %Backend{\n      name: \"backend name\",\n      type: \"Elixir.BorutaIdentity.Accounts.Internal\",\n      smtp_from: \"from@test.factory\",\n      smtp_relay: \"test.smtp.factory\",\n      smtp_ssl: false,\n      smtp_tls: \"never\",\n      smtp_username: \"factory_smtp_username\",\n      smtp_password: \"factory_smtp_password\",\n      smtp_port: 25\n    }\n  end\n\n  def template_factory do\n    %Template{\n      type: \"template_type\",\n      content: \"template content\"\n    }\n  end\n\n  def new_registration_template_factory do\n    %Template{\n      type: \"new_registration\",\n      content: Template.default_content(:new_registration)\n    }\n  end\n\n  def email_template_factory do\n    %EmailTemplate{\n      type: \"template_type\",\n      txt_content: \"template content\",\n      html_content: \"template content\"\n    }\n  end\n\n  def reset_password_instructions_email_template_factory do\n    %EmailTemplate{\n      type: \"reset_password_instructions\",\n      txt_content: EmailTemplate.default_txt_content(:reset_password_instructions),\n      html_content: EmailTemplate.default_html_content(:reset_password_instructions)\n    }\n  end\n\n  def error_template_factory do\n    %ErrorTemplate{\n      type: \"400\",\n      content: \"error template content\"\n    }\n  end\n\n  def role_factory do\n    %Role{\n      name: SecureRandom.hex(32)\n    }\n  end\n\n  def role_scope_factory do\n    %RoleScope{}\n  end\n\n  def organization_factory do\n    %Organization{\n      name: \"Organization \" <> SecureRandom.hex()\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/support/conn_case.ex",
    "content": "defmodule BorutaIdentityWeb.ConnCase do\n  @moduledoc \"\"\"\n  This module defines the test case to be used by\n  tests that require setting up a connection.\n\n  Such tests rely on `Phoenix.ConnTest` and also\n  import other functionality to make it easier\n  to build common data structures and query the data layer.\n\n  Finally, if the test case interacts with the database,\n  we enable the SQL sandbox, so changes done to the database\n  are reverted at the end of every test. If you are using\n  PostgreSQL, you can even run database tests asynchronously\n  by setting `use BorutaIdentityWeb.ConnCase, async: true`, although\n  this option is not recommended for other databases.\n  \"\"\"\n\n  use ExUnit.CaseTemplate\n\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.UserToken\n  alias BorutaIdentity.Repo\n  alias BorutaIdentityWeb.Authenticable\n  alias Ecto.Adapters.SQL.Sandbox\n\n  using do\n    quote do\n      # Import conveniences for testing with connections\n      import Plug.Conn\n      import Phoenix.ConnTest\n      import BorutaIdentityWeb.ConnCase\n\n      alias BorutaIdentityWeb.Router.Helpers, as: Routes\n\n      # The default endpoint for testing\n      @endpoint BorutaIdentityWeb.Endpoint\n    end\n  end\n\n  setup tags do\n    :ok = Sandbox.checkout(BorutaIdentity.Repo)\n    :ok = Sandbox.checkout(BorutaAuth.Repo)\n\n    unless tags[:async] do\n      Sandbox.mode(BorutaIdentity.Repo, {:shared, self()})\n    end\n\n    {:ok, conn: Phoenix.ConnTest.build_conn()}\n  end\n\n  @doc \"\"\"\n  Setup helper that registers and logs in users.\n\n      setup :register_and_log_in\n\n  It stores an updated connection and a registered user in the\n  test context.\n  \"\"\"\n  def register_and_log_in(%{conn: conn}) do\n    user = BorutaIdentity.AccountsFixtures.user_fixture()\n    %{conn: log_in(conn, user), user: user}\n  end\n\n  @doc \"\"\"\n  Generates a session token.\n  \"\"\"\n  @spec generate_user_session_token(user :: User.t()) :: token :: String.t()\n  def generate_user_session_token(%User{id: user_id}) do\n    user = Repo.get!(User, user_id)\n    User.login_changeset(user) |> Repo.update()\n\n    {token, user_token} = UserToken.build_session_token(user)\n    Repo.insert!(user_token)\n    token\n  end\n\n  @doc \"\"\"\n  Logs the given `user` into the `conn`.\n\n  It returns an updated `conn`.\n  \"\"\"\n  def log_in(conn, user, params \\\\ %{}) do\n    token = generate_user_session_token(user)\n\n    conn\n    |> Phoenix.ConnTest.init_test_session(%{})\n    |> Map.put(:body_params, params)\n    |> Authenticable.store_user_session(token)\n  end\n\n  def with_a_request(_params) do\n    identity_provider =\n      BorutaIdentity.Factory.insert(:identity_provider,\n        registrable: true,\n        consentable: true,\n        confirmable: true,\n        user_editable: true,\n        backend: BorutaIdentity.Factory.build(:smtp_backend)\n      )\n\n    client = Boruta.Factory.insert(:client)\n    scope = Boruta.Factory.insert(:scope, name: \"request:scope\", label: \"Scope from request\")\n\n    client_identity_provider =\n      BorutaIdentity.Factory.insert(:client_identity_provider,\n        identity_provider: identity_provider,\n        client_id: client.id\n      )\n\n    {:ok, jwt, _payload} =\n      Joken.encode_and_sign(\n        %{\n          \"client_id\" => client_identity_provider.client_id,\n          \"scope\" => scope.name,\n          \"user_return_to\" => \"/user_return_to\"\n        },\n        BorutaIdentityWeb.Token.application_signer()\n      )\n\n    %{request: jwt, identity_provider: identity_provider, client: client, requested_scope: scope}\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/support/data_case.ex",
    "content": "defmodule BorutaIdentity.DataCase do\n  @moduledoc \"\"\"\n  This module defines the setup for tests requiring\n  access to the application's data layer.\n\n  You may define functions here to be used as helpers in\n  your tests.\n\n  Finally, if the test case interacts with the database,\n  we enable the SQL sandbox, so changes done to the database\n  are reverted at the end of every test. If you are using\n  PostgreSQL, you can even run database tests asynchronously\n  by setting `use BorutaIdentity.DataCase, async: true`, although\n  this option is not recommended for other databases.\n  \"\"\"\n\n  use ExUnit.CaseTemplate\n\n  alias Ecto.Adapters.SQL.Sandbox\n\n  using do\n    quote do\n      alias BorutaIdentity.Repo\n\n      import Ecto\n      import Ecto.Changeset\n      import Ecto.Query\n      import BorutaIdentity.DataCase\n      import BorutaIdentity.Factory\n    end\n  end\n\n  setup tags do\n    :ok = Sandbox.checkout(BorutaIdentity.Repo)\n    :ok = Sandbox.checkout(BorutaAuth.Repo)\n\n    unless tags[:async] do\n      Sandbox.mode(BorutaIdentity.Repo, {:shared, self()})\n      Sandbox.mode(BorutaAuth.Repo, {:shared, self()})\n    end\n\n    :ok\n  end\n\n  @doc \"\"\"\n  A helper that transforms changeset errors into a map of messages.\n\n      assert {:error, changeset} = Accounts.create_user(%{password: \"short\"})\n      assert \"password is too short\" in errors_on(changeset).password\n      assert %{password: [\"password is too short\"]} = errors_on(changeset)\n\n  \"\"\"\n  def errors_on(changeset) do\n    Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->\n      Regex.replace(~r\"%{(\\w+)}\", message, fn _, key ->\n        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()\n      end)\n    end)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/support/fixtures/accounts_fixtures.ex",
    "content": "defmodule BorutaIdentity.AccountsFixtures do\n  @moduledoc \"\"\"\n  This module defines test helpers for creating\n  entities via the `BorutaIdentity.Accounts` context.\n  \"\"\"\n\n  import BorutaIdentity.Factory\n\n  alias Boruta.Ecto.Admin\n  alias BorutaIdentity.Accounts.UserAuthorizedScope\n  alias BorutaIdentity.Repo\n\n  # From BorutaIdentity.Factory\n  @password \"hello world!\"\n\n  def unique_user_email, do: \"user#{System.unique_integer()}@example.com\"\n  def valid_user_password, do: @password\n\n  def user_fixture(attrs \\\\ %{}, account_type \\\\ \"internal\") do\n    backend = attrs[:backend] || insert(:backend)\n    user = insert(:internal_user, Map.merge(%{backend: backend}, attrs))\n\n    insert(:user,\n      username: user.email,\n      uid: user.id,\n      backend: backend,\n      account_type: account_type,\n      metadata: attrs[:metadata] || %{}\n    )\n    |> Repo.preload([:backend, :authorized_scopes, :roles, :organizations])\n  end\n\n  def user_scopes_fixture(user, attrs \\\\ %{}) do\n    {:ok, scope} = Admin.create_scope(%{name: \"name\"})\n\n    {:ok, scope} =\n      Repo.insert(\n        %UserAuthorizedScope{\n          user_id: user.id,\n          scope_id: scope.id\n        }\n        |> Ecto.Changeset.change(attrs)\n      )\n\n    scope\n  end\n\n  def extract_user_token(fun) do\n    {:ok, captured} = fun.(&\"[TOKEN]#{&1}[TOKEN]\")\n    [_, token, _] = String.split(captured.body, \"[TOKEN]\")\n    token\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/support/fixtures/admin_fixtures.ex",
    "content": "defmodule BorutaIdentity.AdminFixtures do\n  @moduledoc \"\"\"\n  This module defines test helpers for creating\n  entities via the `BorutaIdentity.Admin` context.\n  \"\"\"\n\n  def role_fixture(attrs \\\\ %{}) do\n    {:ok, role} =\n      attrs\n      |> Enum.into(%{\n        name: \"some name\"\n      })\n      |> BorutaIdentity.Admin.create_role()\n\n    role\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/support/fixtures/identity_providers_fixtures.ex",
    "content": "defmodule BorutaIdentity.IdentityProvidersFixtures do\n  @moduledoc \"\"\"\n  This module defines test helpers for creating\n  entities via the `BorutaIdentity.IdentityProviders` context.\n  \"\"\"\n\n  @doc \"\"\"\n  Generate a backend.\n  \"\"\"\n  def backend_fixture(attrs \\\\ %{}) do\n    {:ok, backend} =\n      attrs\n      |> Enum.into(%{\n        type: \"Elixir.BorutaIdentity.Accounts.Internal\",\n        name: \"some name\"\n      })\n      |> BorutaIdentity.IdentityProviders.create_backend()\n\n    backend\n  end\nend\n"
  },
  {
    "path": "apps/boruta_identity/test/test_helper.exs",
    "content": "ExUnit.start()\n\nMox.defmock(BorutaIdentity.LdapRepoMock, for: BorutaIdentity.LdapRepo)\n\nEcto.Adapters.SQL.Sandbox.mode(BorutaIdentity.Repo, :manual)\nEcto.Adapters.SQL.Sandbox.mode(BorutaAuth.Repo, :manual)\n\nLogger.remove_backend(:console)\n"
  },
  {
    "path": "apps/boruta_web/.formatter.exs",
    "content": "[\n  import_deps: [:phoenix],\n  inputs: [\"*.{ex,exs}\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": "apps/boruta_web/.gitignore",
    "content": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up here.\n/cover/\n\n# The directory Mix downloads your dependencies sources to.\n/deps/\n\n# Where 3rd-party dependencies like ExDoc output generated docs.\n/doc/\n\n# Ignore .fetch files in case you like to edit your project deps locally.\n/.fetch\n\n# If the VM crashes, it generates a dump, let's ignore it too.\nerl_crash.dump\n\n# Also ignore archive artifacts (built via \"mix archive.build\").\n*.ez\n\n# Ignore package tarball (built via \"mix hex.build\").\nboruta_web-*.tar\n\n# Files matching config/*.secret.exs pattern contain sensitive\n# data and you should not commit them into version control.\n#\n# Alternatively, you may comment the line below and commit the\n# secrets files as long as you replace their contents by environment\n# variables.\n/config/*.secret.exs\n\n/tmp/\n\n/priv/static/admin\n"
  },
  {
    "path": "apps/boruta_web/config/config.exs",
    "content": "import Config\n\nconfig :boruta_web,\n  ecto_repos: [BorutaAuth.Repo, BorutaWeb.Repo]\n\nconfig :boruta_web, BorutaWeb.Endpoint,\n  url: [host: \"localhost\"],\n  secret_key_base: \"Caq0kwgjLGwxoEVPOxUhEiZ3AG2nADaNYi+ceWh2RuAgKF6vv/FfwqM/P7cDcNrR\",\n  render_errors: [view: BorutaWeb.ErrorView, accepts: ~w(html json)],\n  pubsub_server: BorutaWeb.PubSub\n\nconfig :mime, :types, %{\n  \"text/event-stream\" => [\"event-stream\"],\n  \"application/jwt\" => [\"jwt\"]\n}\n\nconfig :phoenix, :json_library, Jason\n\nconfig :swoosh, :api_client, Swoosh.ApiClient.Finch\n\nconfig :boruta, Boruta.Oauth,\n  repo: BorutaAuth.Repo,\n  contexts: [\n    resource_owners: BorutaIdentity.ResourceOwners\n  ],\n  max_ttl: [\n    authorization_code: 600\n  ],\n  issuer: System.get_env(\"BORUTA_OAUTH_BASE_URL\", \"http://localhost:4000\")\n\nconfig :boruta_auth, BorutaAuth.LogRotate,\n  max_retention_days: String.to_integer(System.get_env(\"MAX_LOG_RETENTION_DAYS\", \"60\"))\n\nconfig :hammer,\n  backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4,\n                                 cleanup_interval_ms: 60_000 * 10]}\n\nimport_config \"#{Mix.env()}.exs\"\n"
  },
  {
    "path": "apps/boruta_web/config/dev.exs",
    "content": "import Config\n\nconfig :boruta_web, BorutaWeb.Endpoint,\n  http: [port: System.get_env(\"BORUTA_OAUTH_PORT\", \"4000\") |> String.to_integer()],\n  debug_errors: true,\n  code_reloader: true,\n  watchers: [\n    npm: [\n      \"run\",\n      \"build:watch\",\n      cd: Path.expand(\"../../boruta_identity/assets/wallet\", __DIR__)\n    ]\n  ]\n\nconfig :boruta_web, BorutaWeb.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 5\n\nconfig :boruta_auth, BorutaAuth.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 5\n\nconfig :boruta_identity, BorutaIdentity.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 10\n\nconfig :boruta_admin, BorutaAdmin.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 10\n\nconfig :boruta_identity, Boruta.Accounts,\n  secret_key_base: \"secret\"\n\nconfig :libcluster,\n  topologies: [\n    example: [\n      strategy: Cluster.Strategy.Epmd,\n      config: [hosts: []],\n      connect: {:net_kernel, :connect_node, []},\n      disconnect: {:erlang, :disconnect_node, []},\n      list_nodes: {:erlang, :nodes, [:connected]},\n    ]\n  ]\n"
  },
  {
    "path": "apps/boruta_web/config/prod.exs",
    "content": "import Config\n"
  },
  {
    "path": "apps/boruta_web/config/test.exs",
    "content": "import Config\n\nconfig :boruta_web, BorutaWeb.Endpoint,\n  http: [port: 4002],\n  server: false,\n  secret_key_base: \"averysecretkeybaseaverysecretkeybaseaverysecretkeybaseaverysecretkeybase\"\n\nconfig :boruta_identity, BorutaIdentityWeb.Endpoint,\n  http: [port: 4003],\n  server: false,\n  secret_key_base: \"averysecretkeybaseaverysecretkeybaseaverysecretkeybaseaverysecretkeybase\"\n\nconfig :boruta_web, BorutaWeb.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_web_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool: Ecto.Adapters.SQL.Sandbox\n\nconfig :boruta_identity, BorutaIdentity.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_identity_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool: Ecto.Adapters.SQL.Sandbox\n\nconfig :boruta_auth, BorutaAuth.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_web_test\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool: Ecto.Adapters.SQL.Sandbox\n\nconfig :boruta_identity, Boruta.Accounts, secret_key_base: \"secret\"\n\nconfig :logger, level: :warn\n\nconfig :libcluster,\n  topologies: [\n    example: [\n      strategy: Cluster.Strategy.Epmd,\n      config: [hosts: []],\n      connect: {:net_kernel, :connect_node, []},\n      disconnect: {:erlang, :disconnect_node, []},\n      list_nodes: {:erlang, :nodes, [:connected]},\n    ]\n  ]\n\nconfig :boruta, Boruta.Oauth,\n  did_resolver_base_url: \"https://universalresolver.boruta.patatoid.fr/1.0\"\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta/status_resolver.ex",
    "content": "defmodule Boruta.Did.StatusResolver do\n  @moduledoc false\n\n  def resolve(_status) do\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/application.ex",
    "content": "defmodule BorutaWeb.Application do\n  @moduledoc false\n\n  require Logger\n\n  use Application\n\n  def start(_type, _args) do\n    children = [\n      BorutaWeb.Endpoint,\n      BorutaWeb.Repo,\n      %{\n        id: BorutaWeb.PresentationServer,\n        start: {BorutaWeb.PresentationServer, :start_link, []}\n      },\n      BorutaWeb.Plugs.RateLimit.Counter,\n      {Finch, name: FinchHttp},\n      {Cluster.Supervisor,\n       [Application.get_env(:libcluster, :topologies), [name: BorutaWeb.ClusterSupervisor]]},\n      {Phoenix.PubSub, name: BorutaWeb.PubSub}\n    ]\n\n    BorutaWeb.Logger.start()\n    setup_database()\n\n    opts = [strategy: :one_for_one, name: BorutaWeb.Supervisor]\n    Supervisor.start_link(children, opts)\n  end\n\n  def config_change(changed, _new, removed) do\n    BorutaWeb.Endpoint.config_change(changed, removed)\n    :ok\n  end\n\n  def setup_database do\n    Enum.each([BorutaAuth.Repo, BorutaIdentity.Repo], fn repo ->\n      repo.__adapter__.storage_up(repo.config)\n    end)\n\n    need_seeding? =\n      not (Ecto.Migrator.migrated_versions(BorutaAuth.Repo)\n           # First BorutaAuth migration\n           |> Enum.member?(20_201_129_024_828))\n\n    Enum.each([BorutaAuth.Repo, BorutaIdentity.Repo], fn repo ->\n      Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))\n    end)\n\n    if need_seeding? do\n      seed()\n    end\n\n    :ok\n  end\n\n  defp seed do\n    Code.eval_file(Path.join(:code.priv_dir(:boruta_auth), \"/repo/boruta.seeds.exs\"))\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/did_controller.ex",
    "content": "defmodule BorutaWeb.DidController do\n  use BorutaWeb, :controller\n\n  alias Boruta.Openid.VerifiableCredentials\n\n  def resolve_status(conn, %{\"status\" => salt}) do\n    clients = Boruta.Ecto.Admin.list_clients()\n    status = Enum.reduce_while(clients, :invaild, fn client, _acc ->\n      case VerifiableCredentials.Status.verify_status_token(client.private_key, salt) do\n        :expired -> {:cont, :expired}\n        :invalid -> {:cont, :invalid}\n        status -> {:halt, status}\n      end\n    end)\n\n    send_resp(conn, 200, Atom.to_string(status))\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/fallback_controller.ex",
    "content": "defmodule BorutaWeb.FallbackController do\n  @moduledoc \"\"\"\n  Translates controller action results into valid `Plug.Conn` responses.\n\n  See `Phoenix.Controller.action_fallback/1` for more details.\n  \"\"\"\n  use BorutaWeb, :controller\n\n  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do\n    conn\n    |> put_status(:unprocessable_entity)\n    |> put_view(BorutaWeb.ChangesetView)\n    |> render(\"error.json\", changeset: changeset)\n  end\n\n  def call(conn, {:error, :not_found}) do\n    conn\n    |> put_status(:not_found)\n    |> put_view(BorutaWeb.ErrorView)\n    |> render(:\"404\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/monitoring_controller.ex",
    "content": "defmodule BorutaWeb.MonitoringController do\n  use BorutaWeb, :controller\n\n  plug Phoenix.Ecto.CheckRepoStatus, [otp_app: :boruta_web] when action in [:healthcheck]\n\n  def healthcheck(conn, _params) do\n    send_resp(conn, 204, \"\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/oauth/authorize_controller.ex",
    "content": "defmodule BorutaWeb.AuthorizeError do\n  @enforce_keys [:message]\n  defexception [:message, :plug_status]\nend\n\ndefmodule BorutaWeb.Oauth.AuthorizeController do\n  @dialyzer :no_match\n  @behaviour Boruta.Oauth.AuthorizeApplication\n\n  use BorutaWeb, :controller\n\n  import BorutaIdentityWeb.Authenticable,\n    only: [request_param: 1, get_user_session: 1]\n\n  alias Boruta.ClientsAdapter\n  alias Boruta.Oauth\n  alias Boruta.Oauth.AuthorizationSuccess\n  alias Boruta.Oauth.AuthorizeResponse\n  alias Boruta.Oauth.Error\n  alias Boruta.Oauth.ResourceOwner\n  alias Boruta.Openid.CredentialOfferResponse\n  alias Boruta.Openid.SiopV2Response\n  alias Boruta.Openid.VerifiablePresentationResponse\n  alias BorutaIdentity.Accounts\n  alias BorutaIdentity.Accounts.Deliveries\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Accounts.VerifiableCredentials\n  alias BorutaIdentity.Accounts.VerifiablePresentations\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.ResourceOwners\n  alias BorutaIdentityWeb.Router.Helpers, as: IdentityRoutes\n  alias BorutaIdentityWeb.TemplateView\n  alias BorutaWeb.PresentationServer\n\n  @public_response_types [\"id_token\", \"code\", \"vp_token\"]\n\n  def authorize(%Plug.Conn{} = conn, _params) do\n    current_user = conn.assigns[:current_user]\n\n    conn = put_unsigned_request(conn)\n\n    with {:unchanged, conn} <- public_client?(conn),\n         {:unchanged, conn} <- prompt_redirection(conn, current_user),\n         {:unchanged, conn} <- max_age_redirection(conn, current_user),\n         {:unchanged, conn} <- check_preauthorized(conn),\n         {:unchanged, conn} <- redirect_if_mfa_required(conn, current_user),\n         {:unchanged, conn} <- preauthorize(conn, current_user) do\n      redirect(conn,\n        to:\n          IdentityRoutes.user_session_path(BorutaIdentityWeb.Endpoint, :new, %{\n            request: request_param(conn)\n          })\n      )\n    else\n      {:preauthorized, conn} ->\n        do_authorize(conn, current_user)\n\n      {:preauthorize, conn} ->\n        conn\n\n      {:authorize, conn} ->\n        conn\n\n      {:redirected, conn} ->\n        conn\n    end\n  end\n\n  def authenticated?(conn, %{\"code\" => code}) do\n    PresentationServer.start_presentation(code)\n\n    conn =\n      conn\n      |> put_resp_header(\"content-type\", \"text/event-stream\")\n      |> send_chunked(200)\n\n    receive do\n      {:authenticated, redirect_uri} ->\n        chunk(conn, \"event: authenticated\\ndata: #{redirect_uri}\\n\\n\")\n\n      {:message, message} ->\n        chunk(conn, \"event: message\\ndata: #{message}\\n\\n\")\n    end\n\n    conn\n  end\n\n  def public_client?(\n        %Plug.Conn{\n          query_params: %{\"response_type\" => \"code\" <> _rest, \"client_metadata\" => _client_metadata}\n        } = conn\n  ), do: {:preauthorized, conn}\n\n  def public_client?(\n        %Plug.Conn{\n          query_params: %{\"response_type\" => \"id_token\" <> _rest, \"client_metadata\" => _client_metadata}\n        } = conn\n  ), do: {:preauthorized, conn}\n\n  def public_client?(\n        %Plug.Conn{\n          query_params: %{\"response_type\" => \"vp_token\" <> _rest, \"client_metadata\" => _client_metadata}\n        } = conn\n  ), do: {:preauthorized, conn}\n\n  def public_client?(conn), do: {:unchanged, conn}\n\n  defp redirect_if_mfa_required(conn, current_user) do\n    case ensure_mfa(conn, current_user) do\n      :ok ->\n        {:unchanged, conn}\n\n      {:error, action, reason} ->\n        case get_session(conn, :session_chosen) do\n          true ->\n            conn =\n              conn\n              |> put_flash(:warning, reason)\n              |> redirect(\n                to:\n                  IdentityRoutes.user_session_path(\n                    BorutaIdentityWeb.Endpoint,\n                    action,\n                    %{\n                      request: request_param(conn)\n                    }\n                  )\n              )\n\n            {:redirected, conn}\n\n          _ ->\n            conn =\n              conn\n              |> put_flash(:warning, reason)\n              |> redirect(\n                to:\n                  IdentityRoutes.choose_session_path(BorutaIdentityWeb.Endpoint, :index, %{\n                    request: request_param(conn)\n                  })\n              )\n\n            {:redirected, conn}\n        end\n    end\n  end\n\n  defp ensure_mfa(%Plug.Conn{query_params: query_params} = conn, current_user) do\n    identity_provider =\n      IdentityProviders.get_identity_provider_by_client_id(query_params[\"client_id\"])\n\n    totp_authenticated = (get_session(conn, :totp_authenticated) || %{})[get_user_session(conn)]\n\n    webauthn_authenticated =\n      (get_session(conn, :webauthn_authenticated) || %{})[get_user_session(conn)]\n\n    do_enforce_mfa(identity_provider, current_user, totp_authenticated, webauthn_authenticated)\n  end\n\n  defp do_enforce_mfa(\n         %IdentityProvider{enforce_totp: false, enforce_webauthn: false},\n         %User{totp_registered_at: nil},\n         _totp_authenticated,\n         _webauthn_authenticated\n       ) do\n    :ok\n  end\n\n  defp do_enforce_mfa(\n         %IdentityProvider{webauthnable: true},\n         %User{webauthn_registered_at: %DateTime{}},\n         _totp_authenticated,\n         webauthn_authenticated\n       ) do\n    case webauthn_authenticated do\n      true ->\n        :ok\n\n      _ ->\n        {:error, :initialize_webauthn, \"Multi factor authentication required.\"}\n    end\n  end\n\n  defp do_enforce_mfa(\n         %IdentityProvider{totpable: true},\n         %User{totp_registered_at: %DateTime{}},\n         totp_authenticated,\n         _webauthn_authenticated\n       ) do\n    case totp_authenticated do\n      true ->\n        :ok\n\n      _ ->\n        {:error, :initialize_totp, \"Multi factor authentication required.\"}\n    end\n  end\n\n  defp do_enforce_mfa(\n         %IdentityProvider{enforce_webauthn: true},\n         %User{webauthn_registered_at: nil},\n         _totp_authenticated,\n         webauthn_authenticated\n       ) do\n    case webauthn_authenticated do\n      true ->\n        :ok\n\n      _ ->\n        {:error, :initialize_webauthn, \"Multi factor authentication required.\"}\n    end\n  end\n\n  defp do_enforce_mfa(\n         %IdentityProvider{enforce_totp: true},\n         %User{totp_registered_at: nil},\n         totp_authenticated,\n         _webauthn_authenticated\n       ) do\n    case totp_authenticated do\n      true ->\n        :ok\n\n      _ ->\n        {:error, :initialize_totp, \"Multi factor authentication required.\"}\n    end\n  end\n\n  defp do_enforce_mfa(_identity_provider, _user, _totp_authenticated, _webauthn_authenticated) do\n    :ok\n  end\n\n  defp check_preauthorized(conn) do\n    case get_session(conn, :preauthorizations) do\n      nil ->\n        {:unchanged, conn}\n\n      preauthorizations ->\n        case Map.get(preauthorizations, request_param(conn), false) do\n          false ->\n            {:unchanged, conn}\n\n          true ->\n            preauthorizations = get_session(conn, :preauthorizations) || %{}\n\n            {:preauthorized,\n             conn\n             |> put_session(\n               :preauthorizations,\n               Map.delete(preauthorizations, request_param(conn))\n             )}\n        end\n    end\n  end\n\n  defp max_age_redirection(\n         %Plug.Conn{query_params: %{\"max_age\" => max_age}} = conn,\n         %User{} = current_user\n       ) do\n    case login_expired?(current_user, max_age) do\n      true ->\n        conn =\n          redirect(conn,\n            to:\n              IdentityRoutes.user_session_path(BorutaIdentityWeb.Endpoint, :delete, %{\n                request: request_param(conn)\n              })\n          )\n\n        {:redirected, conn}\n\n      false ->\n        {:unchanged, conn}\n    end\n  end\n\n  defp max_age_redirection(conn, _current_user), do: {:unchanged, conn}\n\n  defp prompt_redirection(\n         %Plug.Conn{query_params: %{\"prompt\" => \"none\"} = query_params} = conn,\n         current_user\n       ) do\n    case ensure_mfa(conn, current_user) do\n      :ok ->\n        {:authorize, do_authorize(conn, current_user)}\n\n      {:error, _action, reason} ->\n        {:redirected,\n         authorize_error(conn, %Error{\n           status: :unauthorized,\n           format: :fragment,\n           redirect_uri: query_params[\"redirect_uri\"],\n           error: :login_required,\n           error_description: reason\n         })}\n    end\n  end\n\n  defp prompt_redirection(%Plug.Conn{query_params: %{\"prompt\" => \"login\"}} = conn, _current_user) do\n    conn =\n      redirect(conn,\n        to:\n          IdentityRoutes.user_session_path(BorutaIdentityWeb.Endpoint, :delete, %{\n            request: request_param(conn)\n          })\n      )\n\n    {:redirected, conn}\n  end\n\n  defp prompt_redirection(conn, _current_user), do: {:unchanged, conn}\n\n  defp preauthorize(conn, nil), do: {:unchanged, conn}\n\n  defp preauthorize(conn, current_user) do\n    conn =\n      conn\n      |> Oauth.preauthorize(\n        resource_owner(conn, current_user),\n        __MODULE__\n      )\n\n    {:preauthorize, conn}\n  end\n\n  defp do_authorize(conn, current_user) do\n    conn\n    |> delete_session(:preauthorizations)\n    |> Oauth.authorize(\n      resource_owner(conn, current_user),\n      __MODULE__\n    )\n  end\n\n  @impl Boruta.Oauth.AuthorizeApplication\n  def preauthorize_success(conn, %AuthorizationSuccess{\n        sub: \"did:\" <> _key = sub,\n        response_types: response_types\n      }) do\n    case Enum.map(@public_response_types, &String.split(&1, \" \"))\n    |> List.first()\n    |> Enum.member?(Enum.split(response_types, \" \") |> List.first()) do\n      true ->\n        Oauth.authorize(\n          conn,\n          %ResourceOwner{\n            sub: sub,\n            presentation_configuration:\n              VerifiablePresentations.public_presentation_configuration()\n          },\n          __MODULE__\n        )\n\n      false ->\n        preauthorize_success(conn, :preauthorized)\n    end\n  end\n\n  def preauthorize_success(conn, _authorization) do\n    session_chosen? = get_session(conn, :session_chosen) || false\n    preauthorizations = get_session(conn, :preauthorizations) || %{}\n\n    case session_chosen? do\n      true ->\n        conn\n        |> put_session(\n          :preauthorizations,\n          Map.merge(preauthorizations, %{request_param(conn) => true})\n        )\n        |> redirect(\n          to:\n            IdentityRoutes.user_consent_path(BorutaIdentityWeb.Endpoint, :index, %{\n              request: request_param(conn)\n            })\n        )\n\n      false ->\n        conn\n        |> redirect(\n          to:\n            IdentityRoutes.choose_session_path(BorutaIdentityWeb.Endpoint, :index, %{\n              request: request_param(conn)\n            })\n        )\n    end\n  end\n\n  @impl Boruta.Oauth.AuthorizeApplication\n  def preauthorize_error(conn, error) do\n    session_chosen? = get_session(conn, :session_chosen) || false\n\n    case {session_chosen?, conn.assigns[:current_user]} do\n      {true, _current_user} ->\n        authorize_error(conn, error)\n\n      {false, _current_user} ->\n        case request_param(conn) do\n          \"\" ->\n            authorize_error(conn, error)\n\n          request ->\n            conn\n            |> redirect(\n              to:\n                IdentityRoutes.choose_session_path(BorutaIdentityWeb.Endpoint, :index, %{\n                  request: request\n                })\n            )\n        end\n    end\n  end\n\n  @impl Boruta.Oauth.AuthorizeApplication\n  def authorize_success(\n        %Plug.Conn{query_params: query_params} = conn,\n        %AuthorizeResponse{} = response\n      ) do\n    # TODO get client_id, grant_type and resource_owner from response\n    client_id = query_params[\"client_id\"]\n    current_user = conn.assigns[:current_user]\n\n    :telemetry.execute(\n      [:authorization, :authorize, :success],\n      %{},\n      %{\n        access_token: response.access_token && response.access_token.value,\n        code: response.code && response.code.value,\n        type: response.type,\n        response_mode: response.response_mode,\n        expires_in: response.expires_in,\n        client_id: client_id,\n        current_user: current_user\n      }\n    )\n\n    conn\n    |> delete_session(:session_chosen)\n    |> redirect(external: AuthorizeResponse.redirect_to_url(response))\n  end\n\n  def authorize_success(\n        %Plug.Conn{} = conn,\n        %SiopV2Response{response_mode: \"post\"} = response\n      ) do\n    {:ok, idp} = Accounts.Utils.client_identity_provider(response.client.id)\n\n    template =\n      IdentityProviders.get_identity_provider_template!(idp.id, :cross_device_presentation)\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        resource_owner: response.code.resource_owner,\n        presentation_deeplink:\n          SiopV2Response.redirect_to_deeplink(response, fn code ->\n            uri = URI.parse(Boruta.Config.issuer())\n\n            %{uri | path: Routes.token_path(conn, :direct_post, code)}\n            |> URI.to_string()\n          end),\n        code: response.code.value\n      }\n    )\n  end\n\n  def authorize_success(\n        %Plug.Conn{} = conn,\n        %SiopV2Response{response_mode: \"direct_post\"} = response\n      ) do\n    # TODO log business event\n\n    conn\n    |> redirect(\n      external:\n        SiopV2Response.redirect_to_deeplink(response, fn code ->\n          uri = URI.parse(Boruta.Config.issuer())\n\n          %{uri | path: Routes.token_path(conn, :direct_post, code)}\n          |> URI.to_string()\n        end)\n    )\n  end\n\n  def authorize_success(\n        %Plug.Conn{} = conn,\n        %VerifiablePresentationResponse{response_mode: \"direct_post\"} = response\n      ) do\n    # TODO log business event\n\n    conn\n    |> redirect(\n      external:\n        VerifiablePresentationResponse.redirect_to_deeplink(response, fn code ->\n          uri = URI.parse(Boruta.Config.issuer())\n\n          %{uri | path: Routes.token_path(conn, :direct_post, code)}\n          |> URI.to_string()\n        end)\n    )\n  end\n\n  def authorize_success(\n        %Plug.Conn{} = conn,\n        %VerifiablePresentationResponse{response_mode: \"post\"} = response\n      ) do\n    {:ok, idp} = Accounts.Utils.client_identity_provider(response.client.id)\n\n    template =\n      IdentityProviders.get_identity_provider_template!(idp.id, :cross_device_presentation)\n\n    conn\n    |> put_layout(false)\n    |> put_view(TemplateView)\n    |> render(\"template.html\",\n      template: template,\n      assigns: %{\n        resource_owner: response.code.resource_owner,\n        presentation_deeplink:\n          VerifiablePresentationResponse.redirect_to_deeplink(response, fn code ->\n            uri = URI.parse(Boruta.Config.issuer())\n\n            %{uri | path: Routes.token_path(conn, :direct_post, code)}\n            |> URI.to_string()\n          end),\n        code: response.code.value\n      }\n    )\n  end\n\n  def authorize_success(\n        %Plug.Conn{query_params: query_params} = conn,\n        %CredentialOfferResponse{tx_code: tx_code, tx_code_required: true} = response\n      ) do\n    current_user = conn.assigns[:current_user]\n\n    case IdentityProviders.get_identity_provider_by_client_id(query_params[\"client_id\"]) do\n      %IdentityProvider{} = identity_provider ->\n        template =\n          IdentityProviders.get_identity_provider_template!(\n            identity_provider.id,\n            :credential_offer\n          )\n\n        case Deliveries.deliver_tx_code(identity_provider.backend, current_user, tx_code) do\n          :ok ->\n            conn\n            |> delete_session(:session_chosen)\n            |> put_layout(false)\n            |> put_view(TemplateView)\n            |> render(\"template.html\",\n              template: template,\n              assigns: %{\n                resource_owner: response.code.resource_owner,\n                credential_offer: response,\n                code: response.code.value\n              }\n            )\n\n          {:error, _error} ->\n            {:error, :bad_request}\n        end\n\n      nil ->\n        raise BorutaIdentity.Accounts.IdentityProviderError,\n              \"identity provider not configured for given OAuth client. Please contact your administrator.\"\n    end\n  end\n\n  def authorize_success(\n        %Plug.Conn{query_params: query_params} = conn,\n        %CredentialOfferResponse{} = response\n      ) do\n    client_id =\n      case query_params[\"client_id\"] do\n        \"did\" <> _key ->\n          ClientsAdapter.public!().id\n\n        client_id ->\n          client_id\n      end\n\n    case IdentityProviders.get_identity_provider_by_client_id(client_id) do\n      %IdentityProvider{} = identity_provider ->\n        template =\n          IdentityProviders.get_identity_provider_template!(\n            identity_provider.id,\n            :credential_offer\n          )\n\n        conn\n        |> delete_session(:session_chosen)\n        |> put_layout(false)\n        |> put_view(TemplateView)\n        |> render(\"template.html\",\n          template: template,\n          assigns: %{\n            resource_owner: response.code.resource_owner,\n            credential_offer: response,\n            code: response.code.value\n          }\n        )\n\n      nil ->\n        raise BorutaIdentity.Accounts.IdentityProviderError,\n              \"identity provider not configured for given OAuth client. Please contact your administrator.\"\n    end\n  end\n\n  @impl Boruta.Oauth.AuthorizeApplication\n  def authorize_error(\n        %Plug.Conn{} = conn,\n        %Error{status: :unauthorized, error: :login_required} = error\n      ) do\n    redirect(conn, external: Error.redirect_to_url(error))\n  end\n\n  def authorize_error(\n        %Plug.Conn{} = conn,\n        %Error{status: :unauthorized, error: :invalid_resource_owner} = error\n      ) do\n    emit_authorize_error_event(conn, error)\n\n    conn\n    |> delete_session(:session_chosen)\n    |> delete_session(:totp_authenticated)\n    |> redirect(\n      to:\n        IdentityRoutes.user_session_path(BorutaIdentityWeb.Endpoint, :new, %{\n          request: request_param(conn)\n        })\n    )\n  end\n\n  def authorize_error(conn, %Error{format: format} = error)\n      when not is_nil(format) do\n    emit_authorize_error_event(conn, error)\n\n    conn\n    |> delete_session(:session_chosen)\n    |> delete_session(:totp_authenticated)\n    |> redirect(external: Error.redirect_to_url(error))\n  end\n\n  def authorize_error(\n        conn,\n        %Error{status: status, error_description: error_description} = error\n      ) do\n    emit_authorize_error_event(conn, error)\n\n    conn\n    |> delete_session(:session_chosen)\n    |> delete_session(:totp_authenticated)\n    |> put_status(status)\n\n    raise %BorutaWeb.AuthorizeError{message: error_description, plug_status: status}\n  end\n\n  defp emit_authorize_error_event(%Plug.Conn{query_params: query_params} = conn, error) do\n    # TODO get client_id and grant_type from error\n    client_id = query_params[\"client_id\"]\n    current_user = conn.assigns[:current_user]\n\n    :telemetry.execute(\n      [:authorization, :authorize, :failure],\n      %{},\n      %{\n        status: error.status,\n        error: error.error,\n        error_description: error.error_description,\n        client_id: client_id,\n        current_user: current_user\n      }\n    )\n  end\n\n  defp login_expired?(current_user, max_age) do\n    now = DateTime.utc_now() |> DateTime.to_unix()\n\n    with \"\" <> max_age <- max_age,\n         {max_age, _} <- Integer.parse(max_age),\n         true <- now - DateTime.to_unix(current_user.last_login_at) >= max_age do\n      true\n    else\n      _ -> false\n    end\n  end\n\n  defp put_unsigned_request(%Plug.Conn{query_params: query_params} = conn) do\n    unsigned_request =\n      with request <- Map.get(query_params, \"request\", \"\"),\n           {:ok, params} <- Joken.peek_claims(request) do\n        params\n      else\n        _ -> %{}\n      end\n\n    query_params = Map.merge(query_params, unsigned_request)\n\n    %{conn | query_params: query_params}\n  end\n\n  defp resource_owner(conn, current_user) do\n    current_user = current_user || %User{}\n\n    anonymous_sub =\n      case conn.query_params[\"client_id\"] do\n        \"did:\" <> _key = did -> did\n        _ -> case conn.query_params[\"client_metadata\"] do\n          nil -> nil\n          _ -> \"unknown\"\n        end\n      end\n\n    scope =\n      case conn.query_params[\"scope\"] do\n        nil -> \"\"\n        scope -> scope\n      end\n\n    %ResourceOwner{\n      sub: current_user.id || anonymous_sub,\n      username: current_user.username,\n      last_login_at: current_user.last_login_at,\n      extra_claims:\n        Map.merge(ResourceOwners.metadata(current_user, scope), current_user.federated_metadata),\n      authorization_details: VerifiableCredentials.authorization_details(current_user, scope),\n      presentation_configuration: VerifiablePresentations.presentation_configuration(current_user)\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/oauth/introspect_controller.ex",
    "content": "defmodule BorutaWeb.Oauth.IntrospectController do\n  @behaviour Boruta.Oauth.IntrospectApplication\n\n  use BorutaWeb, :controller\n\n  alias Boruta.Oauth\n  alias Boruta.Oauth.Error\n  alias Boruta.Oauth.IntrospectResponse\n  alias BorutaWeb.OauthView\n\n  def introspect(%Plug.Conn{} = conn, _params) do\n    conn |> Oauth.introspect(__MODULE__)\n  end\n\n  @impl Boruta.Oauth.IntrospectApplication\n  def introspect_success(%Plug.Conn{body_params: body_params} = conn, %IntrospectResponse{} = response) do\n    # TODO get token from response\n    token = body_params[\"token\"]\n\n    :telemetry.execute(\n      [:authorization, :introspect, :success],\n      %{},\n      %{\n        active: response.active,\n        client_id: response.client_id,\n        sub: response.sub,\n        token: token\n      }\n    )\n\n    conn\n    |> put_view(OauthView)\n    |> render(\"introspect.#{get_format(conn)}\", response: response)\n  end\n\n  @impl Boruta.Oauth.IntrospectApplication\n  def introspect_error(%Plug.Conn{body_params: body_params} = conn, %Error{\n        status: status,\n        error: error,\n        error_description: error_description\n      }) do\n    # TODO get client_id and token from error\n    token = body_params[\"token\"]\n\n    :telemetry.execute(\n      [:authorization, :introspect, :failure],\n      %{},\n      %{\n        status: status,\n        error: error,\n        error_description: error_description,\n        token: token\n      }\n    )\n\n    conn\n    |> put_status(status)\n    |> put_view(OauthView)\n    |> render(\"error.json\", error: error, error_description: error_description)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/oauth/pushed_authorization_request_controller.ex",
    "content": "defmodule BorutaWeb.Oauth.PushedAuthorizationRequestController do\n  @behaviour Boruta.Oauth.PushedAuthorizationRequestApplication\n\n  use BorutaWeb, :controller\n\n  alias Boruta.Oauth\n  alias Boruta.Oauth.Error\n  alias BorutaWeb.OauthView\n\n  def pushed_authorization_request(%Plug.Conn{} = conn, _params) do\n    conn |> Oauth.pushed_authorization_request(__MODULE__)\n  end\n\n  @impl Boruta.Oauth.PushedAuthorizationRequestApplication\n  def request_stored(conn, response) do\n    conn\n    |> put_view(OauthView)\n    |> put_status(:created)\n    |> render(\"pushed_authorization_request.json\", response: response)\n  end\n\n  @impl Boruta.Oauth.PushedAuthorizationRequestApplication\n  def pushed_authorization_error(conn, %Error{\n    status: status,\n    error: error,\n    error_description: error_description\n  }) do\n    conn\n    |> put_view(OauthView)\n    |> put_status(status)\n    |> render(\"error.json\", error: error, error_description: error_description)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/oauth/revoke_controller.ex",
    "content": "defmodule BorutaWeb.Oauth.RevokeController do\n  @behaviour Boruta.Oauth.RevokeApplication\n\n  use BorutaWeb, :controller\n\n  alias Boruta.Oauth\n  alias Boruta.Oauth.Error\n  alias BorutaWeb.OauthView\n\n  def revoke(%Plug.Conn{} = conn, _params) do\n    conn |> Oauth.revoke(__MODULE__)\n  end\n\n  @impl Boruta.Oauth.RevokeApplication\n  def revoke_success(%Plug.Conn{body_params: body_params} = conn) do\n    # TODO get client_id and token from response\n    token = body_params[\"token\"]\n\n    :telemetry.execute(\n      [:authorization, :revoke, :success],\n      %{},\n      %{\n        token: token\n      }\n    )\n\n    send_resp(conn, 200, \"\")\n  end\n\n  @impl Boruta.Oauth.RevokeApplication\n  def revoke_error(%Plug.Conn{body_params: body_params} = conn, %Error{\n        status: status,\n        error: error,\n        error_description: error_description\n      }) do\n    # TODO get client_id and token from error\n    token = body_params[\"token\"]\n\n    :telemetry.execute(\n      [:authorization, :revoke, :failure],\n      %{},\n      %{\n        status: status,\n        error: error,\n        error_description: error_description,\n        token: token\n      }\n    )\n\n    conn\n    |> put_status(status)\n    |> put_view(OauthView)\n    |> render(\"error.json\", error: error, error_description: error_description)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/oauth/token_controller.ex",
    "content": "defmodule BorutaWeb.Oauth.TokenController do\n  @behaviour Boruta.Oauth.TokenApplication\n  @behaviour Boruta.Openid.DirectPostApplication\n\n  use BorutaWeb, :controller\n  import Boruta.Config, only: [issuer: 0]\n\n  alias Boruta.Oauth\n  alias Boruta.Oauth.Error\n  alias Boruta.Oauth.TokenResponse\n  alias Boruta.Openid\n  alias Boruta.Openid.DirectPostResponse\n  alias BorutaWeb.OauthView\n  alias BorutaWeb.PresentationServer\n\n  def token(%Plug.Conn{} = conn, _params) do\n    conn |> Oauth.token(__MODULE__)\n  end\n\n  @impl Boruta.Oauth.TokenApplication\n  def token_success(conn, %TokenResponse{} = response) do\n    # TODO get grant_type from response\n    :telemetry.execute(\n      [:authorization, :token, :success],\n      %{},\n      %{\n        client_id: response.token.client.id,\n        sub: response.token.sub,\n        access_token: response.access_token,\n        agent_token: response.agent_token,\n        token_type: response.token_type,\n        expires_in: response.expires_in,\n        refresh_token: response.refresh_token\n      }\n    )\n\n    conn\n    |> put_view(OauthView)\n    |> put_resp_header(\"pragma\", \"no-cache\")\n    |> put_resp_header(\"cache-control\", \"no-store\")\n    |> render(\"token.json\", response: response)\n  end\n\n  @impl Boruta.Oauth.TokenApplication\n  def token_error(conn, %Error{status: status, error: error, error_description: error_description}) do\n    # TODO get client_id and grant_type from error\n    :telemetry.execute(\n      [:authorization, :token, :failure],\n      %{},\n      %{\n        status: status,\n        error: error,\n        error_description: error_description\n      }\n    )\n\n    conn\n    |> put_status(status)\n    |> put_view(OauthView)\n    |> render(\"error.json\", error: error, error_description: error_description)\n  end\n\n  def direct_post(conn, %{\"code_id\" => code_id} = params) do\n    direct_post_params = %{\n      code_id: code_id,\n      metadata_policy: params[\"metadata_policy\"]\n    }\n\n    direct_post_params =\n      case params do\n        %{\"id_token\" => id_token} -> Map.put(direct_post_params, :id_token, id_token)\n        %{\"vp_token\" => vp_token} -> Map.put(direct_post_params, :vp_token, vp_token)\n        %{} -> direct_post_params\n      end\n\n    direct_post_params =\n      case params do\n        %{\"presentation_submission\" => presentation_submission} ->\n          Map.put(direct_post_params, :presentation_submission, presentation_submission)\n\n        %{} ->\n          direct_post_params\n      end\n\n    Openid.direct_post(conn, direct_post_params, __MODULE__)\n  end\n\n  @impl Boruta.Openid.DirectPostApplication\n  def code_not_found(conn) do\n    send_resp(conn, 404, \"\")\n  end\n\n  @impl Boruta.Openid.DirectPostApplication\n  def authentication_failure(conn, %Error{\n        redirect_uri: nil,\n        status: status,\n        error: error,\n        error_description: error_description\n      }) do\n    conn\n    |> put_status(status)\n    |> put_view(OauthView)\n    |> render(\"error.json\", error: error, error_description: error_description)\n  end\n\n  def authentication_failure(conn, %Error{} = error) do\n    redirect(conn, external: Error.redirect_to_url(error))\n  end\n\n  @impl Boruta.Openid.DirectPostApplication\n  def direct_post_success(conn, %DirectPostResponse{vp_token: vp_token} = response)\n      when not is_nil(vp_token) do\n    {:ok, %{\"kid\" => kid}} = Joken.peek_header(vp_token)\n\n    case tl(String.split(response.code.response_type, \" \")) do\n      [] ->\n        query =\n          %{\n            code: response.code.value,\n            state: response.state\n          }\n          |> URI.encode_query()\n\n        callback_uri = URI.parse(response.redirect_uri)\n\n        callback_uri =\n          %{callback_uri | host: callback_uri.host || \"\", query: query}\n          |> URI.to_string()\n\n        redirect(conn, external: callback_uri)\n\n      response_types ->\n        params = %{\n          \"client_id\" => kid,\n          \"response_type\" => Enum.join(response_types, \" \"),\n          \"client_metadata\" => \"{}\",\n          \"scope\" => response.code.requested_scope,\n          \"state\" => response.code.state,\n          \"code\" => response.code.value,\n          \"redirect_uri\" => response.redirect_uri\n        }\n\n        redirect_uri = issuer() <> Routes.authorize_path(conn, :authorize, params)\n\n        PresentationServer.authenticated(response.code.value, redirect_uri)\n        redirect(conn, external: redirect_uri)\n    end\n  end\n\n  def direct_post_success(\n        conn,\n        %DirectPostResponse{id_token: id_token, error: %Error{}} = response\n      )\n      when not is_nil(id_token) do\n    {:ok, %{\"kid\" => kid}} = Joken.peek_header(id_token)\n\n    params = %{\n      \"client_id\" => kid,\n      \"response_type\" => response.code.response_type,\n      \"client_metadata\" => \"{}\",\n      \"scope\" => response.code.requested_scope,\n      \"state\" => response.code.state,\n      \"code\" => response.code.value,\n      \"redirect_uri\" => response.redirect_uri\n    }\n\n    redirect_uri = issuer() <> Routes.authorize_path(conn, :authorize, params)\n\n    redirect(conn, external: redirect_uri)\n  end\n\n  def direct_post_success(conn, %DirectPostResponse{id_token: id_token, code: code} = response)\n      when not is_nil(id_token) do\n    {:ok, %{\"kid\" => kid}} = Joken.peek_header(id_token)\n\n    case tl(String.split(response.code.response_type, \" \")) do\n      [] ->\n        query =\n          %{\n            code: response.code.value,\n            state: response.state\n          }\n          |> URI.encode_query()\n\n        callback_uri = URI.parse(response.redirect_uri)\n\n        callback_uri =\n          %{callback_uri | host: callback_uri.host || \"\", query: query}\n          |> URI.to_string()\n\n        redirect(conn, external: callback_uri)\n\n      response_types ->\n        params = %{\n          \"client_id\" => kid,\n          \"response_type\" => Enum.join(response_types, \" \"),\n          \"client_metadata\" => \"{}\",\n          \"scope\" => response.code.requested_scope,\n          \"state\" => response.code.state,\n          \"code\" => response.code.value,\n          \"redirect_uri\" => response.redirect_uri\n        }\n\n        redirect_uri = issuer() <> Routes.authorize_path(conn, :authorize, params)\n\n        PresentationServer.authenticated(code.value, redirect_uri)\n        redirect(conn, external: redirect_uri)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/openid/credential_controller.ex",
    "content": "defmodule BorutaWeb.Openid.CredentialController do\n  @behaviour Boruta.Openid.CredentialApplication\n  use BorutaWeb, :controller\n\n  alias Boruta.Oauth.Error\n  alias Boruta.Openid\n  alias Boruta.Openid.CredentialResponse\n  alias Boruta.Openid.DeferedCredentialResponse\n  alias BorutaIdentity.Accounts.VerifiableCredentials\n  alias BorutaWeb.OauthView\n  alias BorutaWeb.PresentationServer\n\n  def credential(conn, params) do\n    Openid.credential(\n      conn,\n      params,\n      VerifiableCredentials.public_credential_configuration(),\n      __MODULE__\n    )\n  end\n\n  def defered_credential(conn, _params) do\n    Openid.defered_credential(conn, __MODULE__)\n  end\n\n  @impl Boruta.Openid.CredentialApplication\n  def credential_created(conn, %CredentialResponse{} = response) do\n    PresentationServer.message(response.token.previous_code, \"Credential issued\")\n\n    conn\n    |> put_view(OauthView)\n    |> render(\"credential.json\", credential_response: response)\n  end\n\n  def credential_created(conn, %DeferedCredentialResponse{} = response) do\n    conn\n    |> put_view(OauthView)\n    |> render(\"defered_credential.json\", credential_response: response)\n  end\n\n  @impl Boruta.Openid.CredentialApplication\n  def credential_failure(conn, %Error{\n        status: status,\n        error: error,\n        error_description: error_description\n      }) do\n    conn\n    |> put_status(status)\n    |> put_view(OauthView)\n    |> render(\"error.json\", error: error, error_description: error_description)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/openid/dynamic_registration_controller.ex",
    "content": "defmodule BorutaWeb.Openid.DynamicRegistrationController do\n  @behaviour Boruta.Openid.DynamicRegistrationApplication\n\n  alias Boruta.Ecto\n  alias Boruta.Oauth\n  alias Boruta.Openid\n  alias BorutaAuth.KeyPairs.KeyPair\n  alias BorutaIdentity.IdentityProviders\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaWeb.OpenidView\n\n  use BorutaWeb, :controller\n\n  def register_client(conn, params) do\n    registration_params =\n      Enum.map(params, fn {key, value} ->\n        {String.to_atom(key), value}\n      end)\n      |> Enum.into(%{})\n      |> Map.put(:id_token_signature_alg, \"RS256\")\n\n    Openid.register_client(conn, registration_params, __MODULE__)\n  end\n\n  @impl Boruta.Openid.DynamicRegistrationApplication\n  def client_registered(conn, %Oauth.Client{id: client_id} = client) do\n    with %Backend{id: backend_id} <- Backend.default!(),\n         {:ok, %IdentityProvider{id: identity_provider_id}} <-\n           IdentityProviders.create_identity_provider(%{\n             name: \"Created with dynamic registration for client #{client_id}\",\n             backend_id: backend_id\n           }),\n         {:ok, client} <- insert_global_key_pair(client),\n         {:ok, _client_identity_provider} <-\n           IdentityProviders.upsert_client_identity_provider(client_id, identity_provider_id) do\n      conn\n      |> put_view(OpenidView)\n      |> put_status(:created)\n      |> render(\"client.json\", client: client)\n    else\n      {:error, changeset} ->\n        registration_failure(conn, changeset)\n    end\n  end\n\n  @impl Boruta.Openid.DynamicRegistrationApplication\n  def registration_failure(conn, changeset) do\n    conn\n    |> put_view(OpenidView)\n    |> put_status(:bad_request)\n    |> render(\"registration_error.json\", changeset: changeset)\n  end\n\n  defp insert_global_key_pair(%Oauth.Client{id: client_id}) do\n    %KeyPair{public_key: public_key, private_key: private_key} = KeyPair.default!()\n\n    client = BorutaAuth.Repo.get!(Ecto.Client, client_id)\n\n    Ecto.Admin.regenerate_client_key_pair(client, public_key, private_key)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/openid/jwks_controller.ex",
    "content": "defmodule BorutaWeb.Openid.JwksController do\n  @behaviour Boruta.Openid.JwksApplication\n\n  alias Boruta.Openid\n  alias BorutaAuth.KeyPairs\n  alias BorutaWeb.OpenidView\n\n  use BorutaWeb, :controller\n\n  def jwks_index(conn, _params) do\n    Openid.jwks(conn, __MODULE__)\n  end\n\n  @impl Boruta.Openid.JwksApplication\n  def jwk_list(conn, jwk_keys) do\n    global_keys = KeyPairs.list_jwks()\n\n    keys =\n      Enum.uniq_by(\n        global_keys ++ jwk_keys,\n        fn %{\"kid\" => kid} -> kid end\n      )\n\n    conn\n    |> put_view(OpenidView)\n    |> render(\"jwks.json\", keys: keys)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/openid/userinfo_controller.ex",
    "content": "defmodule BorutaWeb.Openid.UserinfoController do\n  @behaviour Boruta.Openid.UserinfoApplication\n\n  use BorutaWeb, :controller\n\n  alias Boruta.Openid\n  alias Boruta.Openid.UserinfoResponse\n  alias BorutaWeb.OpenidView\n\n  def userinfo(conn, _params) do\n    Openid.userinfo(conn, __MODULE__)\n  end\n\n  @impl Boruta.Openid.UserinfoApplication\n  def userinfo_fetched(conn, response) do\n    conn\n    |> put_view(OpenidView)\n    |> put_resp_header(\"content-type\", UserinfoResponse.content_type(response))\n    |> render(\"userinfo.#{response.format}\", response: response)\n  end\n\n  @impl Boruta.Openid.UserinfoApplication\n  def unauthorized(conn, error) do\n    conn\n    |> put_resp_header(\n      \"www-authenticate\",\n      \"error=\\\"#{error.error}\\\", error_description=\\\"#{error.error_description}\\\"\"\n    )\n    |> send_resp(:unauthorized, \"\")\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/controllers/openid_controller.ex",
    "content": "defmodule BorutaWeb.OpenidController do\n  use BorutaWeb, :controller\n\n  alias BorutaWeb.OauthView\n\n  def well_known(conn, _params) do\n    scopes = Boruta.Ecto.Admin.list_scopes()\n\n    conn\n    |> put_view(OauthView)\n    |> render(\"well_known.json\", routes: Routes, scopes: scopes)\n  end\n\n  def openid_credential_issuer(conn, _params) do\n    conn\n    |> put_view(OauthView)\n    |> render(\"openid_credential_issuer.json\", routes: Routes)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/endpoint.ex",
    "content": "defmodule BorutaWeb.Endpoint do\n  use Phoenix.Endpoint, otp_app: :boruta_web\n\n  @session_options [\n    store: :cookie,\n    key: \"_boruta_web_key\",\n    signing_salt: \"OCKBuS86\"\n  ]\n\n  plug RemoteIp\n  # unless Mix.env() == :test, do: plug BorutaWeb.Plugs.RateLimit\n  plug Plug.Static,\n    at: \"/\",\n    from: :boruta_web,\n    gzip: false,\n    only: ~w(admin accounts css fonts images js favicon.ico robots.txt)\n\n  if code_reloading? do\n    socket \"/phoenix/live_reload/socket\", Phoenix.LiveReloader.Socket\n    plug Phoenix.LiveReloader\n    plug Phoenix.CodeReloader\n    plug Phoenix.Ecto.CheckRepoStatus, otp_app: :boruta_web\n  end\n\n  plug Plug.RequestId\n  plug Plug.Telemetry,\n    event_prefix: [:boruta_web, :endpoint],\n    log: {__MODULE__, :log_level, []}\n\n  plug Plug.Parsers,\n    parsers: [:urlencoded, :multipart, :json],\n    pass: [\"*/*\"],\n    json_decoder: Phoenix.json_library()\n\n  plug :put_secret_key_base\n  def put_secret_key_base(conn, _) do\n    put_in conn.secret_key_base, Application.get_env(:boruta_web, BorutaWeb.Endpoint)[:secret_key_base]\n  end\n\n  plug Plug.MethodOverride\n  plug Plug.Head\n  plug Plug.Session, @session_options\n  plug CORSPlug\n  plug BorutaWeb.Router\n\n  def log_level(%{private: %{BorutaIdentityWeb.Router => {[\"accounts\"], _}}}), do: false # logs are handled by boruta_identity\n  def log_level(%{path_info: [\"healthcheck\" | _]}), do: false\n  def log_level(%{path_info: path_info}) do\n    case Enum.member?(path_info, \"images\") do\n      true -> false\n      false -> :info\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/gettext.ex",
    "content": "defmodule BorutaWeb.Gettext do\n  @moduledoc \"\"\"\n  A module providing Internationalization with a gettext-based API.\n\n  By using [Gettext](https://hexdocs.pm/gettext),\n  your module gains a set of macros for translations, for example:\n\n      import BorutaWeb.Gettext\n\n      # Simple translation\n      gettext(\"Here is the string to translate\")\n\n      # Plural translation\n      ngettext(\"Here is the string to translate\",\n               \"Here are the strings to translate\",\n               3)\n\n      # Domain-based translation\n      dgettext(\"errors\", \"Here is the error message to translate\")\n\n  See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.\n  \"\"\"\n  use Gettext.Backend, otp_app: :boruta_web\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/logger.ex",
    "content": "defmodule BorutaWeb.Logger do\n  @moduledoc false\n\n  require Logger\n\n  def start do\n    handlers = [\n      {\n        :boruta_web_requests,\n        [:boruta_web, :endpoint, :stop],\n        &__MODULE__.boruta_web_request_handler/4\n      },\n      {\n        :authorization_authorize_success,\n        [:authorization, :authorize, :success],\n        &__MODULE__.authorization_authorize_success_handler/4\n      },\n      {\n        :authorization_authorize_failure,\n        [:authorization, :authorize, :failure],\n        &__MODULE__.authorization_authorize_failure_handler/4\n      },\n      {\n        :authorization_token_success,\n        [:authorization, :token, :success],\n        &__MODULE__.authorization_token_success_handler/4\n      },\n      {\n        :authorization_token_failure,\n        [:authorization, :token, :failure],\n        &__MODULE__.authorization_token_failure_handler/4\n      },\n      {\n        :authorization_introspect_success,\n        [:authorization, :introspect, :success],\n        &__MODULE__.authorization_introspect_success_handler/4\n      },\n      {\n        :authorization_introspect_failure,\n        [:authorization, :introspect, :failure],\n        &__MODULE__.authorization_introspect_failure_handler/4\n      },\n      {\n        :authorization_revoke_success,\n        [:authorization, :revoke, :success],\n        &__MODULE__.authorization_revoke_success_handler/4\n      },\n      {\n        :authorization_revoke_failure,\n        [:authorization, :revoke, :failure],\n        &__MODULE__.authorization_revoke_failure_handler/4\n      }\n    ]\n\n    for {handler_id, event_name, fun} <- handlers do\n      :telemetry.attach(handler_id, event_name, fun, :ok)\n    end\n  end\n\n  def boruta_web_request_handler(_, %{duration: duration}, %{conn: conn} = metadata, _) do\n    remote_ip = :inet.ntoa(conn.remote_ip)\n\n    case log_level(metadata[:options][:log], conn) do\n      false ->\n        :ok\n\n      level ->\n        Logger.log(\n          level,\n          fn ->\n            %{method: method, request_path: path, status: status, state: state} = conn\n            status = Integer.to_string(status)\n\n            [\n              \"boruta_web\",\n              ?\\s,\n              method,\n              ?\\s,\n              path,\n              \" - \",\n              connection_type(state),\n              ?\\s,\n              status,\n              \" from \",\n              remote_ip,\n              \" in \",\n              duration(duration)\n            ]\n          end,\n          type: :request\n        )\n    end\n  end\n\n  def authorization_authorize_success_handler(\n        _,\n        _measurements,\n        %{\n          access_token: access_token,\n          code: code,\n          type: type,\n          response_mode: response_mode,\n          expires_in: expires_in,\n          client_id: client_id,\n          current_user: current_user\n        },\n        _\n      ) do\n    log_line = [\n      \"boruta_web\",\n      ?\\s,\n      \"authorization\",\n      ?\\s,\n      \"authorize\",\n      \" - \",\n      \"success\",\n      log_attribute(\"client_id\", client_id),\n      log_attribute(\"sub\", current_user && current_user.uid),\n      log_attribute(\"type\", type),\n      log_attribute(\"response_mode\", response_mode),\n      log_attribute(\"access_token\", access_token),\n      log_attribute(\"code\", code),\n      log_attribute(\"expires_in\", expires_in)\n    ]\n\n    Logger.log(:info, fn -> log_line end, type: :business)\n  end\n\n  def authorization_authorize_failure_handler(\n        _,\n        _measurements,\n        %{\n          status: status,\n          error: error,\n          error_description: error_description,\n          client_id: client_id,\n          current_user: current_user\n        },\n        _\n      ) do\n    log_line = [\n      \"boruta_web\",\n      ?\\s,\n      \"authorization\",\n      ?\\s,\n      \"authorize\",\n      \" - \",\n      \"failure\",\n      log_attribute(\"client_id\", client_id),\n      log_attribute(\"sub\", current_user && current_user.uid),\n      log_attribute(\"status\", status),\n      log_attribute(\"error\", error),\n      log_attribute(\"error_description\", ~s{\"#{error_description}\"})\n    ]\n\n    Logger.log(:info, fn -> log_line end, type: :business)\n  end\n\n  def authorization_token_success_handler(\n        _,\n        _measurements,\n        %{\n          client_id: client_id,\n          sub: sub,\n          access_token: access_token,\n          agent_token: agent_token,\n          token_type: token_type,\n          expires_in: expires_in,\n          refresh_token: refresh_token\n        },\n        _\n      ) do\n    log_line = [\n      \"boruta_web\",\n      ?\\s,\n      \"authorization\",\n      ?\\s,\n      \"token\",\n      \" - \",\n      \"success\",\n      log_attribute(\"client_id\", client_id),\n      log_attribute(\"sub\", sub),\n      log_attribute(\"access_token\", access_token),\n      log_attribute(\"agent_token\", agent_token),\n      log_attribute(\"token_type\", token_type),\n      log_attribute(\"expires_in\", expires_in),\n      log_attribute(\"refresh_token\", refresh_token)\n    ]\n\n    Logger.log(:info, fn -> log_line end, type: :business)\n  end\n\n  def authorization_token_failure_handler(\n        _,\n        _measurements,\n        %{\n          status: status,\n          error: error,\n          error_description: error_description\n        },\n        _\n      ) do\n    log_line = [\n      \"boruta_web\",\n      ?\\s,\n      \"authorization\",\n      ?\\s,\n      \"token\",\n      \" - \",\n      \"failure\",\n      log_attribute(\"status\", status),\n      log_attribute(\"error\", error),\n      log_attribute(\"error_description\", ~s{\"#{error_description}\"})\n    ]\n\n    Logger.log(:info, fn -> log_line end, type: :business)\n  end\n\n  def authorization_introspect_success_handler(\n        _,\n        _measurements,\n        %{\n          active: active,\n          client_id: client_id,\n          sub: sub,\n          token: token\n        },\n        _\n      ) do\n    log_line = [\n      \"boruta_web\",\n      ?\\s,\n      \"authorization\",\n      ?\\s,\n      \"introspect\",\n      \" - \",\n      \"success\",\n      log_attribute(\"client_id\", client_id),\n      log_attribute(\"sub\", sub),\n      log_attribute(\"access_token\", token),\n      log_attribute(\"active\", active)\n    ]\n\n    Logger.log(:info, fn -> log_line end, type: :business)\n  end\n\n  def authorization_introspect_failure_handler(\n        _,\n        _measurements,\n        %{\n          status: status,\n          error: error,\n          error_description: error_description,\n          token: token\n        },\n        _\n      ) do\n    log_line = [\n      \"boruta_web\",\n      ?\\s,\n      \"authorization\",\n      ?\\s,\n      \"introspect\",\n      \" - \",\n      \"failure\",\n      log_attribute(\"access_token\", token),\n      log_attribute(\"status\", status),\n      log_attribute(\"error\", error),\n      log_attribute(\"error_description\", ~s{\"#{error_description}\"})\n    ]\n\n    Logger.log(:info, fn -> log_line end, type: :business)\n  end\n\n  def authorization_revoke_success_handler(\n        _,\n        _measurements,\n        %{\n          token: token\n        },\n        _\n      ) do\n    log_line = [\n      \"boruta_web\",\n      ?\\s,\n      \"authorization\",\n      ?\\s,\n      \"revoke\",\n      \" - \",\n      \"success\",\n      log_attribute(\"access_token\", token)\n    ]\n\n    Logger.log(:info, fn -> log_line end, type: :business)\n  end\n\n  def authorization_revoke_failure_handler(\n        _,\n        _measurements,\n        %{\n          status: status,\n          error: error,\n          error_description: error_description,\n          token: token\n        },\n        _\n      ) do\n    log_line = [\n      \"boruta_web\",\n      ?\\s,\n      \"authorization\",\n      ?\\s,\n      \"revoke\",\n      \" - \",\n      \"failure\",\n      log_attribute(\"access_token\", token),\n      log_attribute(\"status\", status),\n      log_attribute(\"error\", error),\n      log_attribute(\"error_description\", ~s{\"#{error_description}\"})\n    ]\n\n    Logger.log(:info, fn -> log_line end, type: :business)\n  end\n\n  defp log_attribute(_key, nil), do: \"\"\n  defp log_attribute(key, attribute), do: \" #{key}=#{attribute}\"\n\n  # From Phoenix.Logger\n  defp log_level(nil, _conn), do: :info\n  defp log_level(level, _conn) when is_atom(level), do: level\n\n  defp log_level({mod, fun, args}, conn) when is_atom(mod) and is_atom(fun) and is_list(args) do\n    apply(mod, fun, [conn | args])\n  end\n\n  defp connection_type(:set_chunked), do: \"chunked\"\n  defp connection_type(_), do: \"sent\"\n\n  defp duration(duration) do\n    duration = System.convert_time_unit(duration, :native, :microsecond)\n\n    if duration > 1000 do\n      [duration |> div(1000) |> Integer.to_string(), \"ms\"]\n    else\n      [Integer.to_string(duration), \"µs\"]\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/plugs/rate_limit.ex",
    "content": "defmodule BorutaWeb.Plugs.RateLimit do\n  @moduledoc false\n\n  defmodule Counter do\n    @moduledoc false\n    use Agent\n\n    @base_unit :millisecond\n\n    @memory_length 50\n\n    @time_unit_stamps [\n      millisecond: 1,\n      second: 1_000,\n      minute: 60 * 1_000\n    ]\n\n    def start_link(_args) do\n      Agent.start_link(fn -> %{} end, name: __MODULE__)\n    end\n\n    def get(ip, time_unit) do\n      Agent.get(__MODULE__, fn counter ->\n        Map.get(counter, ip, [])\n      end)\n      |> Enum.count(fn timestamp ->\n        timestamp > :os.system_time(@base_unit) - @time_unit_stamps[time_unit]\n      end)\n    end\n\n    def throttling_timeout(ip, count, time_unit, penality) do\n      now = :os.system_time(@base_unit)\n\n      request_rates = Agent.get(__MODULE__, fn counter ->\n        Map.get(counter, ip, [])\n        |> Enum.filter(fn timestamp ->\n          timestamp > now - @memory_length * @time_unit_stamps[time_unit]\n        end)\n      end)\n      |> Enum.group_by(fn timestamp ->\n        div(timestamp, @time_unit_stamps[time_unit])\n      end)\n\n      timeout = Enum.map(0..@memory_length - 1, fn i ->\n        current = floor(now - (i * @time_unit_stamps[time_unit]))\n\n        Map.get(request_rates, div(current, @time_unit_stamps[time_unit]), [])\n      end)\n      |> Enum.reverse()\n      |> Enum.map(fn\n        [] -> count / @time_unit_stamps[time_unit]\n        timestamps -> Enum.count(timestamps) / @time_unit_stamps[time_unit]\n      end)\n      |> Enum.reduce(1, fn factor, acc ->\n        acc * factor * (@time_unit_stamps[time_unit] / count)\n      end)\n\n      case timeout <= 1 do\n        true -> 0\n        false -> floor(timeout * penality)\n      end\n    end\n\n    def increment(ip, time_unit) do\n      Agent.update(__MODULE__, fn counter ->\n        timestamps =\n          Map.get(counter, ip, [])\n          |> Enum.filter(fn timestamp ->\n            timestamp > :os.system_time(@base_unit) - @memory_length * @time_unit_stamps[time_unit]\n          end)\n\n        Map.put(\n          counter,\n          ip,\n          [:os.system_time(@base_unit) | timestamps]\n        )\n      end)\n    end\n  end\n\n  use BorutaWeb, :controller\n\n  def init(options), do: options\n\n  def call(conn, options) do\n    remote_ip = :inet.ntoa(conn.remote_ip)\n\n    max_timeout = options[:timeout]\n\n    case Counter.throttling_timeout(\n      remote_ip,\n      options[:count],\n      options[:time_unit],\n      options[:penality]\n    ) do\n      timeout when timeout < max_timeout ->\n        :timer.sleep(timeout)\n\n        Counter.increment(remote_ip, options[:time_unit])\n        conn\n      _ ->\n      send_resp(conn, 429, \"\") |> halt()\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/presentation_server.ex",
    "content": "defmodule BorutaWeb.PresentationServer do\n  @moduledoc false\n\n  use GenServer\n\n  def start_link do\n    GenServer.start_link(__MODULE__, [], name: __MODULE__)\n  end\n\n  def init(_args) do\n    {:ok, %{presentations: %{}}}\n  end\n\n  def start_presentation(code) do\n    GenServer.call(__MODULE__, {:start_presentation, code})\n  end\n\n  def authenticated(code, redirect_uri) do\n    GenServer.cast(__MODULE__, {:authenticated, code, redirect_uri})\n  end\n\n  def message(code, message) do\n    GenServer.cast(__MODULE__, {:message, code, message})\n  end\n\n  def handle_call({:start_presentation, code}, {pid, _}, state) do\n    presentations = Map.put(\n      state.presentations,\n      code,\n      %{\n        start: :os.system_time(:microsecond),\n        pid: pid\n      }\n    )\n    {:reply, :ok, %{state | presentations: presentations}}\n  end\n\n  def handle_cast({:authenticated, code, redirect_uri}, state) do\n    case state.presentations[code] do\n      nil -> :ok\n      presentation ->\n        send(presentation[:pid], {:authenticated, redirect_uri})\n    end\n\n    {:noreply, Map.delete(state, code)}\n  end\n\n  def handle_cast({:message, code, message}, state) do\n    case state.presentations[code] do\n      nil -> :ok\n      presentation ->\n        send(presentation[:pid], {:message, message})\n    end\n\n    {:noreply, Map.delete(state, code)}\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/release.ex",
    "content": "defmodule BorutaWeb.Release do\n  @moduledoc false\n  @apps [:boruta_auth, :boruta_identity, :boruta_web]\n\n  def migrate do\n    for repo <- repos() do\n      repo.__adapter__.storage_up(repo.config)\n\n      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))\n    end\n  end\n\n  def rollback(repo, version) do\n    repo.__adapter__.storage_up(repo.config)\n\n    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))\n  end\n\n  def seed do\n    _started =\n      Enum.map(@apps, fn app ->\n        Application.ensure_all_started(app)\n      end)\n\n    Code.eval_file(Path.join(:code.priv_dir(:boruta_auth), \"/repo/boruta.seeds.exs\"))\n  end\n\n  def setup do\n    migrate()\n    seed()\n  end\n\n  defp repos do\n    Enum.flat_map(@apps, fn app ->\n      Application.load(app)\n      Application.fetch_env!(app, :ecto_repos)\n    end)\n    |> Enum.uniq()\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/repo.ex",
    "content": "defmodule BorutaWeb.Repo do\n  use Ecto.Repo,\n    otp_app: :boruta_web,\n    adapter: Ecto.Adapters.Postgres\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/router.ex",
    "content": "defmodule BorutaWeb.Router do\n  use BorutaWeb, :router\n  use Plug.ErrorHandler\n\n  alias BorutaWeb.Plugs.RateLimit\n\n  import BorutaIdentityWeb.Sessions,\n    only: [\n      fetch_current_user: 2\n    ]\n\n  pipeline :browser do\n    plug(:accepts, [\"html\"])\n    plug(:fetch_session)\n    plug(:fetch_flash)\n    plug(:protect_from_forgery)\n    plug(:put_secure_browser_headers)\n  end\n\n  pipeline :protected do\n    plug(:accepts, [\"html\"])\n    plug(:fetch_session)\n    plug(:fetch_flash)\n    plug(:protect_from_forgery)\n    plug(:put_secure_browser_headers)\n  end\n\n  pipeline :api do\n    plug(:accepts, [\"json\", \"jwt\", \"event-stream\"])\n    plug RateLimit, count: 10, time_unit: :second, penality: 500, timeout: 5_000\n  end\n\n  scope \"/\", BorutaWeb do\n    pipe_through(:api)\n\n    get(\"/.well-known/oauth-authorization-server\", OpenidController, :well_known)\n    get(\"/.well-known/openid-configuration\", OpenidController, :well_known)\n    get(\"/.well-known/openid-credential-issuer\", OpenidController, :openid_credential_issuer)\n  end\n\n  get(\"/healthcheck\", BorutaWeb.MonitoringController, :healthcheck, log: false)\n\n  forward(\"/accounts\", BorutaIdentityWeb.Endpoint)\n\n  scope \"/openid\", BorutaWeb do\n    pipe_through(:api)\n\n    options(\"/credential\", Openid.CredentialController, :options)\n    post(\"/credential\", Openid.CredentialController, :credential)\n    post(\"/defered-credential\", Openid.CredentialController, :defered_credential)\n    get(\"/jwks\", Openid.JwksController, :jwks_index)\n    get(\"/jwks/:client_id\", Openid.JwksController, :jwks_show)\n\n    # post(\"/register\", Openid.DynamicRegistrationController, :register_client)\n  end\n\n  scope \"/oauth\", BorutaWeb.Oauth do\n    pipe_through(:api)\n\n    post(\"/token\", TokenController, :token)\n    post(\"/introspect\", IntrospectController, :introspect)\n    post(\"/pushed_authorization_request\", PushedAuthorizationRequestController, :pushed_authorization_request)\n    post(\"/revoke\", RevokeController, :revoke)\n    options(\"/token\", TokenController, :options)\n    options(\"/introspect\", IntrospectController, :options)\n    options(\"/revoke\", RevokeController, :options)\n  end\n\n  scope \"/oauth\", BorutaWeb do\n    pipe_through(:api)\n\n    get(\"/userinfo\", Openid.UserinfoController, :userinfo)\n    post(\"/userinfo\", Openid.UserinfoController, :userinfo)\n  end\n\n  scope \"/oauth\", BorutaWeb.Oauth do\n    pipe_through([:browser, :fetch_current_user])\n\n    get(\"/authorize\", AuthorizeController, :authorize)\n  end\n\n  scope \"/did\", BorutaWeb do\n    pipe_through([:api])\n\n    get(\"/resolve_status/:status\", DidController, :resolve_status)\n  end\n\n  scope \"/openid\", BorutaWeb.Oauth do\n    pipe_through([:api])\n\n    post(\"/direct_post/:code_id\", TokenController, :direct_post)\n    options(\"/presentation_sse\", AuthorizeController, :options)\n    get(\"/presentation_sse\", AuthorizeController, :authenticated?)\n  end\n\n  @impl Plug.ErrorHandler\n  def handle_errors(conn, error) do\n    BorutaIdentityWeb.Router.handle_errors(conn, error)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/templates/error/404.html.eex",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Phoenix Framework</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <h1 class=\"ui icon header\">\n        <i class=\"question icon\"></i>\n        <div class=\"content\">\n          Page not found\n          <div class=\"sub header\">The page you requested was not found. Please contact your administrator.</div>\n        </div>\n      </h1>\n    </div>\n    </main>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/templates/error/500.html.eex",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\"/>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Boruta · Phoenix Framework</title>\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\" integrity=\"sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=\" crossorigin=\"anonymous\" />\n  </head>\n  <body style=\"height: 100%;\">\n    <div class=\"ui placeholder segment\" style=\"height: 100%;\">\n      <h1 class=\"ui icon header\">\n        <i class=\"exclamation icon\"></i>\n        <div class=\"content\">\n          Internal server error\n          <div class=\"sub header\">An unexpected error occured. Please contact your administrator.</div>\n        </div>\n      </h1>\n    </div>\n    </main>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/token.ex",
    "content": "defmodule BorutaWeb.Token do\n  @moduledoc false\n\n  use Joken.Config\n\n  def application_signer do\n    Joken.Signer.create(\n      \"HS512\",\n      Application.get_env(:boruta_web, BorutaWeb.Endpoint)[:secret_key_base]\n    )\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/views/error_helpers.ex",
    "content": "defmodule BorutaWeb.ErrorHelpers do\n  @moduledoc \"\"\"\n  Conveniences for translating and building error messages.\n  \"\"\"\n\n  use Phoenix.HTML\n\n  @doc \"\"\"\n  Generates tag for inlined form input errors.\n  \"\"\"\n  def error_tag(form, field) do\n    Enum.map(Keyword.get_values(form.errors, field), fn error ->\n      content_tag(:span, translate_error(error), class: \"help-block\")\n    end)\n  end\n\n  @doc \"\"\"\n  Translates an error message using gettext.\n  \"\"\"\n  def translate_error({msg, opts}) do\n    # When using gettext, we typically pass the strings we want\n    # to translate as a static argument:\n    #\n    #     # Translate \"is invalid\" in the \"errors\" domain\n    #     dgettext(\"errors\", \"is invalid\")\n    #\n    #     # Translate the number of files with plural rules\n    #     dngettext(\"errors\", \"1 file\", \"%{count} files\", count)\n    #\n    # Because the error messages we show in our forms and APIs\n    # are defined inside Ecto, we need to translate them dynamically.\n    # This requires us to call the Gettext module passing our gettext\n    # backend as first argument.\n    #\n    # Note we use the \"errors\" domain, which means translations\n    # should be written to the errors.po file. The :count option is\n    # set by Ecto and indicates we should also apply plural rules.\n    if count = opts[:count] do\n      Gettext.dngettext(BorutaWeb.Gettext, \"errors\", msg, msg, count, opts)\n    else\n      Gettext.dgettext(BorutaWeb.Gettext, \"errors\", msg, opts)\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/views/error_view.ex",
    "content": "defmodule BorutaWeb.ErrorView do\n  use BorutaWeb, :view\n\n  # If you want to customize a particular status code\n  # for a certain format, you may uncomment below.\n  # def render(\"500.html\", _assigns) do\n  #   \"Internal Server Error\"\n  # end\n\n  # By default, Phoenix returns the status message from\n  # the template name. For example, \"404.html\" becomes\n  # \"Not Found\".\n  def template_not_found(template, _assigns) do\n    Phoenix.Controller.status_message_from_template(template)\n  end\n\n  def render(\"error.json\", %{error: error, message: message}) do\n    %{error: error, message: message}\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/views/oauth_view.ex",
    "content": "defmodule BorutaWeb.OauthView do\n  use BorutaWeb, :view\n\n  alias Boruta.Oauth.Client\n  alias BorutaIdentity.Accounts.VerifiableCredentials\n  alias BorutaWeb.Token\n\n  def render(\"token.json\", %{response: %Boruta.Oauth.TokenResponse{} = response}) do\n    response\n  end\n\n  def render(\"introspect.json\", %{response: %Boruta.Oauth.IntrospectResponse{active: false}}) do\n    %{\"active\" => false}\n  end\n\n  def render(\"introspect.json\", %{response: %Boruta.Oauth.IntrospectResponse{} = response}) do\n    response\n  end\n\n  def render(\"introspect.jwt\", %{response: %Boruta.Oauth.IntrospectResponse{active: false}}) do\n    payload = %{\"active\" => false}\n\n    {:ok, token, _payload} = Joken.encode_and_sign(payload, Token.application_signer())\n\n    token\n  end\n\n  def render(\"introspect.jwt\", %{\n        response: %Boruta.Oauth.IntrospectResponse{private_key: private_key} = response\n      }) do\n    payload =\n      response\n      |> Map.delete(:private_key)\n      |> Map.from_struct()\n\n    signer = Joken.Signer.create(\"RS512\", %{\"pem\" => private_key})\n    {:ok, token, _payload} = Joken.encode_and_sign(payload, signer)\n\n    token\n  end\n\n  def render(\"error.json\", %{error: error, error_description: error_description}) do\n    %{\n      error: error,\n      error_description: error_description\n    }\n  end\n\n  def render(\"well_known.json\", %{routes: routes, scopes: scopes}) do\n    issuer = Boruta.Config.issuer()\n\n    %{\n      \"issuer\" => issuer,\n      \"authorization_endpoint\" => issuer <> routes.authorize_path(BorutaWeb.Endpoint, :authorize),\n      \"token_endpoint\" => issuer <> routes.token_path(BorutaWeb.Endpoint, :token),\n      \"userinfo_endpoint\" => issuer <> routes.userinfo_path(BorutaWeb.Endpoint, :userinfo),\n      \"jwks_uri\" => issuer <> routes.jwks_path(BorutaWeb.Endpoint, :jwks_index),\n      # \"registration_endpoint\" =>\n      #   issuer <> routes.dynamic_registration_path(BorutaWeb.Endpoint, :register_client),\n      \"pushed_authorization_request_endpoint\" =>\n        issuer <> routes.pushed_authorization_request_path(BorutaWeb.Endpoint, :pushed_authorization_request),\n      \"grant_types_supported\" => [\n        \"client_credentials\",\n        \"password\",\n        \"implicit\",\n        \"authorization_code\",\n        \"refresh_token\"\n      ],\n      \"response_types_supported\" => [\n        \"code\",\n        \"token\",\n        \"id_token\",\n        \"code token\",\n        \"code id_token\",\n        \"token id_token\",\n        \"code id_token token\"\n      ],\n      \"scopes_supported\" => Enum.map(scopes, fn scope -> scope.name end),\n      \"response_modes_supported\" => [\"query\", \"fragment\"],\n      \"subject_types_supported\" => [\"public\"],\n      \"token_endpoint_auth_methods_supported\" => [\n        \"client_secret_basic\",\n        \"client_secret_post\",\n        \"client_secret_jwt\",\n        \"private_key_jwt\"\n      ],\n      \"request_object_signing_alg_values_supported\" => Client.Crypto.signature_algorithms(),\n      \"id_token_signing_alg_values_supported\" => Client.Crypto.signature_algorithms(),\n      \"userinfo_signing_alg_values_supported\" => Client.Crypto.signature_algorithms(),\n      \"credential_issuer\" => issuer,\n      \"credential_endpoint\" => issuer <> routes.credential_path(BorutaWeb.Endpoint, :credential),\n      \"defered_credential_endpoint\" => issuer <> routes.credential_path(BorutaWeb.Endpoint, :defered_credential),\n      \"credential_configurations_supported\" =>\n        VerifiableCredentials.credential_configurations_supported(),\n      \"credentials_supported\" => VerifiableCredentials.credentials_supported()\n    }\n  end\n\n  def render(\"openid_credential_issuer.json\", %{routes: routes}) do\n    issuer = Boruta.Config.issuer()\n\n    %{\n      \"issuer\" => issuer,\n      \"token_endpoint\" => issuer <> routes.token_path(BorutaWeb.Endpoint, :token),\n      \"credential_issuer\" => issuer,\n      \"credential_endpoint\" => issuer <> routes.credential_path(BorutaWeb.Endpoint, :credential),\n      \"credential_configurations_supported\" =>\n        VerifiableCredentials.credential_configurations_supported(),\n      \"credentials_supported\" => VerifiableCredentials.credentials_supported()\n    }\n  end\n\n  def render(\"credential.json\", %{credential_response: credential_response}) do\n    %{\n      format: credential_response.format,\n      credential: credential_response.credential\n    }\n    # TODO use associated access token c_nonce\n    |> Map.put(:c_nonce, \"boruta\")\n    |> Map.put(:c_nonce_expires_in, 3600)\n  end\n\n  def render(\"defered_credential.json\", %{credential_response: credential_response}) do\n    %{\n      acceptance_token: credential_response.acceptance_token,\n      c_nonce: credential_response.c_nonce,\n      c_nonce_expires_in: credential_response.c_nonce_expires_in\n    }\n  end\n\n  def render(\"pushed_authorization_request.json\", %{\n        response: %Boruta.Oauth.PushedAuthorizationResponse{} = response\n      }) do\n    response\n  end\n\n  defimpl Jason.Encoder, for: Boruta.Oauth.TokenResponse do\n    def encode(\n          %Boruta.Oauth.TokenResponse{\n            token_type: token_type,\n            access_token: access_token,\n            agent_token: agent_token,\n            id_token: id_token,\n            c_nonce: c_nonce,\n            expires_in: expires_in,\n            refresh_token: refresh_token,\n            authorization_details: authorization_details\n          },\n          options\n        ) do\n      response = %{\n        token_type: token_type,\n        expires_in: expires_in,\n        refresh_token: refresh_token,\n        c_nonce: c_nonce\n      }\n\n      response =\n        case id_token do\n          nil -> response\n          id_token -> Map.put(response, :id_token, id_token)\n        end\n\n      response =\n        case access_token do\n          nil -> response\n          access_token -> Map.put(response, :access_token, access_token)\n        end\n\n      response =\n        case agent_token do\n          nil -> response\n          agent_token -> Map.put(response, :agent_token, agent_token)\n        end\n\n      response =\n        case authorization_details do\n          nil ->\n            response\n\n          _authorization_details ->\n            response\n            |> Map.put(:authorization_details, authorization_details)\n            |> Map.put(:c_nonce_expires_in, 3600)\n        end\n\n      Jason.Encode.map(\n        response,\n        options\n      )\n    end\n  end\n\n  defimpl Jason.Encoder, for: Boruta.Oauth.IntrospectResponse do\n    def encode(\n          %Boruta.Oauth.IntrospectResponse{\n            active: false\n          },\n          options\n        ) do\n      Jason.Encode.map(%{active: false}, options)\n    end\n\n    def encode(\n          %Boruta.Oauth.IntrospectResponse{\n            active: true,\n            client_id: client_id,\n            username: username,\n            scope: scope,\n            sub: sub,\n            iss: iss,\n            exp: exp,\n            iat: iat\n          },\n          options\n        ) do\n      Jason.Encode.map(\n        %{\n          active: true,\n          client_id: client_id,\n          username: username,\n          scope: scope,\n          sub: sub,\n          iss: iss,\n          exp: exp,\n          iat: iat\n        },\n        options\n      )\n    end\n  end\n\n  defimpl Jason.Encoder, for: Boruta.Oauth.PushedAuthorizationResponse do\n    def encode(%Boruta.Oauth.PushedAuthorizationResponse{} = response, options) do\n      Jason.Encode.map(\n        %{\n          request_uri: response.request_uri,\n          expires_in: response.expires_in\n        },\n        options\n      )\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web/views/openid_view.ex",
    "content": "defmodule BorutaWeb.OpenidView do\n  use BorutaWeb, :view\n\n  alias Boruta.Openid.UserinfoResponse\n\n  def render(\"userinfo.json\", %{response: response}) do\n    UserinfoResponse.payload(response)\n  end\n\n  def render(\"jwks.json\", %{keys: keys}) do\n    %{\n      keys: keys\n    }\n  end\n\n  def render(\"jwk.json\", %{client: %Boruta.Ecto.Client{id: client_id, public_key: public_key}}) do\n    {_type, jwk} = public_key |> :jose_jwk.from_pem() |> :jose_jwk.to_map()\n\n    %{\n      keys: [Map.put(jwk, :kid, client_id)]\n    }\n  end\n\n  def render(\"userinfo.jwt\", %{response: response}) do\n    UserinfoResponse.payload(response)\n  end\n\n  def render(\"show.json\", %{client: client}) do\n    %{data: render_one(client, __MODULE__, \"client.json\")}\n  end\n\n  def render(\"client.json\", %{client: client}) do\n    %{\n      client_id: client.id,\n      client_secret: client.secret,\n      client_secret_expires_at: 0\n    }\n  end\n\n  def render(\"registration_error.json\", %{changeset: changeset}) do\n    %{\n      error: \"invalid_client_metadata\",\n      error_description: errors_full_message(changeset)\n    }\n  end\n\n  defp errors_full_message(changeset) do\n    Ecto.Changeset.traverse_errors(changeset, &translate_error/1)\n    |> Enum.map_join(\", \", fn {attribute, messages} ->\n      \"#{attribute} : #{Enum.join(messages, \", \")}\"\n    end)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/boruta_web.ex",
    "content": "defmodule BorutaWeb do\n  @moduledoc \"\"\"\n  The entrypoint for defining your web interface, such\n  as controllers, views, channels and so on.\n\n  This can be used in your application as:\n\n      use BorutaWeb, :controller\n      use BorutaWeb, :view\n\n  The definitions below will be executed for every view,\n  controller, etc, so keep them short and clean, focused\n  on imports, uses and aliases.\n\n  Do NOT define functions inside the quoted expressions\n  below. Instead, define any helper function in modules\n  and import those modules here.\n  \"\"\"\n\n  def controller do\n    quote do\n      use Phoenix.Controller, namespace: BorutaWeb\n      import Plug.Conn\n      import BorutaWeb.Gettext\n      alias BorutaWeb.Router.Helpers, as: Routes\n    end\n  end\n\n  def view do\n    quote do\n      use Phoenix.View,\n        root: \"lib/boruta_web/templates\",\n        namespace: BorutaWeb\n\n      # Import convenience functions from controllers\n      import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]\n\n      # Use all HTML functionality (forms, tags, etc)\n      use Phoenix.HTML\n\n      import BorutaWeb.ErrorHelpers\n      import BorutaWeb.Gettext\n      alias BorutaWeb.Router.Helpers, as: Routes\n    end\n  end\n\n  def router do\n    quote do\n      use Phoenix.Router\n      import Plug.Conn\n      import Phoenix.Controller\n    end\n  end\n\n  def channel do\n    quote do\n      use Phoenix.Channel\n      import BorutaWeb.Gettext\n    end\n  end\n\n  @doc \"\"\"\n  When used, dispatch to the appropriate controller/view/etc.\n  \"\"\"\n  defmacro __using__(which) when is_atom(which) do\n    apply(__MODULE__, which, [])\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/lib/mix/tasks/server.ex",
    "content": "defmodule Mix.Tasks.Boruta.Server do\n  @moduledoc false\n\n  use Mix.Task\n\n  def run(args) do\n    Application.put_env(:boruta_gateway, :server, true, persistent: true)\n    Mix.Tasks.Phx.Server.run(args)\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/mix.exs",
    "content": "defmodule BorutaWeb.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :boruta_web,\n      version: \"0.1.0\",\n      build_path: \"../../_build\",\n      deps_path: \"../../deps\",\n      lockfile: \"../../mix.lock\",\n      elixir: \"~> 1.5\",\n      elixirc_paths: elixirc_paths(Mix.env()),\n      compilers: [:phoenix] ++ Mix.compilers(),\n      start_permanent: Mix.env() == :prod,\n      aliases: aliases(),\n      deps: deps()\n    ]\n  end\n\n  # Configuration for the OTP application.\n  #\n  # Type `mix help compile.app` for more information.\n  def application do\n    [\n      mod: {BorutaWeb.Application, []},\n      extra_applications: [:logger, :runtime_tools]\n    ]\n  end\n\n  # Specifies which paths to compile per environment.\n  defp elixirc_paths(:test), do: [\"lib\", \"test/support\"]\n  defp elixirc_paths(_), do: [\"lib\"]\n\n  # Specifies your project dependencies.\n  #\n  # Type `mix help deps` for examples and options.\n  defp deps do\n    [\n      {:boruta_auth, in_umbrella: true},\n      {:boruta_identity, in_umbrella: true},\n      {:bypass, \"~> 2.1.0\", only: :test},\n      {:cors_plug, \"~> 3.0\"},\n      {:ex_machina, \"~> 2.4\", only: :test},\n      {:finch, \"~> 0.8\"},\n      {:gettext, \"~> 0.11\"},\n      {:hammer, \"~> 6.1\"},\n      {:jason, \"~> 1.0\"},\n      {:joken, \"~> 2.3\"},\n      {:libcluster, \"~> 3.2.1\"},\n      {:owl, \"~> 0.8.0\"},\n      {:phoenix, \"~> 1.6.0\", override: true},\n      {:phoenix_ecto, \"~> 4.1\"},\n      {:phoenix_html, \"~> 3.0\"},\n      {:phoenix_live_reload, \"~> 1.2\", only: :dev},\n      {:phoenix_pubsub, \"~> 2.0\"},\n      {:plug_cowboy, \"~> 2.0\"},\n      {:remote_ip, \"~> 1.1\"},\n      {:telemetry_metrics, \"~> 0.6\"},\n      {:telemetry_poller, \"~> 0.5\"}\n    ]\n  end\n\n  # Aliases are shortcuts or tasks specific to the current project.\n  # For example, we extend the test task to create and migrate the database.\n  #\n  # See the documentation for `Mix` for more info on aliases.\n  defp aliases do\n    [\n      \"ecto.setup\": [\"ecto.create\", \"ecto.migrate\", \"run priv/repo/boruta.seeds.exs\"],\n      \"ecto.reset\": [\"ecto.drop\", \"ecto.setup\"],\n      test: [\"ecto.create --quiet\", \"ecto.migrate\", \"test\"]\n    ]\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/priv/gettext/en/LC_MESSAGES/errors.po",
    "content": "## `msgid`s in this file come from POT (.pot) files.\n##\n## Do not add, change, or remove `msgid`s manually here as\n## they're tied to the ones in the corresponding POT file\n## (with the same domain).\n##\n## Use `mix gettext.extract --merge` or `mix gettext.merge`\n## to merge POT files into PO files.\nmsgid \"\"\nmsgstr \"\"\n\"Language: en\\n\"\n\n## From Ecto.Changeset.cast/4\nmsgid \"can't be blank\"\nmsgstr \"\"\n\n## From Ecto.Changeset.unique_constraint/3\nmsgid \"has already been taken\"\nmsgstr \"\"\n\n## From Ecto.Changeset.put_change/3\nmsgid \"is invalid\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_acceptance/3\nmsgid \"must be accepted\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_format/3\nmsgid \"has invalid format\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_subset/3\nmsgid \"has an invalid entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_exclusion/3\nmsgid \"is reserved\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_confirmation/3\nmsgid \"does not match confirmation\"\nmsgstr \"\"\n\n## From Ecto.Changeset.no_assoc_constraint/3\nmsgid \"is still associated with this entry\"\nmsgstr \"\"\n\nmsgid \"are still associated with this entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_length/3\nmsgid \"should be %{count} character(s)\"\nmsgid_plural \"should be %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have %{count} item(s)\"\nmsgid_plural \"should have %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at least %{count} character(s)\"\nmsgid_plural \"should be at least %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at least %{count} item(s)\"\nmsgid_plural \"should have at least %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at most %{count} character(s)\"\nmsgid_plural \"should be at most %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at most %{count} item(s)\"\nmsgid_plural \"should have at most %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n## From Ecto.Changeset.validate_number/3\nmsgid \"must be less than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be less than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be equal to %{number}\"\nmsgstr \"\"\n"
  },
  {
    "path": "apps/boruta_web/priv/gettext/errors.pot",
    "content": "## This is a PO Template file.\n##\n## `msgid`s here are often extracted from source code.\n## Add new translations manually only if they're dynamic\n## translations that can't be statically extracted.\n##\n## Run `mix gettext.extract` to bring this file up to\n## date. Leave `msgstr`s empty as changing them here has no\n## effect: edit them in PO (`.po`) files instead.\n\n## From Ecto.Changeset.cast/4\nmsgid \"can't be blank\"\nmsgstr \"\"\n\n## From Ecto.Changeset.unique_constraint/3\nmsgid \"has already been taken\"\nmsgstr \"\"\n\n## From Ecto.Changeset.put_change/3\nmsgid \"is invalid\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_acceptance/3\nmsgid \"must be accepted\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_format/3\nmsgid \"has invalid format\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_subset/3\nmsgid \"has an invalid entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_exclusion/3\nmsgid \"is reserved\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_confirmation/3\nmsgid \"does not match confirmation\"\nmsgstr \"\"\n\n## From Ecto.Changeset.no_assoc_constraint/3\nmsgid \"is still associated with this entry\"\nmsgstr \"\"\n\nmsgid \"are still associated with this entry\"\nmsgstr \"\"\n\n## From Ecto.Changeset.validate_length/3\nmsgid \"should be %{count} character(s)\"\nmsgid_plural \"should be %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have %{count} item(s)\"\nmsgid_plural \"should have %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at least %{count} character(s)\"\nmsgid_plural \"should be at least %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at least %{count} item(s)\"\nmsgid_plural \"should have at least %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should be at most %{count} character(s)\"\nmsgid_plural \"should be at most %{count} character(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\nmsgid \"should have at most %{count} item(s)\"\nmsgid_plural \"should have at most %{count} item(s)\"\nmsgstr[0] \"\"\nmsgstr[1] \"\"\n\n## From Ecto.Changeset.validate_number/3\nmsgid \"must be less than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than %{number}\"\nmsgstr \"\"\n\nmsgid \"must be less than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be greater than or equal to %{number}\"\nmsgstr \"\"\n\nmsgid \"must be equal to %{number}\"\nmsgstr \"\"\n"
  },
  {
    "path": "apps/boruta_web/priv/repo/migrations/.keep",
    "content": ""
  },
  {
    "path": "apps/boruta_web/test/boruta_web/controllers/credential_controller_test.exs",
    "content": "defmodule BorutaWeb.CredentialControllerTest do\n  use BorutaWeb.ConnCase\n\n  import Boruta.Factory\n  import BorutaIdentity.AccountsFixtures\n\n  alias Boruta.Config\n  alias Boruta.Ecto.Token\n  alias Boruta.Internal.Signatures\n  alias BorutaIdentity.Accounts.User\n\n  setup %{conn: conn} do\n    {:ok, conn: conn}\n  end\n\n  @tag :skip\n  test \"returns a credential with a valid credential type\", %{conn: conn} do\n    {_, public_jwk} = public_key_fixture() |> JOSE.JWK.from_pem() |> JOSE.JWK.to_map()\n\n    signer =\n      Joken.Signer.create(\"RS256\", %{\"pem\" => private_key_fixture()}, %{\n        \"jwk\" => public_jwk,\n        \"typ\" => \"openid4vci-proof+jwt\"\n      })\n\n    {:ok, token, _claims} =\n      Signatures.Token.generate_and_sign(\n        %{\n          \"aud\" => Config.issuer(),\n          \"iat\" => :os.system_time(:seconds)\n        },\n        signer\n      )\n\n    proof = %{\n      \"proof_type\" => \"jwt\",\n      \"jwt\" => token\n    }\n\n    credential_params = %{\n      \"credential_identifier\" => \"VerifiableCredential\",\n      \"format\" => \"jwt_vc\",\n      \"proof\" => proof\n    }\n\n    backend =\n      BorutaIdentity.Factory.insert(:backend,\n        verifiable_credentials: [\n          %{\n            \"display\" => %{\n              \"background_color\" => \"#53b29f\",\n              \"logo\" => %{\n                \"alt_text\" => \"Boruta PoC logo\",\n                \"url\" => \"https://io.malach.it/assets/images/logo.png\"\n              },\n              \"name\" => \"Federation credential PoC\",\n              \"text_color\" => \"#FFFFFF\"\n            },\n            \"credential_identifier\" => \"FederatedAttributes\",\n            \"types\" => \"VerifiableCredential BorutaCredential\",\n            \"format\" => \"jwt_vc\",\n            \"claims\" => \"family_name\"\n          }\n        ]\n      )\n\n    %User{id: sub} = user_fixture(%{backend: backend, metadata: %{\"family_name\" => \"family_name\"}})\n\n    %Token{value: access_token} =\n      insert(:token,\n        sub: sub,\n        authorization_details: [%{\"credential_identifiers\" => [\"VerifiableCredential\"]}]\n      )\n\n    conn =\n      conn\n      |> put_req_header(\"authorization\", \"Bearer #{access_token}\")\n\n    conn =\n      post(\n        conn,\n        Routes.credential_path(conn, :credential),\n        credential_params\n      )\n\n    assert %{\"credential\" => credential} = json_response(conn, 200)\n    assert credential\n  end\n\n  @tag :skip\n  test \"returns a defered credential with a valid credential type\", %{conn: conn} do\n    {_, public_jwk} = public_key_fixture() |> JOSE.JWK.from_pem() |> JOSE.JWK.to_map()\n\n    signer =\n      Joken.Signer.create(\"RS256\", %{\"pem\" => private_key_fixture()}, %{\n        \"jwk\" => public_jwk,\n        \"typ\" => \"openid4vci-proof+jwt\"\n      })\n\n    {:ok, token, _claims} =\n      Signatures.Token.generate_and_sign(\n        %{\n          \"aud\" => Config.issuer(),\n          \"iat\" => :os.system_time(:seconds)\n        },\n        signer\n      )\n\n    proof = %{\n      \"proof_type\" => \"jwt\",\n      \"jwt\" => token\n    }\n\n    credential_params = %{\n      \"credential_identifier\" => \"VerifiableCredential\",\n      \"format\" => \"jwt_vc\",\n      \"proof\" => proof\n    }\n\n    backend =\n      BorutaIdentity.Factory.insert(:backend,\n        verifiable_credentials: [\n          %{\n            \"display\" => %{\n              \"background_color\" => \"#53b29f\",\n              \"logo\" => %{\n                \"alt_text\" => \"Boruta PoC logo\",\n                \"url\" => \"https://io.malach.it/assets/images/logo.png\"\n              },\n              \"name\" => \"Federation credential PoC\",\n              \"text_color\" => \"#FFFFFF\"\n            },\n            \"credential_identifier\" => \"FederatedAttributes\",\n            \"types\" => \"VerifiableCredential BorutaCredential\",\n            \"format\" => \"jwt_vc\",\n            \"defered\" => true,\n            \"claims\" => \"family_name\"\n          }\n        ]\n      )\n\n    %User{id: sub} = user_fixture(%{backend: backend, metadata: %{\"family_name\" => \"family_name\"}})\n\n    %Token{value: access_token} =\n      insert(:token,\n        sub: sub,\n        authorization_details: [%{\"credential_identifiers\" => [\"VerifiableCredential\"]}]\n      )\n\n    conn =\n      conn\n      |> put_req_header(\"authorization\", \"Bearer #{access_token}\")\n\n    defered_conn =\n      post(\n        conn,\n        Routes.credential_path(conn, :credential),\n        credential_params\n      )\n\n    assert %{\"acceptance_token\" => acceptance_token} = json_response(defered_conn, 200)\n    assert acceptance_token\n\n    conn =\n      post(\n        conn,\n        Routes.credential_path(conn, :defered_credential),\n        credential_params\n      )\n\n    assert %{\"credential\" => credential} = json_response(conn, 200)\n    assert credential\n  end\n\n  def public_key_fixture do\n    \"-----BEGIN RSA PUBLIC KEY-----\\nMIIBCgKCAQEA1PaP/gbXix5itjRCaegvI/B3aFOeoxlwPPLvfLHGA4QfDmVOf8cU\\n8OuZFAYzLArW3PnnwWWy39nVJOx42QRVGCGdUCmV7shDHRsr86+2DlL7pwUa9QyH\\nsTj84fAJn2Fv9h9mqrIvUzAtEYRlGFvjVTGCwzEullpsB0GJafopUTFby8WdSq3d\\nGLJBB1r+Q8QtZnAxxvolhwOmYkBkkidefmm48X7hFXL2cSJm2G7wQyinOey/U8xD\\nZ68mgTakiqS2RtjnFD0dnpBl5CYTe4s6oZKEyFiFNiW4KkR1GVjsKwY9oC2tpyQ0\\nAEUMvk9T9VdIltSIiAvOKlwFzL49cgwZDwIDAQAB\\n-----END RSA PUBLIC KEY-----\\n\\n\"\n  end\n\n  def private_key_fixture do\n    \"-----BEGIN RSA PRIVATE KEY-----\\nMIIEowIBAAKCAQEA1PaP/gbXix5itjRCaegvI/B3aFOeoxlwPPLvfLHGA4QfDmVO\\nf8cU8OuZFAYzLArW3PnnwWWy39nVJOx42QRVGCGdUCmV7shDHRsr86+2DlL7pwUa\\n9QyHsTj84fAJn2Fv9h9mqrIvUzAtEYRlGFvjVTGCwzEullpsB0GJafopUTFby8Wd\\nSq3dGLJBB1r+Q8QtZnAxxvolhwOmYkBkkidefmm48X7hFXL2cSJm2G7wQyinOey/\\nU8xDZ68mgTakiqS2RtjnFD0dnpBl5CYTe4s6oZKEyFiFNiW4KkR1GVjsKwY9oC2t\\npyQ0AEUMvk9T9VdIltSIiAvOKlwFzL49cgwZDwIDAQABAoIBAG0dg/upL8k1IWiv\\n8BNphrXIYLYQmiiBQTPJWZGvWIC2sl7i40yvCXjDjiRnZNK9HwgL94XtALCXYRFR\\nJD41bRA3MO5A0HSPIWwJXwS10/cU56HVCNHjwKa6Rz/QiG2kNASMZEMzlvHtrjna\\ndx36/sjI3HH8gh1BaTZyiuDE72SMkPbL838jfL1YY9uJ0u6hWFDbdn3sqPfJ6Cnz\\n1cu0piT35nkilnIGCNYA0i3lyMeo4XrdXaAJdN9nnqbCi5ewQWqaHbrIIY5LTgzJ\\nYlOr3IiecyokFxHCbULXle60u0KqXYgBHmlQJJr1Dj4c9AkQmefjC2jRMlhOrIzo\\nIkIUeMECgYEA+MNLB+w6vv1ogqzM3M1OLt6bziWJCn+XkziuMrCiY9KeDD+S70+E\\nhfbhM5RjCE3wxC/k59039laT973BmdMHxrDd2zSjOFmCIORv5yrD5oBHMaMZcwuQ\\n45Xisi4aoQoOhyznSnjo/RjeQB7qEDzXFznLLNT79HzqyAtCWD3UIu8CgYEA2yik\\n9FKl7HJEY94D2K6vNh1AHGnkwIQC72pXzlUrVuwQYngj6/Gkhw8ayFBApHfwVCXj\\no9rDYPdNrrAs0Zz0JsiJp6bOCEKCrMYE16UiejUUAg/OZ5eg6+3m3/iWatkzLUuK\\n1LIkVBJlEyY0uPuAaBF0V0VleNvfCGhVYOn46+ECgYAUD4OsduNh5YOZDiBTKgdF\\nBlSgMiyz+QgbKjX6Bn6B+EkgibvqqonwV7FffHbkA40H9SjLfe52YhL6poXHRtpY\\nroillcAX2jgBOQrBJJS5sNyM5y81NNiRUdP/NHKXS/1R71ATlF6NkoTRvOx5NL7P\\ns6xryB0tYSl5ylamUQ4bZwKBgHF6FB9mA//wErVbKcayfIqajq2nrwh30kVBXQG7\\nW9uAE+PIrWDoF/bOvWFnHHGMoOYRUFNxXKUCqDiBhFNs34aNY6lpV1kzhxIK3ksC\\neF2qyhdfM9Kz0mEXJ+pkfw4INNWJPfNv4hueArPtnnMB1rUMBJ+DkU0JG+zwiPTL\\ncVZBAoGBAM6kOsh5KGn3aI83g9ZO0TrKLXXFotxJt31Wu11ydj9K33/Qj3UXcxd4\\nJPXr600F0DkLeUKBob6BALeHFWcrSz5FGLGRqdRxdv+L6g18WH5m2xEs7o6M6e5I\\nIhyUC60ZewJ2M8rV4KgCJJdZE2kENlSgjU92IDVPT9Oetrc7hQJd\\n-----END RSA PRIVATE KEY-----\\n\\n\"\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/controllers/oauth/authorization_code_test.exs",
    "content": "defmodule BorutaWeb.Oauth.AuthorizationCodeTest do\n  use BorutaWeb.ConnCase\n\n  import Boruta.Factory\n  import BorutaIdentity.AccountsFixtures\n\n  alias BorutaIdentityWeb.Authenticable\n\n  describe \"#authorize\" do\n    setup %{conn: conn} do\n      resource_owner = user_fixture()\n      redirect_uri = \"http://redirect.uri\"\n      client = insert(:client, redirect_uris: [redirect_uri])\n\n      {:ok,\n       conn: conn, client: client, redirect_uri: redirect_uri, resource_owner: resource_owner}\n    end\n\n    test \"redirects to choose session if session not chosen\", %{\n      conn: conn,\n      client: client,\n      redirect_uri: redirect_uri,\n      resource_owner: resource_owner\n    } do\n      conn =\n        conn\n        |> log_in(resource_owner)\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"token\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            state: \"state\"\n          })\n        )\n\n      assert redirected_to(conn) =~ IdentityRoutes.choose_session_path(conn, :index)\n    end\n  end\n\n  describe \"authorization code grant\" do\n    setup %{conn: conn} do\n      resource_owner = user_fixture()\n      client = insert(:client)\n      identity_provider = BorutaIdentity.Factory.insert(:identity_provider, consentable: true)\n\n      BorutaIdentity.Factory.insert(:client_identity_provider,\n        client_id: client.id,\n        identity_provider: identity_provider\n      )\n\n      scope = insert(:scope, public: true)\n\n      {:ok,\n       conn: put_req_header(conn, \"content-type\", \"application/x-www-form-urlencoded\"),\n       client: client,\n       resource_owner: resource_owner,\n       scope: scope}\n    end\n\n    test \"renders preauthorize with scope\", %{\n      conn: conn,\n      client: client,\n      resource_owner: resource_owner,\n      scope: scope\n    } do\n      redirect_uri = List.first(client.redirect_uris)\n      request_param = Authenticable.request_param(\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"code\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            scope: scope.name\n          })\n        )\n      )\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true)\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"code\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            scope: scope.name\n          })\n        )\n\n      assert redirected_to(conn) == IdentityRoutes.user_consent_path(conn, :index, request: request_param)\n    end\n\n    test \"redirects to redirect_uri with errors in query if redirect_uri is invalid\", %{\n      conn: conn,\n      client: client,\n      resource_owner: resource_owner\n    } do\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true)\n\n      assert_raise BorutaWeb.AuthorizeError, \"Invalid client_id or redirect_uri.\", fn ->\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"code\",\n            client_id: client.id,\n            redirect_uri: \"http://bad.redirect.uri\",\n            state: \"state\"\n          })\n        )\n      end\n    end\n\n    test \"redirects to redirect_uri with token if current_user is set\", %{\n      conn: conn,\n      client: client,\n      resource_owner: resource_owner\n    } do\n      redirect_uri = List.first(client.redirect_uris)\n      request_param = Authenticable.request_param(\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"code\",\n            client_id: client.id,\n            redirect_uri: redirect_uri\n          })\n        )\n      )\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true, preauthorizations: %{request_param => true})\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"code\",\n            client_id: client.id,\n            redirect_uri: redirect_uri\n          })\n        )\n\n      [_, code] =\n        Regex.run(\n          ~r/#{redirect_uri}\\?code=(.+)/,\n          redirected_to(conn)\n        )\n\n      assert code\n    end\n\n    test \"redirects to redirect_uri with state when session chosen\", %{\n      conn: conn,\n      client: client,\n      resource_owner: resource_owner\n    } do\n      given_state = \"state\"\n      redirect_uri = List.first(client.redirect_uris)\n      request_param = Authenticable.request_param(\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"code\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            state: given_state\n          })\n        )\n      )\n\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true, preauthorizations: %{request_param => true})\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"code\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            state: given_state\n          })\n        )\n\n      assert [_, _redirect_uri] =\n        Regex.run(\n          ~r/(#{redirect_uri})\\?/,\n          redirected_to(conn)\n        )\n\n      [_, code] =\n        Regex.run(\n          ~r/code=([^&]+)/,\n          redirected_to(conn)\n        )\n\n      [_, state] =\n        Regex.run(\n          ~r/state=([^&]+)/,\n          redirected_to(conn)\n        )\n\n      assert code\n      assert state == given_state\n    end\n\n    test \"redirects to redirect_uri with consented scope\", %{\n      conn: conn,\n      client: client,\n      resource_owner: resource_owner,\n      scope: scope\n    } do\n      redirect_uri = List.first(client.redirect_uris)\n      request_param =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"code\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            scope: scope.name\n          })\n        )\n        |> Authenticable.request_param()\n\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true, preauthorizations: %{request_param => true})\n\n      BorutaIdentity.Factory.insert(:consent,\n        user_id: resource_owner.id,\n        client_id: client.id,\n        scopes: [scope.name]\n      )\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"code\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            scope: scope.name\n          })\n        )\n\n      [_, code] =\n        Regex.run(\n          ~r/#{redirect_uri}\\?code=(.+)/,\n          redirected_to(conn)\n        )\n\n      assert code\n    end\n\n    @tag :skip\n    test \"delivers a token inexchange of a code\"\n\n    @tag :skip\n    test \"preauthorize error case\"\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/controllers/oauth/authorize_controller_test.exs",
    "content": "defmodule BorutaWeb.AuthorizeControllerTest do\n  use BorutaWeb.ConnCase\n\n  import Boruta.Factory\n  import BorutaIdentity.AccountsFixtures\n\n  setup %{conn: conn} do\n    {:ok, conn: conn}\n  end\n\n  describe \"#authorize\" do\n    setup %{conn: conn} do\n      resource_owner = user_fixture()\n      redirect_uri = \"http://redirect.uri\"\n      client = insert(:client, redirect_uris: [redirect_uri])\n\n      {:ok,\n       conn: conn, client: client, redirect_uri: redirect_uri, resource_owner: resource_owner}\n    end\n\n    test \"redirects to choose session if session not chosen\", %{\n      conn: conn,\n      client: client,\n      redirect_uri: redirect_uri,\n      resource_owner: resource_owner\n    } do\n      conn = conn\n             |> log_in(resource_owner)\n\n      conn = get(\n        conn,\n        Routes.authorize_path(conn, :authorize, %{\n          response_type: \"token\",\n          client_id: client.id,\n          redirect_uri: redirect_uri,\n          state: \"state\"\n        })\n      )\n\n      assert redirected_to(conn) =~ IdentityRoutes.choose_session_path(conn, :index)\n    end\n\n    @tag :skip\n    test \"error renders and redirections\"\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/controllers/oauth/client_credentials_test.exs",
    "content": "defmodule BorutaWeb.Oauth.ClientCredentialsTest do\n  use BorutaWeb.ConnCase\n\n  import Boruta.Factory\n\n  setup %{conn: conn} do\n    {:ok, conn: conn}\n  end\n\n  describe \"client_credentials grant\" do\n    setup %{conn: conn} do\n      client = insert(:client)\n\n      {:ok,\n       conn: put_req_header(conn, \"content-type\", \"application/x-www-form-urlencoded\"),\n       client: client}\n    end\n\n    test \"returns an error with invalid query parameters\", %{conn: conn} do\n      conn = post(conn, \"/oauth/token\")\n\n      assert json_response(conn, 400) == %{\n               \"error\" => \"invalid_request\",\n               \"error_description\" =>\n                 \"Request is not a valid OAuth request. Need a grant_type param.\"\n             }\n    end\n\n    test \"returns an error with an invalid grant type\", %{conn: conn} do\n      conn = post(conn, \"/oauth/token\", \"grant_type=bad_grant_type\")\n\n      assert json_response(conn, 400) == %{\n               \"error\" => \"invalid_request\",\n               \"error_description\" =>\n                 \"Request body validation failed. #/grant_type do match required pattern /^(client_credentials|agent_credentials|password|agent_code|authorization_code|refresh_token)$/.\"\n             }\n    end\n\n    test \"returns an error with invalid body parameters\", %{conn: conn} do\n      conn = post(conn, \"/oauth/token\", \"grant_type=client_credentials\")\n\n      assert json_response(conn, 400) == %{\n               \"error\" => \"invalid_request\",\n               \"error_description\" =>\n                 \"Request body validation failed. Required property client_id is missing at #.\"\n             }\n    end\n\n    test \"returns an error with invalid client_id\", %{conn: conn} do\n      conn =\n        post(\n          conn,\n          \"/oauth/token\",\n          \"grant_type=client_credentials&client_id=666&client_secret=666\"\n        )\n\n      assert json_response(conn, 400) == %{\n               \"error\" => \"invalid_request\",\n               \"error_description\" =>\n                 \"Request body validation failed. #/client_id do match required pattern /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.\"\n             }\n    end\n\n    test \"returns an error with invalid client_id/secret couple\", %{conn: conn} do\n      conn =\n        post(\n          conn,\n          \"/oauth/token\",\n          \"grant_type=client_credentials&client_id=6a2f41a3-c54c-fce8-32d2-0324e1c32e22&client_secret=666\"\n        )\n\n      assert json_response(conn, 401) == %{\n               \"error\" => \"invalid_client\",\n               \"error_description\" => \"Invalid client_id or client_secret.\"\n             }\n    end\n\n    test \"returns an error with invalid client_secret\", %{conn: conn, client: client} do\n      conn =\n        post(\n          conn,\n          \"/oauth/token\",\n          \"grant_type=client_credentials&client_id=#{client.id}&client_secret=666\"\n        )\n\n      assert json_response(conn, 401) == %{\n               \"error\" => \"invalid_client\",\n               \"error_description\" => \"Invalid client_id or client_secret.\"\n             }\n    end\n\n    test \"returns a token response with valid client_id/client_secret\", %{\n      conn: conn,\n      client: client\n    } do\n      conn =\n        post(\n          conn,\n          \"/oauth/token\",\n          \"grant_type=client_credentials&client_id=#{client.id}&client_secret=#{client.secret}\"\n        )\n\n      %{\n        \"access_token\" => access_token,\n        \"token_type\" => token_type,\n        \"expires_in\" => expires_in,\n        \"refresh_token\" => refresh_token\n      } = json_response(conn, 200)\n\n      assert access_token\n      assert token_type == \"bearer\"\n      assert expires_in\n      assert refresh_token\n    end\n\n    test \"returns a token response with valid agent token request\", %{\n      conn: conn,\n      client: client\n    } do\n      conn =\n        post(\n          conn,\n          \"/oauth/token\",\n          \"grant_type=agent_credentials&client_id=#{client.id}&client_secret=#{client.secret}&bind_data={}&bind_configuration={}\"\n        )\n\n      %{\n        \"agent_token\" => agent_token,\n        \"token_type\" => token_type,\n        \"expires_in\" => expires_in,\n        \"refresh_token\" => refresh_token\n      } = json_response(conn, 200)\n\n      assert agent_token\n      assert token_type == \"bearer\"\n      assert expires_in\n      assert refresh_token\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/controllers/oauth/direct_post_test.exs",
    "content": "defmodule BorutaWeb.Integration.DirectPostTest do\n  use BorutaWeb.ConnCase, async: false\n\n  alias Boruta.Internal.Signatures\n\n  setup %{conn: conn} do\n    client = Boruta.Factory.insert(:client, id_token_signature_alg: \"RS512\")\n\n    code =\n      Boruta.Factory.insert(:token,\n        type: \"code\",\n        redirect_uri: \"http://redirect.uri\",\n        response_type: \"id_token\",\n        state: \"state\",\n        sub:\n          \"did:jwk:eyJlIjoiQVFBQiIsImt0eSI6IlJTQSIsIm4iOiIxUGFQX2diWGl4NWl0alJDYWVndklfQjNhRk9lb3hsd1BQTHZmTEhHQTRRZkRtVk9mOGNVOE91WkZBWXpMQXJXM1BubndXV3kzOW5WSk94NDJRUlZHQ0dkVUNtVjdzaERIUnNyODYtMkRsTDdwd1VhOVF5SHNUajg0ZkFKbjJGdjloOW1xckl2VXpBdEVZUmxHRnZqVlRHQ3d6RXVsbHBzQjBHSmFmb3BVVEZieThXZFNxM2RHTEpCQjFyLVE4UXRabkF4eHZvbGh3T21Za0Jra2lkZWZtbTQ4WDdoRlhMMmNTSm0yRzd3UXlpbk9leV9VOHhEWjY4bWdUYWtpcVMyUnRqbkZEMGRucEJsNUNZVGU0czZvWktFeUZpRk5pVzRLa1IxR1Zqc0t3WTlvQzJ0cHlRMEFFVU12azlUOVZkSWx0U0lpQXZPS2x3RnpMNDljZ3daRHcifQ\"\n      )\n\n    signer =\n      Joken.Signer.create(\"RS256\", %{\"pem\" => private_key_fixture()}, %{\n        \"kid\" =>\n          \"did:jwk:eyJlIjoiQVFBQiIsImt0eSI6IlJTQSIsIm4iOiIxUGFQX2diWGl4NWl0alJDYWVndklfQjNhRk9lb3hsd1BQTHZmTEhHQTRRZkRtVk9mOGNVOE91WkZBWXpMQXJXM1BubndXV3kzOW5WSk94NDJRUlZHQ0dkVUNtVjdzaERIUnNyODYtMkRsTDdwd1VhOVF5SHNUajg0ZkFKbjJGdjloOW1xckl2VXpBdEVZUmxHRnZqVlRHQ3d6RXVsbHBzQjBHSmFmb3BVVEZieThXZFNxM2RHTEpCQjFyLVE4UXRabkF4eHZvbGh3T21Za0Jra2lkZWZtbTQ4WDdoRlhMMmNTSm0yRzd3UXlpbk9leV9VOHhEWjY4bWdUYWtpcVMyUnRqbkZEMGRucEJsNUNZVGU0czZvWktFeUZpRk5pVzRLa1IxR1Zqc0t3WTlvQzJ0cHlRMEFFVU12azlUOVZkSWx0U0lpQXZPS2x3RnpMNDljZ3daRHcifQ\",\n        \"typ\" => \"openid4vci-proof+jwt\"\n      })\n\n    {:ok, id_token, _claims} =\n      Signatures.Token.generate_and_sign(\n        %{\n          \"iss\" =>\n            \"did:jwk:eyJlIjoiQVFBQiIsImt0eSI6IlJTQSIsIm4iOiIxUGFQX2diWGl4NWl0alJDYWVndklfQjNhRk9lb3hsd1BQTHZmTEhHQTRRZkRtVk9mOGNVOE91WkZBWXpMQXJXM1BubndXV3kzOW5WSk94NDJRUlZHQ0dkVUNtVjdzaERIUnNyODYtMkRsTDdwd1VhOVF5SHNUajg0ZkFKbjJGdjloOW1xckl2VXpBdEVZUmxHRnZqVlRHQ3d6RXVsbHBzQjBHSmFmb3BVVEZieThXZFNxM2RHTEpCQjFyLVE4UXRabkF4eHZvbGh3T21Za0Jra2lkZWZtbTQ4WDdoRlhMMmNTSm0yRzd3UXlpbk9leV9VOHhEWjY4bWdUYWtpcVMyUnRqbkZEMGRucEJsNUNZVGU0czZvWktFeUZpRk5pVzRLa1IxR1Zqc0t3WTlvQzJ0cHlRMEFFVU12azlUOVZkSWx0U0lpQXZPS2x3RnpMNDljZ3daRHcifQ\"\n        },\n        signer\n      )\n\n    {:ok,\n     client: client,\n     id_token: id_token,\n     code: code,\n     conn: put_req_header(conn, \"content-type\", \"application/x-www-form-urlencoded\")}\n  end\n\n  describe \"SIOPV2 direct post\" do\n    test \"unauthorized with a bad id_token\", %{conn: conn} do\n      conn =\n        post(\n          conn,\n          \"/openid/direct_post/bad_code\",\n          \"id_token=bad_id_token\"\n        )\n\n      assert json_response(conn, 401) == %{\n               \"error\" => \"unauthorized\",\n               \"error_description\" =>\n                 \"{:error, :token_malformed}\"\n             }\n    end\n\n    @tag :skip\n    test \"not found with a bad code\", %{id_token: id_token, conn: conn} do\n      conn =\n        post(\n          conn,\n          \"/openid/direct_post/bad_code\",\n          \"id_token=#{id_token}\"\n        )\n\n      assert response(conn, 404)\n    end\n\n    @tag :skip\n    test \"authenticates\", %{id_token: id_token, code: code, conn: conn} do\n      conn =\n        post(\n          conn,\n          \"/openid/direct_post/#{code.id}\",\n          \"id_token=#{id_token}\"\n        )\n\n      assert redirected_to(conn) =~ ~r/#{code.redirect_uri}/\n      assert redirected_to(conn) =~ ~r/code=#{code.value}/\n      assert redirected_to(conn) =~ ~r/state=#{code.state}/\n    end\n  end\n\n  def public_key_fixture do\n    \"-----BEGIN RSA PUBLIC KEY-----\\nMIIBCgKCAQEA1PaP/gbXix5itjRCaegvI/B3aFOeoxlwPPLvfLHGA4QfDmVOf8cU\\n8OuZFAYzLArW3PnnwWWy39nVJOx42QRVGCGdUCmV7shDHRsr86+2DlL7pwUa9QyH\\nsTj84fAJn2Fv9h9mqrIvUzAtEYRlGFvjVTGCwzEullpsB0GJafopUTFby8WdSq3d\\nGLJBB1r+Q8QtZnAxxvolhwOmYkBkkidefmm48X7hFXL2cSJm2G7wQyinOey/U8xD\\nZ68mgTakiqS2RtjnFD0dnpBl5CYTe4s6oZKEyFiFNiW4KkR1GVjsKwY9oC2tpyQ0\\nAEUMvk9T9VdIltSIiAvOKlwFzL49cgwZDwIDAQAB\\n-----END RSA PUBLIC KEY-----\\n\\n\"\n  end\n\n  def private_key_fixture do\n    \"-----BEGIN RSA PRIVATE KEY-----\\nMIIEowIBAAKCAQEA1PaP/gbXix5itjRCaegvI/B3aFOeoxlwPPLvfLHGA4QfDmVO\\nf8cU8OuZFAYzLArW3PnnwWWy39nVJOx42QRVGCGdUCmV7shDHRsr86+2DlL7pwUa\\n9QyHsTj84fAJn2Fv9h9mqrIvUzAtEYRlGFvjVTGCwzEullpsB0GJafopUTFby8Wd\\nSq3dGLJBB1r+Q8QtZnAxxvolhwOmYkBkkidefmm48X7hFXL2cSJm2G7wQyinOey/\\nU8xDZ68mgTakiqS2RtjnFD0dnpBl5CYTe4s6oZKEyFiFNiW4KkR1GVjsKwY9oC2t\\npyQ0AEUMvk9T9VdIltSIiAvOKlwFzL49cgwZDwIDAQABAoIBAG0dg/upL8k1IWiv\\n8BNphrXIYLYQmiiBQTPJWZGvWIC2sl7i40yvCXjDjiRnZNK9HwgL94XtALCXYRFR\\nJD41bRA3MO5A0HSPIWwJXwS10/cU56HVCNHjwKa6Rz/QiG2kNASMZEMzlvHtrjna\\ndx36/sjI3HH8gh1BaTZyiuDE72SMkPbL838jfL1YY9uJ0u6hWFDbdn3sqPfJ6Cnz\\n1cu0piT35nkilnIGCNYA0i3lyMeo4XrdXaAJdN9nnqbCi5ewQWqaHbrIIY5LTgzJ\\nYlOr3IiecyokFxHCbULXle60u0KqXYgBHmlQJJr1Dj4c9AkQmefjC2jRMlhOrIzo\\nIkIUeMECgYEA+MNLB+w6vv1ogqzM3M1OLt6bziWJCn+XkziuMrCiY9KeDD+S70+E\\nhfbhM5RjCE3wxC/k59039laT973BmdMHxrDd2zSjOFmCIORv5yrD5oBHMaMZcwuQ\\n45Xisi4aoQoOhyznSnjo/RjeQB7qEDzXFznLLNT79HzqyAtCWD3UIu8CgYEA2yik\\n9FKl7HJEY94D2K6vNh1AHGnkwIQC72pXzlUrVuwQYngj6/Gkhw8ayFBApHfwVCXj\\no9rDYPdNrrAs0Zz0JsiJp6bOCEKCrMYE16UiejUUAg/OZ5eg6+3m3/iWatkzLUuK\\n1LIkVBJlEyY0uPuAaBF0V0VleNvfCGhVYOn46+ECgYAUD4OsduNh5YOZDiBTKgdF\\nBlSgMiyz+QgbKjX6Bn6B+EkgibvqqonwV7FffHbkA40H9SjLfe52YhL6poXHRtpY\\nroillcAX2jgBOQrBJJS5sNyM5y81NNiRUdP/NHKXS/1R71ATlF6NkoTRvOx5NL7P\\ns6xryB0tYSl5ylamUQ4bZwKBgHF6FB9mA//wErVbKcayfIqajq2nrwh30kVBXQG7\\nW9uAE+PIrWDoF/bOvWFnHHGMoOYRUFNxXKUCqDiBhFNs34aNY6lpV1kzhxIK3ksC\\neF2qyhdfM9Kz0mEXJ+pkfw4INNWJPfNv4hueArPtnnMB1rUMBJ+DkU0JG+zwiPTL\\ncVZBAoGBAM6kOsh5KGn3aI83g9ZO0TrKLXXFotxJt31Wu11ydj9K33/Qj3UXcxd4\\nJPXr600F0DkLeUKBob6BALeHFWcrSz5FGLGRqdRxdv+L6g18WH5m2xEs7o6M6e5I\\nIhyUC60ZewJ2M8rV4KgCJJdZE2kENlSgjU92IDVPT9Oetrc7hQJd\\n-----END RSA PRIVATE KEY-----\\n\\n\"\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/controllers/oauth/implicit_test.exs",
    "content": "defmodule BorutaWeb.Oauth.ImplicitTest do\n  use BorutaWeb.ConnCase\n\n  import Boruta.Factory\n  import BorutaIdentity.AccountsFixtures\n\n  alias BorutaIdentityWeb.Authenticable\n\n  setup %{conn: conn} do\n    {:ok, conn: conn}\n  end\n\n  describe \"implicit grant\" do\n    setup %{conn: conn} do\n      resource_owner = user_fixture()\n      redirect_uri = \"http://redirect.uri\"\n      client = insert(:client, redirect_uris: [redirect_uri])\n      identity_provider = BorutaIdentity.Factory.insert(:identity_provider, consentable: true)\n\n      BorutaIdentity.Factory.insert(:client_identity_provider,\n        client_id: client.id,\n        identity_provider: identity_provider\n      )\n\n      scope = insert(:scope, public: true)\n\n      {:ok,\n       conn: conn,\n       client: client,\n       redirect_uri: redirect_uri,\n       resource_owner: resource_owner,\n       scope: scope}\n    end\n\n    # TODO test different validation cases\n    test \"validates request params\", %{\n      conn: conn,\n      resource_owner: resource_owner\n    } do\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true)\n\n      assert_raise BorutaWeb.AuthorizeError,\n                   \"Request is not a valid OAuth request. Need a response_type param.\",\n                   fn ->\n                     get(conn, \"/oauth/authorize\")\n                   end\n    end\n\n    test \"returns an error if client_id is invalid\", %{\n      conn: conn,\n      redirect_uri: redirect_uri,\n      resource_owner: resource_owner\n    } do\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true)\n\n      assert_raise BorutaWeb.AuthorizeError, \"Invalid client_id or redirect_uri.\", fn ->\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"token\",\n            client_id: \"6a2f41a3-c54c-fce8-32d2-0324e1c32e22\",\n            redirect_uri: redirect_uri,\n            state: \"state\"\n          })\n        )\n      end\n    end\n\n    test \"redirect to user authentication page\", %{\n      conn: conn,\n      client: client,\n      redirect_uri: redirect_uri\n    } do\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"token\",\n            client_id: client.id,\n            redirect_uri: redirect_uri\n          })\n        )\n\n      # NOTE Path will be scoped in production with configuration and be forwarded to\n      assert redirected_to(conn) =~ \"/users/choose_session\"\n    end\n\n    test \"redirects to redirect_uri with token if current_user is set\", %{\n      conn: conn,\n      client: client,\n      redirect_uri: redirect_uri,\n      resource_owner: resource_owner\n    } do\n      request_param =\n        Authenticable.request_param(\n          get(\n            conn,\n            Routes.authorize_path(conn, :authorize, %{\n              response_type: \"token\",\n              client_id: client.id,\n              redirect_uri: redirect_uri\n            })\n          )\n        )\n\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true, preauthorizations: %{request_param => true})\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"token\",\n            client_id: client.id,\n            redirect_uri: redirect_uri\n          })\n        )\n\n      [_, access_token, expires_in] =\n        Regex.run(\n          ~r/#{redirect_uri}#access_token=(.+)&expires_in=(.+)/,\n          redirected_to(conn)\n        )\n\n      assert access_token\n      assert expires_in\n    end\n\n    test \"redirects to redirect_uri with state\", %{\n      conn: conn,\n      client: client,\n      redirect_uri: redirect_uri,\n      resource_owner: resource_owner\n    } do\n      given_state = \"state\"\n\n      request_param =\n        Authenticable.request_param(\n          get(\n            conn,\n            Routes.authorize_path(conn, :authorize, %{\n              response_type: \"token\",\n              client_id: client.id,\n              redirect_uri: redirect_uri,\n              state: given_state\n            })\n          )\n        )\n\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true, preauthorizations: %{request_param => true})\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"token\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            state: given_state\n          })\n        )\n\n      assert [_, \"bearer\"] =\n        Regex.run(\n          ~r/token_type=([^&]+)/,\n          redirected_to(conn)\n        )\n\n      assert [_, _redirect_uri] =\n        Regex.run(\n          ~r/(#{redirect_uri})#/,\n          redirected_to(conn)\n        )\n\n      [_, access_token] =\n        Regex.run(\n          ~r/access_token=([^&]+)/,\n          redirected_to(conn)\n        )\n\n      [_, expires_in] =\n        Regex.run(\n          ~r/expires_in=([^&]+)/,\n          redirected_to(conn)\n        )\n\n      [_, state] =\n        Regex.run(\n          ~r/state=([^&]+)/,\n          redirected_to(conn)\n        )\n      assert access_token\n      assert expires_in\n      assert state == given_state\n    end\n\n    test \"renders preauthorize with scope\", %{\n      conn: conn,\n      client: client,\n      redirect_uri: redirect_uri,\n      resource_owner: resource_owner,\n      scope: scope\n    } do\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true)\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"token\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            scope: scope.name\n          })\n        )\n\n      # TODO test request query param\n      assert redirected_to(conn) =~ IdentityRoutes.user_consent_path(conn, :index)\n    end\n\n    test \"redirects to redirect_uri with consented scope\", %{\n      conn: conn,\n      client: client,\n      redirect_uri: redirect_uri,\n      resource_owner: resource_owner,\n      scope: scope\n    } do\n      request_param =\n        Authenticable.request_param(\n          get(\n            conn,\n            Routes.authorize_path(conn, :authorize, %{\n              response_type: \"token\",\n              client_id: client.id,\n              redirect_uri: redirect_uri,\n              scope: scope.name\n            })\n          )\n        )\n\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true, preauthorizations: %{request_param => true})\n\n      BorutaIdentity.Factory.insert(:consent,\n        user_id: resource_owner.id,\n        client_id: client.id,\n        scopes: [scope.name]\n      )\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"token\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            scope: scope.name\n          })\n        )\n\n      [_, access_token, expires_in] =\n        Regex.run(\n          ~r/#{redirect_uri}#access_token=(.+)&expires_in=(.+)/,\n          redirected_to(conn)\n        )\n\n      assert access_token\n      assert expires_in\n    end\n\n    @tag :skip\n    test \"preauthorize error case\"\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/controllers/oauth/introspect_test.exs",
    "content": "defmodule BorutaWeb.Oauth.IntrospectTest do\n  use BorutaWeb.ConnCase\n\n  import Boruta.Factory\n  import BorutaIdentity.AccountsFixtures\n\n  setup %{conn: conn} do\n    {:ok, conn: conn}\n  end\n\n  describe \"introspect\" do\n    setup %{conn: conn} do\n      client = insert(:client)\n      client_token = insert(:token, type: \"access_token\", client: client, scope: \"\")\n      resource_owner = user_fixture()\n\n      resource_owner_token =\n        insert(:token,\n          type: \"access_token\",\n          client: client,\n          sub: resource_owner.id,\n          scope: \"\"\n        )\n\n      {:ok,\n       conn: put_req_header(conn, \"content-type\", \"application/x-www-form-urlencoded\"),\n       client: client,\n       client_token: client_token,\n       resource_owner_token: resource_owner_token,\n       resource_owner: resource_owner}\n    end\n\n    test \"returns an error if request is invalid\", %{conn: conn} do\n      conn =\n        post(\n          conn,\n          \"/oauth/introspect\"\n        )\n\n      assert json_response(conn, 400) == %{\n               \"error\" => \"invalid_request\",\n               \"error_description\" =>\n                 \"Request validation failed. Required properties client_id, token are missing at #.\"\n             }\n    end\n\n    test \"returns an error if client is invalid\", %{conn: conn, client: client} do\n      conn =\n        post(\n          conn,\n          \"/oauth/introspect\",\n          \"client_id=#{client.id}&client_secret=bad_secret&token=token\"\n        )\n\n      assert json_response(conn, 401) == %{\n               \"error\" => \"invalid_client\",\n               \"error_description\" => \"Invalid client_id or client_secret.\"\n             }\n    end\n\n    test \"returns an inactive token response if token is invalid\", %{conn: conn, client: client} do\n      conn =\n        post(\n          conn,\n          \"/oauth/introspect\",\n          \"client_id=#{client.id}&client_secret=#{client.secret}&token=bad_token\"\n        )\n\n      assert json_response(conn, 200) == %{\"active\" => false}\n    end\n\n    test \"returns an introspect token response if client, token are valid\", %{\n      conn: conn,\n      client: client,\n      client_token: token\n    } do\n      conn =\n        post(\n          conn,\n          \"/oauth/introspect\",\n          \"client_id=#{client.id}&client_secret=#{client.secret}&token=#{token.value}\"\n        )\n\n      assert json_response(conn, 200) == %{\n               \"active\" => true,\n               \"client_id\" => client.id,\n               \"exp\" => token.expires_at,\n               \"iat\" => DateTime.to_unix(token.inserted_at),\n               \"iss\" => \"http://localhost:4000\",\n               \"scope\" => token.scope,\n               \"sub\" => nil,\n               \"username\" => nil\n             }\n    end\n\n    test \"returns an introspect token response if resource owner token is valid\", %{\n      conn: conn,\n      client: client,\n      resource_owner_token: token,\n      resource_owner: resource_owner\n    } do\n      conn =\n        post(\n          conn,\n          \"/oauth/introspect\",\n          \"client_id=#{client.id}&client_secret=#{client.secret}&token=#{token.value}\"\n        )\n\n      assert json_response(conn, 200) == %{\n               \"active\" => true,\n               \"client_id\" => client.id,\n               \"exp\" => token.expires_at,\n               \"iat\" => DateTime.to_unix(token.inserted_at),\n               \"iss\" => \"http://localhost:4000\",\n               \"scope\" => token.scope,\n               \"sub\" => resource_owner.id,\n               \"username\" => resource_owner.username\n             }\n    end\n\n    test \"returns a jwt token when accept header set\", %{\n      conn: conn,\n      client: client,\n      client_token: token\n    } do\n      signer = Joken.Signer.create(\"RS512\", %{\"pem\" => client.public_key})\n      conn = put_req_header(conn, \"accept\", \"application/jwt\")\n\n      conn =\n        post(\n          conn,\n          \"/oauth/introspect\",\n          \"client_id=#{client.id}&client_secret=#{client.secret}&token=#{token.value}\"\n        )\n\n      case Joken.Signer.verify(response(conn, 200), signer) do\n        {:ok, payload} ->\n          assert payload == %{\n                   \"active\" => true,\n                   \"client_id\" => client.id,\n                   \"exp\" => token.expires_at,\n                   \"iat\" => DateTime.to_unix(token.inserted_at),\n                   \"iss\" => \"http://localhost:4000\",\n                   \"scope\" => token.scope,\n                   \"sub\" => nil,\n                   \"username\" => nil\n                 }\n\n        _ ->\n          assert false\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/controllers/oauth/openid_connect_test.exs",
    "content": "defmodule BorutaWeb.Integration.OpenidConnectTest do\n  use BorutaWeb.ConnCase, async: false\n\n  import Boruta.Factory\n  import BorutaIdentity.AccountsFixtures\n\n  alias Boruta.ClientsAdapter\n  alias Boruta.Ecto.Admin\n  alias Boruta.Ecto.Client\n  alias Boruta.Ecto.ClientStore\n  alias Boruta.Oauth\n  alias BorutaIdentity.IdentityProviders.ClientIdentityProvider\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentityWeb.Authenticable\n\n  describe \"OpenID Connect flows\" do\n    setup %{conn: conn} do\n      public_client = Admin.get_client!(ClientsAdapter.public!().id)\n      {:ok, _client} = Admin.update_client(public_client, %{supported_grant_types: Oauth.Client.grant_types()})\n      ClientStore.invalidate_public()\n\n      resource_owner = user_fixture()\n      redirect_uri = \"http://redirect.uri\"\n      client = insert(:client, redirect_uris: [redirect_uri])\n      scope = insert(:scope, public: true)\n\n      {:ok,\n       conn: conn,\n       client: client,\n       redirect_uri: redirect_uri,\n       resource_owner: resource_owner,\n       scope: scope}\n    end\n\n    test \"redirect to login with prompt=login\", %{conn: conn} do\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            prompt: \"login\"\n          })\n        )\n\n      assert redirected_to(conn) =~ \"/users/log_out\"\n    end\n\n    test \"returns an error with prompt=none without any current_user\", %{\n      conn: conn,\n      client: client,\n      redirect_uri: redirect_uri\n    } do\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"id_token\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            prompt: \"none\",\n            scope: \"openid\",\n            nonce: \"nonce\"\n          })\n        )\n\n      assert redirected_to(conn) =~ ~r/error=login_required/\n    end\n\n    test \"authorizes with prompt=none with anonymous client (verifiable presentation - wallet)\", %{\n      conn: conn,\n      redirect_uri: redirect_uri\n    } do\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"vp_token\",\n            client_id: \"did:key:test\",\n            redirect_uri: redirect_uri,\n            client_metadata: \"{}\",\n            prompt: \"none\",\n            scope: \"openid\",\n            nonce: \"nonce\"\n          })\n        )\n\n      assert redirected_to(conn) =~ ~r/request=/\n      assert redirected_to(conn) =~ ~r/redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fopenid%2Fdirect_post%2F/\n      assert redirected_to(conn) =~ ~r/#{redirect_uri}/\n    end\n\n    test \"authorizes with prompt=none with anonymous client (siopv2 - wallet)\", %{\n      conn: conn,\n      redirect_uri: redirect_uri\n    } do\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"code\",\n            client_id: \"did:key:test\",\n            redirect_uri: redirect_uri,\n            client_metadata: \"{}\",\n            prompt: \"none\",\n            scope: \"openid\",\n            nonce: \"nonce\"\n          })\n        )\n\n      assert redirected_to(conn) =~ ~r/request=/\n      assert redirected_to(conn) =~ ~r/redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fopenid%2Fdirect_post%2F/\n      assert redirected_to(conn) =~ ~r/#{redirect_uri}/\n    end\n\n    test \"returns an error with prompt=none without any current_user (preauthorized)\", %{\n      conn: conn,\n      client: client,\n      redirect_uri: redirect_uri\n    } do\n      request_param =\n        Authenticable.request_param(\n          get(\n            conn,\n            Routes.authorize_path(conn, :authorize, %{\n              response_type: \"id_token\",\n              client_id: client.id,\n              redirect_uri: redirect_uri,\n              prompt: \"none\",\n              scope: \"openid\",\n              nonce: \"nonce\"\n            })\n          )\n        )\n\n      conn =\n        init_test_session(conn, session_chosen: true, preauthorizations: %{request_param => true})\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"id_token\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            prompt: \"none\",\n            scope: \"openid\",\n            nonce: \"nonce\"\n          })\n        )\n\n      assert redirected_to(conn) =~ ~r/error=login_required/\n    end\n\n    test \"authorize with prompt='none' and a current_user\", %{\n      conn: conn,\n      client: client,\n      resource_owner: resource_owner,\n      redirect_uri: redirect_uri\n    } do\n      request_param =\n        Authenticable.request_param(\n          get(\n            conn,\n            Routes.authorize_path(conn, :authorize, %{\n              response_type: \"id_token\",\n              client_id: client.id,\n              redirect_uri: redirect_uri,\n              prompt: \"none\",\n              scope: \"openid\",\n              nonce: \"nonce\"\n            })\n          )\n        )\n\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(session_chosen: true, preauthorizations: %{request_param => true})\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"id_token\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            prompt: \"none\",\n            scope: \"openid\",\n            nonce: \"nonce\"\n          })\n        )\n\n      assert url = redirected_to(conn)\n\n      assert [_, _id_token] =\n               Regex.run(\n                 ~r/#{redirect_uri}#id_token=(.+)/,\n                 url\n               )\n    end\n\n    test \"logs in with an expired max_age and current_user\", %{\n      conn: conn,\n      client: client,\n      resource_owner: resource_owner,\n      redirect_uri: redirect_uri\n    } do\n      conn =\n        conn\n        |> log_in(resource_owner)\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"id_token\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            scope: \"openid\",\n            nonce: \"nonce\",\n            max_age: 0\n          })\n        )\n\n      assert redirected_to(conn) =~ \"/users/log_out\"\n    end\n\n    test \"redirects to redirect_uri session with a non expired max_age and current_user\", %{\n      conn: conn,\n      client: client,\n      resource_owner: resource_owner,\n      redirect_uri: redirect_uri\n    } do\n      request_param =\n        Authenticable.request_param(\n          get(\n            conn,\n            Routes.authorize_path(conn, :authorize, %{\n              response_type: \"id_token\",\n              client_id: client.id,\n              redirect_uri: redirect_uri,\n              scope: \"openid\",\n              nonce: \"nonce\",\n              max_age: 10\n            })\n          )\n        )\n\n      conn =\n        conn\n        |> log_in(resource_owner)\n        |> init_test_session(preauthorizations: %{request_param => true})\n\n      conn =\n        get(\n          conn,\n          Routes.authorize_path(conn, :authorize, %{\n            response_type: \"id_token\",\n            client_id: client.id,\n            redirect_uri: redirect_uri,\n            scope: \"openid\",\n            nonce: \"nonce\",\n            max_age: 10\n          })\n        )\n\n      assert url = redirected_to(conn)\n\n      assert [_, _id_token] =\n               Regex.run(\n                 ~r/#{redirect_uri}#id_token=(.+)/,\n                 url\n               )\n    end\n  end\n\n  describe \"jwks endpoints\" do\n    test \"returns public client key\", %{conn: conn} do\n      conn = get(conn, Routes.jwks_path(conn, :jwks_index))\n\n      assert %{\"keys\" => keys} = json_response(conn, 200)\n      assert Enum.count(keys) == 1\n    end\n\n    test \"returns all clients keys\", %{conn: conn} do\n      %Client{} = insert(:client)\n\n      conn = get(conn, Routes.jwks_path(conn, :jwks_index))\n\n      assert keys = json_response(conn, 200)[\"keys\"]\n      assert Enum.find(keys, fn %{\"kid\" => kid} -> kid == \"Ac9ufCpgwReXGJ6LI\" end)\n    end\n  end\n\n  describe \"userinfo\" do\n    test \"returns userinfo\", %{conn: conn} do\n      sub = user_fixture().id\n\n      token = insert(:token, sub: sub)\n\n      conn =\n        conn\n        |> put_req_header(\"authorization\", \"bearer #{token.value}\")\n        |> post(Routes.userinfo_path(conn, :userinfo))\n\n      assert json_response(conn, 200)\n    end\n\n    test \"returns userinfo as jwt\", %{conn: conn} do\n      sub = user_fixture().id\n\n      token = insert(:token, sub: sub)\n\n      {:ok, _client} =\n        Ecto.Changeset.change(token.client, %{userinfo_signed_response_alg: \"HS512\"})\n        |> BorutaAuth.Repo.update()\n\n      conn =\n        conn\n        |> put_req_header(\"authorization\", \"bearer #{token.value}\")\n        |> post(Routes.userinfo_path(conn, :userinfo))\n\n      assert String.starts_with?(response(conn, 200), \"ey\")\n    end\n  end\n\n  describe \"discovery 1.0\" do\n    test \"returns required keys\", %{conn: conn} do\n      BorutaIdentity.Factory.insert(:backend,\n        verifiable_credentials: [\n          %{\n            \"display\" => %{\n              \"background_color\" => \"#53b29f\",\n              \"logo\" => %{\n                \"alt_text\" => \"Boruta PoC logo\",\n                \"url\" => \"https://io.malach.it/assets/images/logo.png\"\n              },\n              \"name\" => \"Federation credential PoC\",\n              \"text_color\" => \"#FFFFFF\"\n            },\n            \"claims\" => [%{\"name\" => \"claim\", \"label\" => \"label\"}],\n            \"credential_identifier\" => \"FederatedAttributes\",\n            \"format\" => \"jwt_vc\",\n            \"types\" => \"VerifiableCredential BorutaCredential\"\n          }\n        ]\n      )\n      Boruta.Factory.insert(:scope, name: \"well_known\")\n\n      conn = get(conn, Routes.openid_path(conn, :well_known))\n\n      assert json_response(conn, 200) == %{\n               \"authorization_endpoint\" => \"http://localhost:4000/oauth/authorize\",\n               \"credential_endpoint\" => \"http://localhost:4000/openid/credential\",\n               \"defered_credential_endpoint\" => \"http://localhost:4000/openid/defered-credential\",\n               \"pushed_authorization_request_endpoint\" => \"http://localhost:4000/oauth/pushed_authorization_request\",\n               \"credential_issuer\" => \"http://localhost:4000\",\n               \"credentials_supported\" => [],\n               \"credential_configurations_supported\" => %{\n                 \"FederatedAttributes\" => %{\n                   \"credential_definition\" => %{\n                     \"credentialSubject\" => %{\"claim\" => [%{\"name\" => \"label\"}]},\n                     \"type\" => [\"VerifiableCredential\", \"BorutaCredential\"]\n                   },\n                   \"credential_signing_alg_values_supported\" => [\n                     \"ES256\",\n                     \"ES384\",\n                     \"ES512\",\n                     \"RS256\",\n                     \"RS384\",\n                     \"RS512\",\n                     \"HS256\",\n                     \"HS384\",\n                     \"HS512\",\n                     \"EdDSA\"\n                   ],\n                   \"cryptographic_binding_methods_supported\" => [\"did:jwk\", \"did:key\"],\n                   \"display\" => [\n                     %{\n                       \"background_color\" => \"#53b29f\",\n                       \"locale\" => \"en-US\",\n                       \"logo\" => %{\n                         \"alt_text\" => \"Boruta PoC logo\",\n                         \"url\" => \"https://io.malach.it/assets/images/logo.png\"\n                       },\n                       \"name\" => \"Federation credential PoC\",\n                       \"text_color\" => \"#FFFFFF\"\n                     }\n                   ],\n                   \"format\" => \"jwt_vc\",\n                   \"scope\" => \"FederatedAttributes\"\n                 }\n               },\n               \"grant_types_supported\" => [\n                 \"client_credentials\",\n                 \"password\",\n                 \"implicit\",\n                 \"authorization_code\",\n                 \"refresh_token\"\n               ],\n               \"id_token_signing_alg_values_supported\" => [\n                 \"ES256\",\n                 \"ES384\",\n                 \"ES512\",\n                 \"RS256\",\n                 \"RS384\",\n                 \"RS512\",\n                 \"HS256\",\n                 \"HS384\",\n                 \"HS512\",\n                 \"EdDSA\"\n               ],\n               \"issuer\" => \"http://localhost:4000\",\n               \"jwks_uri\" => \"http://localhost:4000/openid/jwks\",\n               # \"registration_endpoint\" => \"http://localhost:4000/openid/register\",\n               \"request_object_signing_alg_values_supported\" => [\n                 \"ES256\",\n                 \"ES384\",\n                 \"ES512\",\n                 \"RS256\",\n                 \"RS384\",\n                 \"RS512\",\n                 \"HS256\",\n                 \"HS384\",\n                 \"HS512\",\n                 \"EdDSA\"\n               ],\n               \"response_modes_supported\" => [\"query\", \"fragment\"],\n               \"response_types_supported\" => [\n                 \"code\",\n                 \"token\",\n                 \"id_token\",\n                 \"code token\",\n                 \"code id_token\",\n                 \"token id_token\",\n                 \"code id_token token\"\n               ],\n               \"scopes_supported\" => [\"well_known\"],\n               \"subject_types_supported\" => [\"public\"],\n               \"token_endpoint\" => \"http://localhost:4000/oauth/token\",\n               \"token_endpoint_auth_methods_supported\" => [\n                 \"client_secret_basic\",\n                 \"client_secret_post\",\n                 \"client_secret_jwt\",\n                 \"private_key_jwt\"\n               ],\n               \"userinfo_endpoint\" => \"http://localhost:4000/oauth/userinfo\",\n               \"userinfo_signing_alg_values_supported\" => [\n                 \"ES256\",\n                 \"ES384\",\n                 \"ES512\",\n                 \"RS256\",\n                 \"RS384\",\n                 \"RS512\",\n                 \"HS256\",\n                 \"HS384\",\n                 \"HS512\",\n                 \"EdDSA\"\n               ]\n             }\n    end\n  end\n\n  # describe \"dynamic registration\" do\n  #   test \"returns an error when data is invalid\", %{conn: conn} do\n  #     conn =\n  #       post(conn, Routes.dynamic_registration_path(conn, :register_client), %{redirect_uris: nil})\n\n  #     assert json_response(conn, 400) == %{\n  #              \"error\" => \"invalid_client_metadata\",\n  #              \"error_description\" => \"redirect_uris : can't be blank\"\n  #            }\n  #   end\n\n  #   test \"registers client\", %{conn: conn} do\n  #     conn =\n  #       post(conn, Routes.dynamic_registration_path(conn, :register_client), %{\n  #         redirect_uris: [\"https://test.uri\"]\n  #       })\n\n  #     assert %{\n  #              \"client_id\" => client_id,\n  #              \"client_secret\" => client_secret,\n  #              \"client_secret_expires_at\" => 0\n  #            } = json_response(conn, 201)\n\n  #     assert client_id\n  #     assert client_secret\n  #   end\n\n  #   test \"creates associated identity provider\", %{conn: conn} do\n  #     conn =\n  #       post(conn, Routes.dynamic_registration_path(conn, :register_client), %{\n  #         redirect_uris: [\"https://test.uri\"]\n  #       })\n\n  #     assert %{\n  #              \"client_id\" => client_id\n  #            } = json_response(conn, 201)\n\n  #     assert %ClientIdentityProvider{identity_provider_id: identity_provider_id} =\n  #              BorutaIdentity.Repo.get_by(ClientIdentityProvider, client_id: client_id)\n\n  #     assert BorutaIdentity.Repo.get!(IdentityProvider, identity_provider_id)\n  #   end\n  # end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/controllers/oauth/password_test.exs",
    "content": "defmodule BorutaWeb.Oauth.PasswordTest do\n  use BorutaWeb.ConnCase\n\n  import Boruta.Factory\n  import BorutaIdentity.AccountsFixtures\n\n  alias BorutaIdentity.IdentityProviders.Backend\n\n  setup %{conn: conn} do\n    {:ok, conn: conn}\n  end\n\n  describe \"password grant\" do\n    setup %{conn: conn} do\n      password = valid_user_password()\n      resource_owner = user_fixture(%{password: password, backend: Backend.default!()})\n      client = insert(:client)\n\n      {:ok,\n       conn: put_req_header(conn, \"content-type\", \"application/x-www-form-urlencoded\"),\n       client: client,\n       resource_owner: resource_owner,\n       password: password}\n    end\n\n    test \"returns a token response with valid client_id/client_secret\", %{\n      conn: conn,\n      client: client,\n      resource_owner: resource_owner,\n      password: password\n    } do\n      conn =\n        post(\n          conn,\n          \"/oauth/token\",\n          \"grant_type=password&username=#{resource_owner.username}&password=#{password}&client_id=#{\n            client.id\n          }&client_secret=#{client.secret}\"\n        )\n\n      %{\n        \"access_token\" => access_token,\n        \"token_type\" => token_type,\n        \"expires_in\" => expires_in,\n        \"refresh_token\" => refresh_token\n      } = json_response(conn, 200)\n\n      assert access_token\n      assert token_type == \"bearer\"\n      assert expires_in\n      assert refresh_token\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/controllers/oauth/revoke_test.exs",
    "content": "defmodule BorutaWeb.Oauth.RevokeTest do\n  use BorutaWeb.ConnCase\n\n  import Boruta.Factory\n  import BorutaIdentity.AccountsFixtures\n\n  alias Boruta.Ecto.Token\n\n  setup %{conn: conn} do\n    {:ok, conn: conn}\n  end\n\n  describe \"revoke\" do\n    setup %{conn: conn} do\n      client = insert(:client)\n      client_token = insert(:token, type: \"access_token\", value: SecureRandom.uuid(), client: client)\n      resource_owner = user_fixture()\n\n      resource_owner_token =\n        insert(:token, type: \"access_token\", value: \"888\", client: client, sub: resource_owner.id)\n\n      {:ok,\n       conn: put_req_header(conn, \"content-type\", \"application/x-www-form-urlencoded\"),\n       client: client,\n       client_token: client_token,\n       resource_owner_token: resource_owner_token,\n       resource_owner: resource_owner}\n    end\n\n    test \"returns an error if request is invalid\", %{conn: conn} do\n      conn =\n        post(\n          conn,\n          \"/oauth/revoke\"\n        )\n\n      assert json_response(conn, 400) == %{\n               \"error\" => \"invalid_request\",\n               \"error_description\" =>\n                 \"Request validation failed. Required properties client_id, token are missing at #.\"\n             }\n    end\n\n    test \"returns an error if client is invalid\", %{conn: conn, client: client} do\n      conn =\n        post(\n          conn,\n          \"/oauth/revoke\",\n          \"client_id=#{client.id}&client_secret=bad_secret&token=token\"\n        )\n\n      assert json_response(conn, 401) == %{\n               \"error\" => \"invalid_client\",\n               \"error_description\" => \"Invalid client_id or client_secret.\"\n             }\n    end\n\n    test \"returns a success if token is invalid\", %{conn: conn, client: client} do\n      conn =\n        post(\n          conn,\n          \"/oauth/revoke\",\n          \"client_id=#{client.id}&client_secret=#{client.secret}&token=bad_token\"\n        )\n\n      assert response(conn, 200)\n    end\n\n    test \"return a success if client, token are valid\", %{\n      conn: conn,\n      client: client,\n      client_token: token\n    } do\n      conn =\n        post(\n          conn,\n          \"/oauth/revoke\",\n          \"client_id=#{client.id}&client_secret=#{client.secret}&token=#{token.value}\"\n        )\n\n      assert response(conn, 200)\n    end\n\n    test \"revoke token if client, token are valid\", %{\n      conn: conn,\n      client: client,\n      client_token: token\n    } do\n      post(\n        conn,\n        \"/oauth/revoke\",\n        \"client_id=#{client.id}&client_secret=#{client.secret}&token=#{token.value}\"\n      )\n\n      assert BorutaAuth.Repo.get_by(Token, value: token.value).revoked_at\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/controllers/pushed_authorization_request_controller_test.exs",
    "content": "defmodule BorutaWeb.Oauth.PushedAuthorizationRequestControllerTest do\n  use BorutaWeb.ConnCase\n\n  import Boruta.Factory\n\n  setup %{conn: conn} do\n    {:ok, conn: conn}\n  end\n\n  describe \"pushed authorization request\" do\n    test \"respond with an error\", %{conn: conn} do\n      request_params = %{}\n\n      conn =\n        post(\n          conn,\n          Routes.pushed_authorization_request_path(conn, :pushed_authorization_request),\n          request_params\n        )\n\n      assert json_response(conn, 400) == %{\n               \"error\" => \"invalid_request\",\n               \"error_description\" =>\n                 \"Request is not a valid OAuth request. Need a response_type param.\"\n             }\n    end\n\n    test \"stores the request\", %{conn: conn} do\n      client = insert(:client, redirect_uris: [\"http://redirect_uri\"])\n\n      request_params = %{\n        \"response_type\" => \"code\",\n        \"client_id\" => client.id,\n        \"redirect_uri\" => List.first(client.redirect_uris)\n      }\n\n      conn =\n        post(\n          conn,\n          Routes.pushed_authorization_request_path(conn, :pushed_authorization_request),\n          request_params\n        )\n\n      assert %{\n               \"request_uri\" => request_uri,\n               \"expires_in\" => expires_in\n             } = json_response(conn, 201)\n\n      assert request_uri\n      assert expires_in\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/plugs/rate_limit_test.exs",
    "content": "defmodule BorutaWeb.Plugs.RateLimitTest do\n  use ExUnit.Case\n\n  alias BorutaWeb.Plugs.RateLimit\n\n  setup do\n    {:ok, conn: Phoenix.ConnTest.build_conn()}\n  end\n\n  describe \"rate limiting\" do\n    test \"request passes\", %{conn: conn} do\n      :timer.sleep(1)\n      options = [time_unit: :millisecond, count: 1]\n      assert RateLimit.call(conn, options) == conn\n    end\n\n    test \"request is throttled\", %{conn: conn} do\n      :timer.sleep(1000)\n      b_conn = %{conn | remote_ip: {127, 0, 0, 2}}\n      options = [time_unit: :second, count: 5, penality: 500]\n      assert RateLimit.call(conn, options) == conn\n      assert RateLimit.call(b_conn, options) == b_conn\n    end\n  end\n\n  describe \"Counter.get\" do\n    test \"gives the count within the time unit range\" do\n      :timer.sleep(1000)\n      ip = :ip\n      time_unit = :second\n\n      assert RateLimit.Counter.get(ip, time_unit) == 0\n      Agent.update(RateLimit.Counter, fn _counter -> %{ip => [:os.system_time(:millisecond)]} end)\n      assert RateLimit.Counter.get(ip, time_unit) == 1\n\n      Agent.update(RateLimit.Counter, fn _counter ->\n        %{ip => [:os.system_time(:millisecond), :os.system_time(:millisecond)]}\n      end)\n\n      assert RateLimit.Counter.get(ip, time_unit) == 2\n\n      Agent.update(RateLimit.Counter, fn _counter ->\n        %{ip => [:os.system_time(:millisecond), :os.system_time(:millisecond) - 1000]}\n      end)\n\n      assert RateLimit.Counter.get(ip, time_unit) == 1\n    end\n  end\n\n  describe \"Counter.throttling_timeout\" do\n    test \"gives the timeout within the time unit range\" do\n      :timer.sleep(1000)\n      ip = :ip\n      time_unit = :second\n      penality = 100\n      count = 1\n\n      Agent.update(RateLimit.Counter, fn _counter -> %{} end)\n      assert RateLimit.Counter.throttling_timeout(ip, count, time_unit, penality) == 0\n\n      Agent.update(RateLimit.Counter, fn _counter -> %{ip => [:os.system_time(:millisecond)]} end)\n      assert RateLimit.Counter.throttling_timeout(ip, count, time_unit, penality) == 0\n\n      Agent.update(RateLimit.Counter, fn _counter ->\n        %{\n          ip => [\n            :os.system_time(:millisecond),\n            :os.system_time(:millisecond),\n            :os.system_time(:millisecond),\n            :os.system_time(:millisecond),\n            :os.system_time(:millisecond),\n            :os.system_time(:millisecond),\n            :os.system_time(:millisecond)\n          ]\n        }\n      end)\n\n      assert RateLimit.Counter.throttling_timeout(ip, count, time_unit, penality) == 700\n\n      Agent.update(RateLimit.Counter, fn _counter ->\n        %{\n          ip => [\n            :os.system_time(:millisecond) - 1000,\n            :os.system_time(:millisecond) - 800,\n            :os.system_time(:millisecond)\n          ]\n        }\n      end)\n\n      assert RateLimit.Counter.throttling_timeout(ip, count, time_unit, penality) == 200\n\n      Agent.update(RateLimit.Counter, fn _counter ->\n        %{ip => [:os.system_time(:millisecond), :os.system_time(:millisecond) - 1000]}\n      end)\n\n      assert RateLimit.Counter.throttling_timeout(ip, count, time_unit, penality) == 0\n    end\n  end\n\n  describe \"Counter.increment\" do\n    test \"updates the counter\" do\n      :timer.sleep(1000)\n      ip = :ip\n      time_unit = :second\n\n      assert Agent.get(RateLimit.Counter, fn counter ->\n               Map.get(counter, ip, [])\n               |> Enum.count(fn timestamp -> timestamp > :os.system_time(:millisecond) - 1000 end)\n             end) == 0\n\n      RateLimit.Counter.increment(ip, time_unit)\n\n      assert Agent.get(RateLimit.Counter, fn %{^ip => timestamps} ->\n               timestamps\n               |> Enum.count(fn timestamp -> timestamp > :os.system_time(:millisecond) - 1000 end)\n             end) == 1\n\n      RateLimit.Counter.increment(ip, time_unit)\n\n      assert Agent.get(RateLimit.Counter, fn %{^ip => timestamps} ->\n               timestamps\n               |> Enum.count(fn timestamp -> timestamp > :os.system_time(:millisecond) - 1000 end)\n             end) == 2\n\n      :timer.sleep(1000)\n      RateLimit.Counter.increment(ip, time_unit)\n\n      assert Agent.get(RateLimit.Counter, fn %{^ip => timestamps} ->\n               timestamps\n               |> Enum.count(fn timestamp -> timestamp > :os.system_time(:millisecond) - 1000 end)\n             end) ==\n               1\n    end\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/views/error_view_test.exs",
    "content": "defmodule BorutaWeb.ErrorViewTest do\n  use BorutaWeb.ConnCase, async: true\n\n  # Bring render/3 and render_to_string/3 for testing custom views\n  import Phoenix.View\n\n  test \"renders 404.html\" do\n    assert render_to_string(BorutaWeb.ErrorView, \"404.html\", []) =~ \"Page not found\"\n  end\n\n  test \"renders 500.html\" do\n    assert render_to_string(BorutaWeb.ErrorView, \"500.html\", []) =~ \"Internal server error\"\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/views/layout_view_test.exs",
    "content": "defmodule BorutaWeb.LayoutViewTest do\n  use BorutaWeb.ConnCase, async: true\nend\n"
  },
  {
    "path": "apps/boruta_web/test/boruta_web/views/page_view_test.exs",
    "content": "defmodule BorutaWeb.PageViewTest do\n  use BorutaWeb.ConnCase, async: true\nend\n"
  },
  {
    "path": "apps/boruta_web/test/support/boruta_factory.ex",
    "content": "defmodule Boruta.Factory do\n  @moduledoc false\n\n  use ExMachina.Ecto, repo: BorutaAuth.Repo\n\n  alias Boruta.Ecto\n\n  def client_factory do\n    %Ecto.Client{\n      secret: SecureRandom.urlsafe_base64(),\n      redirect_uris: [\"https://redirect.uri/oauth2-redirect-path\"],\n      access_token_ttl: 3600,\n      authorization_code_ttl: 60,\n      private_key:\n        \"-----BEGIN RSA PRIVATE KEY-----\\nMIIEowIBAAKCAQEA1PaP/gbXix5itjRCaegvI/B3aFOeoxlwPPLvfLHGA4QfDmVO\\nf8cU8OuZFAYzLArW3PnnwWWy39nVJOx42QRVGCGdUCmV7shDHRsr86+2DlL7pwUa\\n9QyHsTj84fAJn2Fv9h9mqrIvUzAtEYRlGFvjVTGCwzEullpsB0GJafopUTFby8Wd\\nSq3dGLJBB1r+Q8QtZnAxxvolhwOmYkBkkidefmm48X7hFXL2cSJm2G7wQyinOey/\\nU8xDZ68mgTakiqS2RtjnFD0dnpBl5CYTe4s6oZKEyFiFNiW4KkR1GVjsKwY9oC2t\\npyQ0AEUMvk9T9VdIltSIiAvOKlwFzL49cgwZDwIDAQABAoIBAG0dg/upL8k1IWiv\\n8BNphrXIYLYQmiiBQTPJWZGvWIC2sl7i40yvCXjDjiRnZNK9HwgL94XtALCXYRFR\\nJD41bRA3MO5A0HSPIWwJXwS10/cU56HVCNHjwKa6Rz/QiG2kNASMZEMzlvHtrjna\\ndx36/sjI3HH8gh1BaTZyiuDE72SMkPbL838jfL1YY9uJ0u6hWFDbdn3sqPfJ6Cnz\\n1cu0piT35nkilnIGCNYA0i3lyMeo4XrdXaAJdN9nnqbCi5ewQWqaHbrIIY5LTgzJ\\nYlOr3IiecyokFxHCbULXle60u0KqXYgBHmlQJJr1Dj4c9AkQmefjC2jRMlhOrIzo\\nIkIUeMECgYEA+MNLB+w6vv1ogqzM3M1OLt6bziWJCn+XkziuMrCiY9KeDD+S70+E\\nhfbhM5RjCE3wxC/k59039laT973BmdMHxrDd2zSjOFmCIORv5yrD5oBHMaMZcwuQ\\n45Xisi4aoQoOhyznSnjo/RjeQB7qEDzXFznLLNT79HzqyAtCWD3UIu8CgYEA2yik\\n9FKl7HJEY94D2K6vNh1AHGnkwIQC72pXzlUrVuwQYngj6/Gkhw8ayFBApHfwVCXj\\no9rDYPdNrrAs0Zz0JsiJp6bOCEKCrMYE16UiejUUAg/OZ5eg6+3m3/iWatkzLUuK\\n1LIkVBJlEyY0uPuAaBF0V0VleNvfCGhVYOn46+ECgYAUD4OsduNh5YOZDiBTKgdF\\nBlSgMiyz+QgbKjX6Bn6B+EkgibvqqonwV7FffHbkA40H9SjLfe52YhL6poXHRtpY\\nroillcAX2jgBOQrBJJS5sNyM5y81NNiRUdP/NHKXS/1R71ATlF6NkoTRvOx5NL7P\\ns6xryB0tYSl5ylamUQ4bZwKBgHF6FB9mA//wErVbKcayfIqajq2nrwh30kVBXQG7\\nW9uAE+PIrWDoF/bOvWFnHHGMoOYRUFNxXKUCqDiBhFNs34aNY6lpV1kzhxIK3ksC\\neF2qyhdfM9Kz0mEXJ+pkfw4INNWJPfNv4hueArPtnnMB1rUMBJ+DkU0JG+zwiPTL\\ncVZBAoGBAM6kOsh5KGn3aI83g9ZO0TrKLXXFotxJt31Wu11ydj9K33/Qj3UXcxd4\\nJPXr600F0DkLeUKBob6BALeHFWcrSz5FGLGRqdRxdv+L6g18WH5m2xEs7o6M6e5I\\nIhyUC60ZewJ2M8rV4KgCJJdZE2kENlSgjU92IDVPT9Oetrc7hQJd\\n-----END RSA PRIVATE KEY-----\\n\\n\",\n      public_key:\n        \"-----BEGIN RSA PUBLIC KEY-----\\nMIIBCgKCAQEA1PaP/gbXix5itjRCaegvI/B3aFOeoxlwPPLvfLHGA4QfDmVOf8cU\\n8OuZFAYzLArW3PnnwWWy39nVJOx42QRVGCGdUCmV7shDHRsr86+2DlL7pwUa9QyH\\nsTj84fAJn2Fv9h9mqrIvUzAtEYRlGFvjVTGCwzEullpsB0GJafopUTFby8WdSq3d\\nGLJBB1r+Q8QtZnAxxvolhwOmYkBkkidefmm48X7hFXL2cSJm2G7wQyinOey/U8xD\\nZ68mgTakiqS2RtjnFD0dnpBl5CYTe4s6oZKEyFiFNiW4KkR1GVjsKwY9oC2tpyQ0\\nAEUMvk9T9VdIltSIiAvOKlwFzL49cgwZDwIDAQAB\\n-----END RSA PUBLIC KEY-----\\n\\n\"\n    }\n  end\n\n  def scope_factory do\n    %Ecto.Scope{\n      name: SecureRandom.hex(10),\n      public: false\n    }\n  end\n\n  def token_factory do\n    %Ecto.Token{\n      client: build(:client),\n      type: \"access_token\",\n      value: Boruta.TokenGenerator.generate(),\n      expires_at: :os.system_time(:seconds) + 10\n    }\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/support/boruta_identity_factory.ex",
    "content": "defmodule BorutaIdentity.Factory do\n  @moduledoc false\n\n  use ExMachina.Ecto, repo: BorutaIdentity.Repo\n\n  alias BorutaIdentity.Accounts.Consent\n  alias BorutaIdentity.Accounts.Internal\n  alias BorutaIdentity.Accounts.Role\n  alias BorutaIdentity.Accounts.RoleScope\n  alias BorutaIdentity.Accounts.User\n  alias BorutaIdentity.Configuration.ErrorTemplate\n  alias BorutaIdentity.IdentityProviders.Backend\n  alias BorutaIdentity.IdentityProviders.ClientIdentityProvider\n  alias BorutaIdentity.IdentityProviders.IdentityProvider\n  alias BorutaIdentity.IdentityProviders.Template\n\n  # @password \"hello world!\"\n  @hashed_password \"$argon2id$v=19$m=131072,t=8,p=4$9lPv7KsJogno0FlnhaRQXA$TeTY9FYjR1HJtZzg+N1z0oDC+0Mn7buPpOMhDP+M2Ik\"\n\n  def user_factory do\n    %User{\n      username: \"user#{System.unique_integer()}@example.com\",\n      uid: SecureRandom.hex(),\n      backend: build(:backend)\n    }\n  end\n\n  def internal_user_factory do\n    %Internal.User{\n      email: \"user#{System.unique_integer()}@example.com\",\n      hashed_password: @hashed_password,\n      backend: build(:backend)\n    }\n  end\n\n  def consent_factory do\n    %Consent{\n      client_id: SecureRandom.uuid(),\n      scopes: []\n    }\n  end\n\n  def client_identity_provider_factory do\n    %ClientIdentityProvider{\n      client_id: SecureRandom.uuid(),\n      identity_provider: build(:identity_provider)\n    }\n  end\n\n  def identity_provider_factory do\n    %IdentityProvider{\n      name: sequence(:name, &\"identity provider #{&1}\"),\n      backend: build(:backend)\n    }\n  end\n\n  def backend_factory do\n    %Backend{\n      name: \"backend name\",\n      type: \"Elixir.BorutaIdentity.Accounts.Internal\"\n    }\n  end\n\n  def template_factory do\n    %Template{\n      type: \"template_type\",\n      content: \"template content\"\n    }\n  end\n\n  def new_registration_template_factory do\n    %Template{\n      type: \"new_registration\",\n      content: Template.default_content(:new_registration)\n    }\n  end\n\n  def error_template_factory do\n    %ErrorTemplate{\n      type: \"400\",\n      content: \"error template content\"\n    }\n  end\n\n  def role_factory do\n    %Role{\n      name: SecureRandom.hex(32)\n    }\n  end\n\n  def role_scope_factory do\n    %RoleScope{}\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/support/conn_case.ex",
    "content": "defmodule BorutaWeb.ConnCase do\n  @moduledoc \"\"\"\n  This module defines the test case to be used by\n  tests that require setting up a connection.\n\n  Such tests rely on `Phoenix.ConnTest` and also\n  import other functionality to make it easier\n  to build common data structures and query the data layer.\n\n  Finally, if the test case interacts with the database,\n  it cannot be async. For this reason, every test runs\n  inside a transaction which is reset at the beginning\n  of the test unless the test case is marked as async.\n  \"\"\"\n\n  use ExUnit.CaseTemplate\n\n  alias Boruta.Ecto.Scopes\n  alias Ecto.Adapters.SQL.Sandbox\n\n  using do\n    quote do\n      # Import conveniences for testing with connections\n      import Plug.Conn\n      import Phoenix.ConnTest\n      import BorutaIdentityWeb.ConnCase\n      import BorutaWeb.ConnCase\n\n      alias BorutaIdentityWeb.Router.Helpers, as: IdentityRoutes\n      alias BorutaWeb.Router.Helpers, as: Routes\n\n      # The default endpoint for testing\n      @endpoint BorutaWeb.Endpoint\n    end\n  end\n\n  setup tags do\n    :ok = Sandbox.checkout(BorutaIdentity.Repo)\n    :ok = Sandbox.checkout(BorutaWeb.Repo)\n    :ok = Sandbox.checkout(BorutaAuth.Repo)\n\n    :ok = Scopes.invalidate(:public)\n\n    unless tags[:async] do\n      Sandbox.mode(BorutaIdentity.Repo, {:shared, self()})\n      Sandbox.mode(BorutaWeb.Repo, {:shared, self()})\n    end\n\n    {:ok, conn: Phoenix.ConnTest.build_conn()}\n  end\nend\n"
  },
  {
    "path": "apps/boruta_web/test/test_helper.exs",
    "content": "ExUnit.start()\n\nEcto.Adapters.SQL.Sandbox.mode(BorutaIdentity.Repo, :manual)\nEcto.Adapters.SQL.Sandbox.mode(BorutaWeb.Repo, :manual)\nEcto.Adapters.SQL.Sandbox.mode(BorutaAuth.Repo, :manual)\n\nApplication.ensure_all_started(:bypass)\n\nLogger.remove_backend(:console)\n"
  },
  {
    "path": "boruta-admin.openapi.json",
    "content": "{\r\n  \"openapi\": \"3.0.0\",\r\n  \"info\": {\r\n    \"title\": \"Boruta Administration\",\r\n    \"contact\": {},\r\n    \"version\": \"1.0\"\r\n  },\r\n  \"servers\": [\r\n    {\r\n      \"url\": \"http://example.com\",\r\n      \"variables\": {}\r\n    }\r\n  ],\r\n  \"paths\": {\r\n    \"/api/clients\": {\r\n      \"get\": {\r\n        \"tags\": [\r\n          \"Clients\"\r\n        ],\r\n        \"summary\": \"list clients\",\r\n        \"operationId\": \"listclients\",\r\n        \"parameters\": [],\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"type\": \"array\",\r\n                      \"items\": {\r\n                        \"$ref\": \"#/components/schemas/Client\"\r\n                      }\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"post\": {\r\n        \"tags\": [\r\n          \"Clients\"\r\n        ],\r\n        \"summary\": \"create a client\",\r\n        \"operationId\": \"createaclient\",\r\n        \"parameters\": [],\r\n        \"requestBody\": {\r\n          \"description\": \"\",\r\n          \"content\": {\r\n            \"application/json\": {\r\n              \"schema\": {\r\n                \"$ref\": \"#/components/schemas/createaclientrequest\"\r\n              },\r\n              \"example\": {\r\n                \"client\": {\r\n                  \"access_token_ttl\": 86400,\r\n                  \"authorization_code_ttl\": 60,\r\n                  \"authorize_scope\": false,\r\n                  \"authorized_scopes\": [],\r\n                  \"id\": \"b70e3293-d661-4eee-a709-d5161cb27d9d\",\r\n                  \"id_token_ttl\": 86400,\r\n                  \"name\": \"Created from API\",\r\n                  \"pkce\": false,\r\n                  \"public_refresh_token\": false,\r\n                  \"public_revoke\": false,\r\n                  \"redirect_uris\": [],\r\n                  \"refresh_token_ttl\": 2592000,\r\n                  \"identity_provider\": {\r\n                    \"id\": \"f74e3d72-b41c-4fad-aa38-635c45831a1e\"\r\n                  },\r\n                  \"secret\": \"GMx4LfhqIkLea1rzYGcr5cEmZ9ugif5EgKHQ3qgdvJ72Nyfyk9wC6YU7YICedaDEd8ZVNrOkoAV77dQPzBWLgm\",\r\n                  \"supported_grant_types\": [\r\n                    \"client_credentials\",\r\n                    \"password\",\r\n                    \"authorization_code\",\r\n                    \"refresh_token\",\r\n                    \"implicit\",\r\n                    \"revoke\",\r\n                    \"introspect\"\r\n                  ]\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"required\": true\r\n        },\r\n        \"responses\": {\r\n          \"201\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/Client\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      }\r\n    },\r\n    \"/api/clients/{client_id}\": {\r\n      \"get\": {\r\n        \"tags\": [\r\n          \"Clients\"\r\n        ],\r\n        \"summary\": \"show a client\",\r\n        \"operationId\": \"showaclient\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"client_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/Client\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"patch\": {\r\n        \"tags\": [\r\n          \"Clients\"\r\n        ],\r\n        \"summary\": \"update a client\",\r\n        \"operationId\": \"updateaclient\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"client_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"requestBody\": {\r\n          \"description\": \"\",\r\n          \"content\": {\r\n            \"application/json\": {\r\n              \"schema\": {\r\n                \"$ref\": \"#/components/schemas/updateaclientrequest\"\r\n              },\r\n              \"example\": {\r\n                \"client\": {\r\n                  \"access_token_ttl\": 86400,\r\n                  \"authorization_code_ttl\": 60,\r\n                  \"authorize_scope\": false,\r\n                  \"authorized_scopes\": [],\r\n                  \"id\": \"b70e3293-d661-4eee-a709-d5161cb27d9d\",\r\n                  \"id_token_ttl\": 86400,\r\n                  \"name\": \"Updated from API\",\r\n                  \"pkce\": false,\r\n                  \"public_refresh_token\": false,\r\n                  \"public_revoke\": false,\r\n                  \"redirect_uris\": [],\r\n                  \"refresh_token_ttl\": 2592000,\r\n                  \"identity_provider\": {\r\n                    \"id\": \"f74e3d72-b41c-4fad-aa38-635c45831a1e\"\r\n                  },\r\n                  \"secret\": \"GMx4LfhqIkLea1rzYGcr5cEmZ9ugif5EgKHQ3qgdvJ72Nyfyk9wC6YU7YICedaDEd8ZVNrOkoAV77dQPzBWLgm\",\r\n                  \"supported_grant_types\": [\r\n                    \"client_credentials\",\r\n                    \"password\",\r\n                    \"authorization_code\",\r\n                    \"refresh_token\",\r\n                    \"implicit\",\r\n                    \"revoke\",\r\n                    \"introspect\"\r\n                  ]\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"required\": true\r\n        },\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/Client\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"delete\": {\r\n        \"tags\": [\r\n          \"Clients\"\r\n        ],\r\n        \"summary\": \"delete a client\",\r\n        \"operationId\": \"deleteaclient\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"client_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"responses\": {\r\n          \"204\": {\r\n            \"description\": \"\",\r\n            \"headers\": {}\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      }\r\n    },\r\n    \"/api/scopes\": {\r\n      \"get\": {\r\n        \"tags\": [\r\n          \"Scopes\"\r\n        ],\r\n        \"summary\": \"list scopes\",\r\n        \"operationId\": \"listscopes\",\r\n        \"parameters\": [],\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"array\",\r\n                  \"items\": {\r\n                    \"$ref\": \"#/components/schemas/Scope\"\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"post\": {\r\n        \"tags\": [\r\n          \"Scopes\"\r\n        ],\r\n        \"summary\": \"create a scope\",\r\n        \"operationId\": \"createascope\",\r\n        \"parameters\": [],\r\n        \"requestBody\": {\r\n          \"description\": \"\",\r\n          \"content\": {\r\n            \"application/json\": {\r\n              \"schema\": {\r\n                \"$ref\": \"#/components/schemas/createascoperequest\"\r\n              },\r\n              \"example\": {\r\n                \"scope\": {\r\n                  \"name\": \"from:api\",\r\n                  \"label\": \"Created from API\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"required\": true\r\n        },\r\n        \"responses\": {\r\n          \"201\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/Scope\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      }\r\n    },\r\n    \"/api/scopes/{scope_id}\": {\r\n      \"get\": {\r\n        \"tags\": [\r\n          \"Scopes\"\r\n        ],\r\n        \"summary\": \"show a scope\",\r\n        \"operationId\": \"showascope\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"scope_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/Scope\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"patch\": {\r\n        \"tags\": [\r\n          \"Scopes\"\r\n        ],\r\n        \"summary\": \"update a scope\",\r\n        \"operationId\": \"updateascope\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"scope_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"requestBody\": {\r\n          \"description\": \"\",\r\n          \"content\": {\r\n            \"application/json\": {\r\n              \"schema\": {\r\n                \"$ref\": \"#/components/schemas/updateascoperequest\"\r\n              },\r\n              \"example\": {\r\n                \"scope\": {\r\n                  \"label\": \"Updated from API\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"required\": true\r\n        },\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/Scope\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"delete\": {\r\n        \"tags\": [\r\n          \"Scopes\"\r\n        ],\r\n        \"summary\": \"delete a scope\",\r\n        \"operationId\": \"deleteascope\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"scope_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"responses\": {\r\n          \"204\": {\r\n            \"description\": \"\",\r\n            \"headers\": {}\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      }\r\n    },\r\n    \"/api/upstreams\": {\r\n      \"get\": {\r\n        \"tags\": [\r\n          \"Upstreams\"\r\n        ],\r\n        \"summary\": \"list upstreams\",\r\n        \"operationId\": \"listupstreams\",\r\n        \"parameters\": [],\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"array\",\r\n                  \"items\": {\r\n                    \"$ref\": \"#/components/schemas/Upstream\"\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"post\": {\r\n        \"tags\": [\r\n          \"Upstreams\"\r\n        ],\r\n        \"summary\": \"create an upstream\",\r\n        \"operationId\": \"createanupstream\",\r\n        \"parameters\": [],\r\n        \"requestBody\": {\r\n          \"description\": \"\",\r\n          \"content\": {\r\n            \"application/json\": {\r\n              \"schema\": {\r\n                \"$ref\": \"#/components/schemas/createanupstreamrequest\"\r\n              },\r\n              \"example\": {\r\n                \"upstream\": {\r\n                  \"authorize\": true,\r\n                  \"host\": \"from.api\",\r\n                  \"pool_size\": 10,\r\n                  \"port\": \"80\",\r\n                  \"required_scopes\": {\r\n                    \"GET\": [\r\n                      \"test\"\r\n                    ]\r\n                  },\r\n                  \"scheme\": \"http\",\r\n                  \"strip_uri\": true,\r\n                  \"uris\": [\r\n                    \"/from-api\"\r\n                  ]\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"required\": true\r\n        },\r\n        \"responses\": {\r\n          \"201\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/Upstream\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      }\r\n    },\r\n    \"/api/upstreams/{upstream_id}\": {\r\n      \"get\": {\r\n        \"tags\": [\r\n          \"Upstreams\"\r\n        ],\r\n        \"summary\": \"show an upstream\",\r\n        \"operationId\": \"showanupstream\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"upstream_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/Upstream\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"patch\": {\r\n        \"tags\": [\r\n          \"Upstreams\"\r\n        ],\r\n        \"summary\": \"update an upstream\",\r\n        \"operationId\": \"updateanupstream\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"upstream_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"requestBody\": {\r\n          \"description\": \"\",\r\n          \"content\": {\r\n            \"application/json\": {\r\n              \"schema\": {\r\n                \"$ref\": \"#/components/schemas/updateanupstreamrequest\"\r\n              },\r\n              \"example\": {\r\n                \"upstream\": {\r\n                  \"authorize\": true,\r\n                  \"host\": \"from.api.updated\",\r\n                  \"pool_size\": 10,\r\n                  \"port\": \"80\",\r\n                  \"required_scopes\": {\r\n                    \"GET\": [\r\n                      \"test\"\r\n                    ]\r\n                  },\r\n                  \"scheme\": \"http\",\r\n                  \"strip_uri\": true,\r\n                  \"uris\": [\r\n                    \"/from-api\"\r\n                  ]\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"required\": true\r\n        },\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/Upstream\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"delete\": {\r\n        \"tags\": [\r\n          \"Upstreams\"\r\n        ],\r\n        \"summary\": \"delete an upstream\",\r\n        \"operationId\": \"deleteanupstream\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"upstream_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"responses\": {\r\n          \"204\": {\r\n            \"description\": \"\",\r\n            \"headers\": {}\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      }\r\n    },\r\n    \"/api/identity-providers\": {\r\n      \"get\": {\r\n        \"tags\": [\r\n          \"identity providers\"\r\n        ],\r\n        \"summary\": \"list identity providers\",\r\n        \"operationId\": \"listidentityProviders\",\r\n        \"parameters\": [],\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"array\",\r\n                  \"items\": {\r\n                    \"$ref\": \"#/components/schemas/IdentityProvider\"\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"post\": {\r\n        \"tags\": [\r\n          \"identity providers\"\r\n        ],\r\n        \"summary\": \"create a identity provider\",\r\n        \"operationId\": \"createaidentityProvider\",\r\n        \"parameters\": [],\r\n        \"requestBody\": {\r\n          \"description\": \"\",\r\n          \"content\": {\r\n            \"application/json\": {\r\n              \"schema\": {\r\n                \"$ref\": \"#/components/schemas/createaidentityProviderrequest\"\r\n              },\r\n              \"example\": {\r\n                \"identity_provider\": {\r\n                  \"choose_session\": true,\r\n                  \"confirmable\": false,\r\n                  \"consentable\": false,\r\n                  \"name\": \"Created fom API\",\r\n                  \"registrable\": false,\r\n                  \"type\": \"internal\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"required\": true\r\n        },\r\n        \"responses\": {\r\n          \"201\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/IdentityProvider\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      }\r\n    },\r\n    \"/api/identity-providers/{identity_provider_id}\": {\r\n      \"get\": {\r\n        \"tags\": [\r\n          \"identity providers\"\r\n        ],\r\n        \"summary\": \"show a identity provider\",\r\n        \"operationId\": \"showaidentityProvider\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"identity_provider_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/IdentityProvider\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"patch\": {\r\n        \"tags\": [\r\n          \"identity providers\"\r\n        ],\r\n        \"summary\": \"update a identity provider\",\r\n        \"operationId\": \"updateaidentityProvider\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"identity_provider_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"requestBody\": {\r\n          \"description\": \"\",\r\n          \"content\": {\r\n            \"application/json\": {\r\n              \"schema\": {\r\n                \"$ref\": \"#/components/schemas/updateaidentityProviderrequest\"\r\n              },\r\n              \"example\": {\r\n                \"identity_provider\": {\r\n                  \"choose_session\": true,\r\n                  \"confirmable\": false,\r\n                  \"consentable\": false,\r\n                  \"name\": \"Updated fom API\",\r\n                  \"registrable\": false,\r\n                  \"type\": \"internal\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"required\": true\r\n        },\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/IdentityProvider\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      },\r\n      \"delete\": {\r\n        \"tags\": [\r\n          \"identity providers\"\r\n        ],\r\n        \"summary\": \"delete a identity provider\",\r\n        \"operationId\": \"deleteaidentityProvider\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"identity_provider_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"responses\": {\r\n          \"204\": {\r\n            \"description\": \"\",\r\n            \"headers\": {}\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      }\r\n    },\r\n    \"/api/identity-providers/{identity_provider_id}/templates/{template_name}\": {\r\n      \"get\": {\r\n        \"tags\": [\r\n          \"identity providers\"\r\n        ],\r\n        \"summary\": \"show a identity provider template\",\r\n        \"operationId\": \"showaidentityProvidertemplate\",\r\n        \"parameters\": [\r\n          {\r\n            \"name\": \"identity_provider_id\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          },\r\n          {\r\n            \"name\": \"template_name\",\r\n            \"in\": \"path\",\r\n            \"description\": \"\",\r\n            \"required\": true,\r\n            \"style\": \"simple\",\r\n            \"schema\": {\r\n              \"type\": \"string\"\r\n            }\r\n          }\r\n        ],\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"$ref\": \"#/components/schemas/Template\"\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      }\r\n    },\r\n    \"/api/users\": {\r\n      \"get\": {\r\n        \"tags\": [\r\n          \"identity providers\"\r\n        ],\r\n        \"summary\": \"list users\",\r\n        \"operationId\": \"listusers\",\r\n        \"parameters\": [],\r\n        \"responses\": {\r\n          \"200\": {\r\n            \"description\": \"\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"type\": \"object\",\r\n                  \"properties\": {\r\n                    \"data\": {\r\n                      \"type\": \"array\",\r\n                      \"items\": {\r\n                        \"$ref\": \"#/components/schemas/User\"\r\n                      }\r\n                    }\r\n                  }\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"401\": {\r\n            \"description\": \"The client is unauthorized to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Unauthorized\"\r\n                }\r\n              }\r\n            }\r\n          },\r\n          \"403\": {\r\n            \"description\": \"The client is forbidden to access this resource.\",\r\n            \"headers\": {},\r\n            \"content\": {\r\n              \"application/json\": {\r\n                \"schema\": {\r\n                  \"$ref\": \"#/components/schemas/Forbidden\"\r\n                }\r\n              }\r\n            }\r\n          }\r\n        },\r\n        \"deprecated\": false,\r\n        \"security\": [\r\n          {\r\n            \"bearer\": []\r\n          }\r\n        ]\r\n      }\r\n    }\r\n  },\r\n  \"components\": {\r\n    \"schemas\": {\r\n      \"createaclientrequest\": {\r\n        \"title\": \"createaclientrequest\",\r\n        \"required\": [\r\n          \"client\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"client\": {\r\n            \"$ref\": \"#/components/schemas/Client\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"client\": {\r\n            \"access_token_ttl\": 86400,\r\n            \"authorization_code_ttl\": 60,\r\n            \"authorize_scope\": false,\r\n            \"authorized_scopes\": [],\r\n            \"id\": \"b70e3293-d661-4eee-a709-d5161cb27d9d\",\r\n            \"id_token_ttl\": 86400,\r\n            \"name\": \"Created from API\",\r\n            \"pkce\": false,\r\n            \"public_refresh_token\": false,\r\n            \"public_revoke\": false,\r\n            \"redirect_uris\": [],\r\n            \"refresh_token_ttl\": 2592000,\r\n            \"identity_provider\": {\r\n              \"id\": \"f74e3d72-b41c-4fad-aa38-635c45831a1e\"\r\n            },\r\n            \"secret\": \"GMx4LfhqIkLea1rzYGcr5cEmZ9ugif5EgKHQ3qgdvJ72Nyfyk9wC6YU7YICedaDEd8ZVNrOkoAV77dQPzBWLgm\",\r\n            \"supported_grant_types\": [\r\n              \"client_credentials\",\r\n              \"password\",\r\n              \"authorization_code\",\r\n              \"refresh_token\",\r\n              \"implicit\",\r\n              \"revoke\",\r\n              \"introspect\"\r\n            ]\r\n          }\r\n        }\r\n      },\r\n      \"Client\": {\r\n        \"title\": \"Client\",\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"access_token_ttl\": {\r\n            \"type\": \"integer\",\r\n            \"format\": \"int32\"\r\n          },\r\n          \"authorization_code_ttl\": {\r\n            \"type\": \"integer\",\r\n            \"format\": \"int32\"\r\n          },\r\n          \"authorize_scope\": {\r\n            \"type\": \"boolean\"\r\n          },\r\n          \"authorized_scopes\": {\r\n            \"type\": \"array\",\r\n            \"items\": {\r\n              \"type\": \"string\"\r\n            },\r\n            \"description\": \"\"\r\n          },\r\n          \"id\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"id_token_ttl\": {\r\n            \"type\": \"integer\",\r\n            \"format\": \"int32\"\r\n          },\r\n          \"name\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"pkce\": {\r\n            \"type\": \"boolean\"\r\n          },\r\n          \"public_refresh_token\": {\r\n            \"type\": \"boolean\"\r\n          },\r\n          \"public_revoke\": {\r\n            \"type\": \"boolean\"\r\n          },\r\n          \"redirect_uris\": {\r\n            \"type\": \"array\",\r\n            \"items\": {\r\n              \"type\": \"string\"\r\n            },\r\n            \"description\": \"\"\r\n          },\r\n          \"refresh_token_ttl\": {\r\n            \"type\": \"integer\",\r\n            \"format\": \"int32\"\r\n          },\r\n          \"identity_provider\": {\r\n            \"$ref\": \"#/components/schemas/IdentityProvider\"\r\n          },\r\n          \"secret\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"supported_grant_types\": {\r\n            \"type\": \"array\",\r\n            \"items\": {\r\n              \"type\": \"string\"\r\n            },\r\n            \"description\": \"\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"access_token_ttl\": 86400,\r\n          \"authorization_code_ttl\": 60,\r\n          \"authorize_scope\": false,\r\n          \"authorized_scopes\": [],\r\n          \"id\": \"b70e3293-d661-4eee-a709-d5161cb27d9d\",\r\n          \"id_token_ttl\": 86400,\r\n          \"name\": \"Created from API\",\r\n          \"pkce\": false,\r\n          \"public_refresh_token\": false,\r\n          \"public_revoke\": false,\r\n          \"redirect_uris\": [],\r\n          \"refresh_token_ttl\": 2592000,\r\n          \"identity_provider\": {\r\n            \"id\": \"f74e3d72-b41c-4fad-aa38-635c45831a1e\"\r\n          },\r\n          \"secret\": \"GMx4LfhqIkLea1rzYGcr5cEmZ9ugif5EgKHQ3qgdvJ72Nyfyk9wC6YU7YICedaDEd8ZVNrOkoAV77dQPzBWLgm\",\r\n          \"supported_grant_types\": [\r\n            \"client_credentials\",\r\n            \"password\",\r\n            \"authorization_code\",\r\n            \"refresh_token\",\r\n            \"implicit\",\r\n            \"revoke\",\r\n            \"introspect\"\r\n          ]\r\n        }\r\n      },\r\n      \"updateaclientrequest\": {\r\n        \"title\": \"updateaclientrequest\",\r\n        \"required\": [\r\n          \"client\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"client\": {\r\n            \"$ref\": \"#/components/schemas/Client\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"client\": {\r\n            \"access_token_ttl\": 86400,\r\n            \"authorization_code_ttl\": 60,\r\n            \"authorize_scope\": false,\r\n            \"authorized_scopes\": [],\r\n            \"id\": \"b70e3293-d661-4eee-a709-d5161cb27d9d\",\r\n            \"id_token_ttl\": 86400,\r\n            \"name\": \"Updated from API\",\r\n            \"pkce\": false,\r\n            \"public_refresh_token\": false,\r\n            \"public_revoke\": false,\r\n            \"redirect_uris\": [],\r\n            \"refresh_token_ttl\": 2592000,\r\n            \"identity_provider\": {\r\n              \"id\": \"f74e3d72-b41c-4fad-aa38-635c45831a1e\"\r\n            },\r\n            \"secret\": \"GMx4LfhqIkLea1rzYGcr5cEmZ9ugif5EgKHQ3qgdvJ72Nyfyk9wC6YU7YICedaDEd8ZVNrOkoAV77dQPzBWLgm\",\r\n            \"supported_grant_types\": [\r\n              \"client_credentials\",\r\n              \"password\",\r\n              \"authorization_code\",\r\n              \"refresh_token\",\r\n              \"implicit\",\r\n              \"revoke\",\r\n              \"introspect\"\r\n            ]\r\n          }\r\n        }\r\n      },\r\n      \"createascoperequest\": {\r\n        \"title\": \"createascoperequest\",\r\n        \"required\": [\r\n          \"scope\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"scope\": {\r\n            \"$ref\": \"#/components/schemas/Scope\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"scope\": {\r\n            \"name\": \"from:api\",\r\n            \"label\": \"Created from API\"\r\n          }\r\n        }\r\n      },\r\n      \"Scope\": {\r\n        \"title\": \"Scope\",\r\n        \"required\": [\r\n          \"name\",\r\n          \"label\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"name\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"label\": {\r\n            \"type\": \"string\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"name\": \"from:api\",\r\n          \"label\": \"Created from API\"\r\n        }\r\n      },\r\n      \"updateascoperequest\": {\r\n        \"title\": \"updateascoperequest\",\r\n        \"required\": [\r\n          \"scope\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"scope\": {\r\n            \"$ref\": \"#/components/schemas/Scope1\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"scope\": {\r\n            \"label\": \"Updated from API\"\r\n          }\r\n        }\r\n      },\r\n      \"Scope1\": {\r\n        \"title\": \"Scope1\",\r\n        \"required\": [\r\n          \"label\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"label\": {\r\n            \"type\": \"string\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"label\": \"Updated from API\"\r\n        }\r\n      },\r\n      \"deleteascoperequest\": {\r\n        \"title\": \"deleteascoperequest\",\r\n        \"required\": [\r\n          \"scope\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"scope\": {\r\n            \"$ref\": \"#/components/schemas/Scope1\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"scope\": {\r\n            \"label\": \"Updated from API\"\r\n          }\r\n        }\r\n      },\r\n      \"createanupstreamrequest\": {\r\n        \"title\": \"createanupstreamrequest\",\r\n        \"required\": [\r\n          \"upstream\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"upstream\": {\r\n            \"$ref\": \"#/components/schemas/Upstream\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"upstream\": {\r\n            \"authorize\": true,\r\n            \"host\": \"from.api\",\r\n            \"pool_size\": 10,\r\n            \"port\": \"80\",\r\n            \"required_scopes\": {\r\n              \"GET\": [\r\n                \"test\"\r\n              ]\r\n            },\r\n            \"scheme\": \"http\",\r\n            \"strip_uri\": true,\r\n            \"uris\": [\r\n              \"/from-api\"\r\n            ]\r\n          }\r\n        }\r\n      },\r\n      \"Upstream\": {\r\n        \"title\": \"Upstream\",\r\n        \"required\": [\r\n          \"authorize\",\r\n          \"host\",\r\n          \"pool_size\",\r\n          \"port\",\r\n          \"required_scopes\",\r\n          \"scheme\",\r\n          \"strip_uri\",\r\n          \"uris\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"authorize\": {\r\n            \"type\": \"boolean\"\r\n          },\r\n          \"host\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"pool_size\": {\r\n            \"type\": \"integer\",\r\n            \"format\": \"int32\"\r\n          },\r\n          \"port\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"required_scopes\": {\r\n            \"$ref\": \"#/components/schemas/RequiredScopes\"\r\n          },\r\n          \"scheme\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"strip_uri\": {\r\n            \"type\": \"boolean\"\r\n          },\r\n          \"uris\": {\r\n            \"type\": \"array\",\r\n            \"items\": {\r\n              \"type\": \"string\"\r\n            },\r\n            \"description\": \"\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"authorize\": true,\r\n          \"host\": \"from.api\",\r\n          \"pool_size\": 10,\r\n          \"port\": \"80\",\r\n          \"required_scopes\": {\r\n            \"GET\": [\r\n              \"test\"\r\n            ]\r\n          },\r\n          \"scheme\": \"http\",\r\n          \"strip_uri\": true,\r\n          \"uris\": [\r\n            \"/from-api\"\r\n          ]\r\n        }\r\n      },\r\n      \"RequiredScopes\": {\r\n        \"title\": \"RequiredScopes\",\r\n        \"required\": [\r\n          \"GET\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"GET\": {\r\n            \"type\": \"array\",\r\n            \"items\": {\r\n              \"type\": \"string\"\r\n            },\r\n            \"description\": \"\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"GET\": [\r\n            \"test\"\r\n          ]\r\n        }\r\n      },\r\n      \"updateanupstreamrequest\": {\r\n        \"title\": \"updateanupstreamrequest\",\r\n        \"required\": [\r\n          \"upstream\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"upstream\": {\r\n            \"$ref\": \"#/components/schemas/Upstream\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"upstream\": {\r\n            \"authorize\": true,\r\n            \"host\": \"from.api.updated\",\r\n            \"pool_size\": 10,\r\n            \"port\": \"80\",\r\n            \"required_scopes\": {\r\n              \"GET\": [\r\n                \"test\"\r\n              ]\r\n            },\r\n            \"scheme\": \"http\",\r\n            \"strip_uri\": true,\r\n            \"uris\": [\r\n              \"/from-api\"\r\n            ]\r\n          }\r\n        }\r\n      },\r\n      \"deleteanupstreamrequest\": {\r\n        \"title\": \"deleteanupstreamrequest\",\r\n        \"required\": [\r\n          \"scope\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"scope\": {\r\n            \"$ref\": \"#/components/schemas/Scope1\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"scope\": {\r\n            \"label\": \"Updated from API\"\r\n          }\r\n        }\r\n      },\r\n      \"createaidentityProviderrequest\": {\r\n        \"title\": \"createaidentityProviderrequest\",\r\n        \"required\": [\r\n          \"identity_provider\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"identity_provider\": {\r\n            \"$ref\": \"#/components/schemas/IdentityProvider\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"identity_provider\": {\r\n            \"choose_session\": true,\r\n            \"confirmable\": false,\r\n            \"consentable\": false,\r\n            \"name\": \"Created fom API\",\r\n            \"registrable\": false,\r\n            \"type\": \"internal\"\r\n          }\r\n        }\r\n      },\r\n      \"IdentityProvider\": {\r\n        \"title\": \"IdentityProvider\",\r\n        \"required\": [\r\n          \"choose_session\",\r\n          \"confirmable\",\r\n          \"consentable\",\r\n          \"name\",\r\n          \"registrable\",\r\n          \"type\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"choose_session\": {\r\n            \"type\": \"boolean\"\r\n          },\r\n          \"confirmable\": {\r\n            \"type\": \"boolean\"\r\n          },\r\n          \"consentable\": {\r\n            \"type\": \"boolean\"\r\n          },\r\n          \"name\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"registrable\": {\r\n            \"type\": \"boolean\"\r\n          },\r\n          \"type\": {\r\n            \"type\": \"string\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"choose_session\": true,\r\n          \"confirmable\": false,\r\n          \"consentable\": false,\r\n          \"name\": \"Created fom API\",\r\n          \"registrable\": false,\r\n          \"type\": \"internal\"\r\n        }\r\n      },\r\n      \"Template\": {\r\n        \"title\": \"Template\",\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"id\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"content\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"identity_provider_id\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"type\": {\r\n            \"type\": \"string\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"content\": \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n  <head>\\n    <meta charset=\\\"utf-8\\\"/>\\n    <meta http-equiv=\\\"X-UA-Compatible\\\" content=\\\"IE=edge\\\"/>\\n    <meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\"/>\\n    <link rel=\\\"stylesheet\\\" href=\\\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\\\" integrity=\\\"sha512-8bHTC73gkZ7rZ7vpqUQThUDhqcNFyYi2xgDgPDHc+GXVGHXq+xPjynxIopALmOPqzo9JZj0k6OqqewdGO3EsrQ==\\\" crossorigin=\\\"anonymous\\\" />\\n    <title>BorutaIdentity · Phoenix Framework</title>\\n    <style>\\n    html, body {\\n      height: 100%;\\n      margin: 0;\\n    }\\n\\n    .wrapper {\\n      height: 100%;\\n      display: flex;\\n    }\\n\\n    .main.message {\\n      position: absolute;\\n      top: 1rem;\\n      left: 1rem;\\n      right: 1rem;\\n    }\\n\\n    header {\\n      height: 100%;\\n      width: 50%;\\n      background-image: url('https://picsum.photos/1920/1080?grayscale');\\n      background-size: cover;\\n      background-position: center;\\n    }\\n\\n    main {\\n      flex: 1;\\n      position: relative;\\n      display: flex;\\n      align-items: center;\\n      justify-content: center;\\n      padding: 1rem;\\n    }\\n\\n    main>.content {\\n      flex: 1;\\n      max-width: 500px;\\n    }\\n\\n    @media (max-width: 768px) {\\n      header {\\n        display: none;\\n      }\\n    }\\n    </style>\\n  </head>\\n  <body>\\n    <div class=\\\"wrapper\\\">\\n      <header></header>\\n      <main role=\\\"main\\\" class=\\\"container\\\">\\n        <div class=\\\"content\\\">\\n          {{#messages}}\\n          <div class=\\\"ui main {{ type }} message\\\">\\n            {{ content }}\\n          </div>\\n          {{/messages}}\\n          {{> inner_content }}\\n        </div>\\n      </main>\\n    </div>\\n  </body>\\n</html>\\n\",\r\n          \"id\": null,\r\n          \"identity_provider_id\": \"75da9775-2f6f-4418-aff5-b98e63830079\",\r\n          \"type\": \"layout\"\r\n        }\r\n      },\r\n      \"User\": {\r\n        \"title\": \"User\",\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"id\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"email\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"authorized_scopes\": {\r\n            \"type\": \"array\",\r\n            \"items\": {\r\n              \"$ref\": \"#/components/schemas/Scope\"\r\n            }\r\n          },\r\n          \"type\": {\r\n            \"type\": \"string\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"content\": \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n  <head>\\n    <meta charset=\\\"utf-8\\\"/>\\n    <meta http-equiv=\\\"X-UA-Compatible\\\" content=\\\"IE=edge\\\"/>\\n    <meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1.0\\\"/>\\n    <link rel=\\\"stylesheet\\\" href=\\\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css\\\" integrity=\\\"sha512-8bHTC73gkZ7rZ7vpqUQThUDhqcNFyYi2xgDgPDHc+GXVGHXq+xPjynxIopALmOPqzo9JZj0k6OqqewdGO3EsrQ==\\\" crossorigin=\\\"anonymous\\\" />\\n    <title>BorutaIdentity · Phoenix Framework</title>\\n    <style>\\n    html, body {\\n      height: 100%;\\n      margin: 0;\\n    }\\n\\n    .wrapper {\\n      height: 100%;\\n      display: flex;\\n    }\\n\\n    .main.message {\\n      position: absolute;\\n      top: 1rem;\\n      left: 1rem;\\n      right: 1rem;\\n    }\\n\\n    header {\\n      height: 100%;\\n      width: 50%;\\n      background-image: url('https://picsum.photos/1920/1080?grayscale');\\n      background-size: cover;\\n      background-position: center;\\n    }\\n\\n    main {\\n      flex: 1;\\n      position: relative;\\n      display: flex;\\n      align-items: center;\\n      justify-content: center;\\n      padding: 1rem;\\n    }\\n\\n    main>.content {\\n      flex: 1;\\n      max-width: 500px;\\n    }\\n\\n    @media (max-width: 768px) {\\n      header {\\n        display: none;\\n      }\\n    }\\n    </style>\\n  </head>\\n  <body>\\n    <div class=\\\"wrapper\\\">\\n      <header></header>\\n      <main role=\\\"main\\\" class=\\\"container\\\">\\n        <div class=\\\"content\\\">\\n          {{#messages}}\\n          <div class=\\\"ui main {{ type }} message\\\">\\n            {{ content }}\\n          </div>\\n          {{/messages}}\\n          {{> inner_content }}\\n        </div>\\n      </main>\\n    </div>\\n  </body>\\n</html>\\n\",\r\n          \"id\": null,\r\n          \"identity_provider_id\": \"75da9775-2f6f-4418-aff5-b98e63830079\",\r\n          \"type\": \"layout\"\r\n        }\r\n      },\r\n      \"updateaidentityProviderrequest\": {\r\n        \"title\": \"updateaidentityProviderrequest\",\r\n        \"required\": [\r\n          \"identity_provider\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"identity_provider\": {\r\n            \"$ref\": \"#/components/schemas/IdentityProvider\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"identity_provider\": {\r\n            \"choose_session\": true,\r\n            \"confirmable\": false,\r\n            \"consentable\": false,\r\n            \"name\": \"Updated fom API\",\r\n            \"registrable\": false,\r\n            \"type\": \"internal\"\r\n          }\r\n        }\r\n      },\r\n      \"deleteaidentityProviderrequest\": {\r\n        \"title\": \"deleteaidentityProviderrequest\",\r\n        \"required\": [\r\n          \"scope\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"scope\": {\r\n            \"$ref\": \"#/components/schemas/Scope1\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"scope\": {\r\n            \"label\": \"Updated from API\"\r\n          }\r\n        }\r\n      },\r\n      \"Unauthorized\": {\r\n        \"title\": \"Unauthorized\",\r\n        \"required\": [\r\n          \"code\",\r\n          \"message\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"code\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"message\": {\r\n            \"type\": \"string\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"code\": \"UNAUTHORIZED\",\r\n          \"message\": \"The client is unauthorized to access this resource.\"\r\n        }\r\n      },\r\n      \"Forbidden\": {\r\n        \"title\": \"Forbidden\",\r\n        \"required\": [\r\n          \"code\",\r\n          \"message\"\r\n        ],\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n          \"code\": {\r\n            \"type\": \"string\"\r\n          },\r\n          \"message\": {\r\n            \"type\": \"string\"\r\n          }\r\n        },\r\n        \"example\": {\r\n          \"code\": \"FORBIDDEN\",\r\n          \"message\": \"The client is forbidden to access this resource.\"\r\n        }\r\n      }\r\n    },\r\n    \"securitySchemes\": {\r\n      \"bearer\": {\r\n        \"type\": \"http\",\r\n        \"scheme\": \"bearer\"\r\n      }\r\n    }\r\n  },\r\n  \"security\": [],\r\n  \"tags\": [\r\n    {\r\n      \"name\": \"Clients\"\r\n    },\r\n    {\r\n      \"name\": \"Scopes\"\r\n    },\r\n    {\r\n      \"name\": \"Upstreams\"\r\n    },\r\n    {\r\n      \"name\": \"identity providers\"\r\n    },\r\n    {\r\n      \"name\": \"Misc\",\r\n      \"description\": \"\"\r\n    }\r\n  ]\r\n}\r\n"
  },
  {
    "path": "config/config.exs",
    "content": "import Config\n\nfor config <- \"../apps/*/config/config.exs\" |> Path.expand(__DIR__) |> Path.wildcard() do\n  import_config config\nend\n\nconfig :logger,\n  utc_log: true,\n  backends: [\n    {LoggerFileBackend, :boruta_web_business_logger},\n    {LoggerFileBackend, :boruta_web_request_logger},\n    {LoggerFileBackend, :boruta_identity_business_logger},\n    {LoggerFileBackend, :boruta_identity_request_logger},\n    {LoggerFileBackend, :boruta_admin_request_logger},\n    {LoggerFileBackend, :boruta_gateway_business_logger},\n    {LoggerFileBackend, :boruta_gateway_request_logger},\n    :console\n  ]\n\nEnum.map([:request, :business], fn type ->\n  Enum.map([:boruta_web, :boruta_identity, :boruta_admin, :boruta_gateway], fn application ->\n    config :logger, :\"#{application}_#{type}_logger\",\n      format: \"$dateT$timeZ $metadata[$level] $message\\n\",\n      path: \"./log/test\",\n      metadata: [:request_id],\n      metadata_filter: [application: application, type: type],\n      level: :info\n  end)\nend)\n\nconfig :logger, :console,\n  format: \"$dateT$timeZ $metadata[$level] $message\\n\",\n  metadata: [:request_id],\n  level: :info\n\nconfig :phoenix, :json_library, Jason\n\nconfig :mime, :types, %{\n  \"text/event-stream\" => [\"event-stream\"],\n  \"application/jwt\" => [\"jwt\"]\n}\n\nimport_config \"#{Mix.env()}.exs\"\n"
  },
  {
    "path": "config/dev.exs",
    "content": "import Config\n\n# Do not include metadata nor timestamps in development logs\nconfig :logger, :console, level: :debug\n\n# Initialize plugs at runtime for faster development compilation\nconfig :phoenix, :plug_init_mode, :runtime\n\n# Set a higher stacktrace during development. Avoid configuring such\n# in production as building large stacktraces may be expensive.\nconfig :phoenix, :stacktrace_depth, 20\n"
  },
  {
    "path": "config/prod.exs",
    "content": "import Config\n\nconfig :logger, level: :info\n\nconfig :phoenix, :filter_parameters, [\"password\", \"client_secret\"]\n\nconfig :swoosh, local: false\n\nconfig :boruta_web, BorutaWeb.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_web_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 10\n\nconfig :boruta_identity, BorutaIdentity.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_identity_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 10\n\nconfig :boruta_gateway, BorutaGateway.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_gateway_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 10\n\nconfig :boruta_admin, BorutaAdmin.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_gateway_dev\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 10\n\nconfig :boruta_gateway,\n  server: true,\n  sidecar_server: true\n\nconfig :boruta_web, BorutaWeb.Endpoint,\n  server: true,\n  cache_static_manifest: \"priv/static/cache_manifest.json\"\n\nconfig :boruta_admin, BorutaAdminWeb.Endpoint,\n  server: true,\n  cache_static_manifest: \"priv/static/cache_manifest.json\"\n\nconfig :boruta_identity, BorutaIdentity.Endpoint,\n  server: false,\n  cache_static_manifest: \"priv/static/cache_manifest.json\"\n"
  },
  {
    "path": "config/releases.exs",
    "content": "import Config\n\nconfig :boruta_auth, BorutaAuth.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_web\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: String.to_integer(System.get_env(\"POOL_SIZE\", \"5\"))\n\nconfig :boruta_web, BorutaWeb.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_web\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 1\n\nconfig :boruta_identity, BorutaIdentity.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_identity\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: String.to_integer(System.get_env(\"POOL_SIZE\", \"5\")),\n  after_connect: {BorutaIdentity.Repo, :set_limit, []}\n\nconfig :boruta_gateway, BorutaGateway.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_gateway\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 1\n\nconfig :boruta_admin, BorutaAdmin.Repo,\n  username: System.get_env(\"POSTGRES_USER\") || \"postgres\",\n  password: System.get_env(\"POSTGRES_PASSWORD\") || \"postgres\",\n  database: System.get_env(\"POSTGRES_DATABASE\") || \"boruta_admin\",\n  hostname: System.get_env(\"POSTGRES_HOST\") || \"localhost\",\n  pool_size: 1\n\nconfig :boruta_identity, Boruta.Accounts, secret_key_base: System.get_env(\"SECRET_KEY_BASE\")\n\nconfig :boruta_identity, BorutaIdentity.SMTP, adapter: Swoosh.Adapters.SMTP\n\nconfig :boruta_gateway,\n  port: System.get_env(\"BORUTA_GATEWAY_PORT\") |> String.to_integer(),\n  sidecar_port: System.get_env(\"BORUTA_GATEWAY_SIDECAR_PORT\") |> String.to_integer(),\n  configuration_path:\n    System.get_env(\"BORUTA_GATEWAY_CONFIGURATION_PATH\", \"config/example-configuration.yml\"),\n  server: true\n\nconfig :boruta_web, BorutaWeb.Endpoint,\n  http: [\n    port: System.get_env(\"BORUTA_OAUTH_PORT\") |> String.to_integer(),\n    ip: System.get_env(\"BORUTA_OAUTH_BIND\", \"::\") |> String.to_charlist() |> :inet.parse_address() |> elem(1),\n    transport_options: [\n      num_acceptors: String.to_integer(System.get_env(\"WEB_ACCEPTORS\", \"64\"))\n    ]\n  ],\n  url: [scheme: System.get_env(\"BORUTA_OAUTH_SCHEME\", \"https\"), host: System.get_env(\"BORUTA_OAUTH_HOST\")],\n  secret_key_base: System.get_env(\"SECRET_KEY_BASE\")\n\nconfig :boruta_identity, BorutaIdentityWeb.Endpoint,\n  url: [scheme: System.get_env(\"BORUTA_OAUTH_SCHEME\", \"https\"), host: System.get_env(\"BORUTA_OAUTH_HOST\"), path: \"/accounts\", port: System.get_env(\"BORUTA_OAUTH_PORT\")],\n  secret_key_base: System.get_env(\"SECRET_KEY_BASE\")\n\nconfig :boruta_admin, BorutaAdminWeb.Endpoint,\n  http: [\n    port: System.get_env(\"BORUTA_ADMIN_PORT\") |> String.to_integer(),\n    ip: System.get_env(\"BORUTA_ADMIN_BIND\", \"::\") |> String.to_charlist() |> :inet.parse_address() |> elem(1),\n    protocol_options: [idle_timeout: 3_600_000, inactivity_timeout: 3_600_000]\n  ],\n  url: [scheme: \"https\", host: System.get_env(\"BORUTA_ADMIN_HOST\")],\n  secret_key_base: System.get_env(\"SECRET_KEY_BASE\")\n\nconfig :boruta_admin,\n  configuration_path:\n    System.get_env(\"BORUTA_CONFIGURATION_PATH\")\n\nconfig :boruta_web, BorutaAdminWeb.Authorization,\n  oauth2: [\n    site: System.get_env(\"BORUTA_ADMIN_OAUTH_BASE_URL\")\n  ],\n  sub_restricted: System.get_env(\"BORUTA_SUB_RESTRICTED\", nil),\n  organization_restricted: System.get_env(\"BORUTA_ORGANIZATION_RESTRICTED\", nil)\n\nconfig :boruta, Boruta.Oauth,\n  repo: BorutaAuth.Repo,\n  contexts: [\n    resource_owners: BorutaIdentity.ResourceOwners\n  ],\n  max_ttl: [\n    authorization_code: 600\n  ],\n  issuer: System.get_env(\"BORUTA_OAUTH_BASE_URL\"),\n  did_resolver_base_url: System.get_env(\"DID_RESOLVER_BASE_URL\", \"https://api.godiddy.com/1.0.0/universal-resolver\"),\n  did_registrar_base_url: System.get_env(\"DID_REGISTRAR_BASE_URL\", \"https://api.godiddy.com/1.0.0/universal-registrar\"),\n  universal_did_auth: %{\n    type: \"bearer\",\n    token: System.get_env(\"DID_SERVICES_API_KEY\")\n  }\n\nconfig :boruta_auth, BorutaAuth.LogRotate,\n  max_retention_days: String.to_integer(System.get_env(\"MAX_LOG_RETENTION_DAYS\", \"60\"))\n\nif System.get_env(\"K8S_NAMESPACE\") && System.get_env(\"K8S_SELECTOR\") do\n  config :libcluster,\n    topologies: [\n      k8s: [\n        strategy: Cluster.Strategy.Kubernetes,\n        config: [\n          mode: :ip,\n          kubernetes_ip_lookup_mode: :pods,\n          kubernetes_node_basename: \"boruta\",\n          kubernetes_selector: System.get_env(\"K8S_SELECTOR\"),\n          kubernetes_namespace: System.get_env(\"K8S_NAMESPACE\"),\n          polling_interval: 10_000\n        ]\n      ]\n    ]\nelse\n  config :libcluster,\n    topologies: [\n      example: [\n        strategy: Cluster.Strategy.Epmd,\n        config: [hosts: []],\n        connect: {:net_kernel, :connect_node, []},\n        disconnect: {:erlang, :disconnect_node, []},\n        list_nodes: {:erlang, :nodes, [:connected]}\n      ]\n    ]\nend\n"
  },
  {
    "path": "config/test.exs",
    "content": "import Config\n\n# Print only warnings and errors during test\nconfig :logger, level: :warn\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3\"\n\nvolumes:\n  boruta-logs:\n\nservices:\n  postgres:\n    image: postgres:14\n    environment:\n      POSTGRES_USER: \"postgres\"\n      POSTGRES_PASSWORD: \"postgres\"\n      POSTGRES_DATABASE: \"boruta_release\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  boruta:\n    stdin_open: true\n    tty: true\n    build:\n      context: .\n      dockerfile: Dockerfile.full\n      args:\n        BORUTA_OAUTH_BASE_URL: \"http://localhost:8080\"\n    ports:\n      - \"8080:8080\"\n      - \"8081:8081\"\n      - \"8082:8082\"\n      - \"8083:8083\"\n    volumes:\n      - \"boruta-logs:/app/log\"\n    env_file: \"./.env.example\"\n    environment:\n      MIX_ENV: \"prod\"\n      POSTGRES_USER: \"postgres\"\n      POSTGRES_PASSWORD: \"postgres\"\n      POSTGRES_DATABASE: \"boruta_release\"\n      POSTGRES_HOST: \"postgres\"\n    depends_on:\n      postgres:\n        condition: service_healthy\n\n"
  },
  {
    "path": "examples/README.md",
    "content": "# example configurations\n\n## Sidecar authorization gateways\n\nlocated at `examples/sidecar-authorization-gateways`\n\n\n1. run database migrations\n\n```bash\ndocker-compose run boruta-server ./bin/boruta eval \"BorutaWeb.Release.setup()\"\ndocker-compose run boruta-server ./bin/boruta eval \"BorutaGateway.Release.setup()\"\n```\n\n2. load (micro)gateways configuration\n\n```\ndocker-compose run boruta-server ./bin/boruta eval \"BorutaGateway.Release.load_configuration()\"\ndocker-compose run httpbin-sidecar ./bin/boruta_gateway eval \"BorutaGateway.Release.load_configuration()\"\ndocker-compose run protected-httpbin-sidecar ./bin/boruta_gateway eval \"BorutaGateway.Release.load_configuration()\"\n```\n\nOnce done, you can run the docker images as follow:\n\n```bash\ndocker-compose up\n```\n\nThe applications will be available on different ports (depending on the docker compose environment configuration):\n- http://localhost:8080 for the authorization server\n- http://localhost:8081 for the admin interface\n- http://localhost:8082 for the gateway\n\nThe gateway will have two endpoints:\n- `http://localhost:8082/httpbin` which expose the httpbin service publicly\n- `http://localhost:8082/protected-httpbin` which expose the httpbin and restrict traffic to test scope granted users\n"
  },
  {
    "path": "examples/sidecar-authorization-gateways/config/example-gateway-configuration.yml",
    "content": "---\nconfiguration:\n  gateway:\n    - authorize: false\n      error_content_type: \"text/plain\"\n      forbidden_response: \"forbidden\"\n      unauthorized_response: \"unauthorized\"\n      forwarded_token_secret: \"this is a secret\"\n      forwarded_token_signature_alg: \"HS512\"\n      pool_count: 1\n      pool_size: 10\n      max_idle_time: 10\n      scheme: \"http\"\n      host: \"httpbin-sidecar\"\n      port: 8083\n      uris: [\"/httpbin\"]\n      strip_uri: true\n    - authorize: false\n      error_content_type: \"text/plain\"\n      forbidden_response: \"forbidden\"\n      unauthorized_response: \"unauthorized\"\n      forwarded_token_secret: \"this is a secret\"\n      forwarded_token_signature_alg: \"HS512\"\n      pool_count: 1\n      pool_size: 10\n      max_idle_time: 10\n      scheme: \"http\"\n      host: \"protected-httpbin-sidecar\"\n      port: 8083\n      uris: [\"/protected-httpbin\"]\n      strip_uri: true\n"
  },
  {
    "path": "examples/sidecar-authorization-gateways/config/example-httpbin-configuration.yml",
    "content": "---\nconfiguration:\n  node_name: \"httpbin\"\n  microgateway:\n    - authorize: false\n      error_content_type: \"text\"\n      forbidden_response: \"forbidden\"\n      unauthorized_response: \"unauthorized\"\n      forwarded_token_secret: \"this is a secret\"\n      forwarded_token_signature_alg: \"HS512\"\n      pool_count: 1\n      pool_size: 10\n      max_idle_time: 10\n      scheme: \"http\"\n      host: \"httpbin\"\n      port: 80\n      uris: [\"/\"]\n      strip_uri: false\n"
  },
  {
    "path": "examples/sidecar-authorization-gateways/config/example-protected-httpbin-configuration.yml",
    "content": "---\nconfiguration:\n  node_name: \"protected-httpbin\"\n  microgateway:\n    - authorize: true\n      error_content_type: \"text/plain\"\n      forbidden_response: \"forbidden\"\n      unauthorized_response: \"unauthorized\"\n      forwarded_token_secret: \"this is a secret\"\n      forwarded_token_signature_alg: \"HS512\"\n      pool_count: 1\n      pool_size: 10\n      max_idle_time: 10\n      required_scopes:\n        GET: [\"test\"]\n      scheme: \"http\"\n      host: \"httpbin\"\n      port: 80\n      uris: [\"/\"]\n      strip_uri: false\n"
  },
  {
    "path": "examples/sidecar-authorization-gateways/docker-compose.yml",
    "content": "version: \"3\"\n\nvolumes:\n  boruta-logs:\n\nservices:\n  postgres:\n    image: postgres:14\n    environment:\n      POSTGRES_USER: \"postgres\"\n      POSTGRES_PASSWORD: \"postgres\"\n      POSTGRES_DATABASE: \"boruta_release\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  boruta-server:\n    image: ghcr.io/malach-it/boruta-server:master\n    ports:\n      - \"8080:8080\"\n      - \"8081:8081\"\n      - \"8082:8082\"\n    volumes:\n      - \"boruta-logs:/app/log\"\n      - \"./config:/app/config\"\n    env_file: \"../../.env.example\"\n    environment:\n      MIX_ENV: \"prod\"\n      POSTGRES_USER: \"postgres\"\n      POSTGRES_PASSWORD: \"postgres\"\n      POSTGRES_DATABASE: \"boruta_release\"\n      POSTGRES_HOST: \"postgres\"\n      BORUTA_GATEWAY_CONFIGURATION_PATH: \"config/example-gateway-configuration.yml\"\n    depends_on:\n      postgres:\n        condition: service_healthy\n\n  httpbin-sidecar:\n    image: ghcr.io/malach-it/boruta-gateway:master\n    volumes:\n      - \"boruta-logs:/app/log\"\n      - \"./config:/app/config\"\n    env_file: \"../../.env.example\"\n    environment:\n      MIX_ENV: \"prod\"\n      POSTGRES_USER: \"postgres\"\n      POSTGRES_PASSWORD: \"postgres\"\n      POSTGRES_DATABASE: \"boruta_release\"\n      POSTGRES_HOST: \"postgres\"\n      BORUTA_GATEWAY_CONFIGURATION_PATH: \"config/example-httpbin-configuration.yml\"\n    depends_on:\n      postgres:\n        condition: service_healthy\n\n  protected-httpbin-sidecar:\n    image: ghcr.io/malach-it/boruta-gateway:master\n    volumes:\n      - \"boruta-logs:/app/log\"\n      - \"./config:/app/config\"\n    env_file: \"../../.env.example\"\n    environment:\n      MIX_ENV: \"prod\"\n      POSTGRES_USER: \"postgres\"\n      POSTGRES_PASSWORD: \"postgres\"\n      POSTGRES_DATABASE: \"boruta_release\"\n      POSTGRES_HOST: \"postgres\"\n      BORUTA_GATEWAY_CONFIGURATION_PATH: \"config/example-protected-httpbin-configuration.yml\"\n    depends_on:\n      postgres:\n        condition: service_healthy\n\n  httpbin:\n    image: kennethreitz/httpbin\n"
  },
  {
    "path": "mix.exs",
    "content": "defmodule Boruta.Umbrella.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      version: \"0.8.0\",\n      apps_path: \"apps\",\n      start_permanent: Mix.env() == :prod,\n      deps: deps(),\n      dialyzer: [\n        plt_add_apps: [:mix]\n      ],\n      aliases: aliases(),\n      releases: [\n        boruta: [\n          include_executables_for: [:unix],\n          applications: [\n            boruta_web: :permanent,\n            boruta_admin: :permanent,\n            boruta_gateway: :permanent\n          ]\n        ],\n        boruta_gateway: [\n          include_executables_for: [:unix],\n          applications: [\n            boruta_gateway: :permanent\n          ]\n        ],\n        boruta_auth: [\n          include_executables_for: [:unix],\n          applications: [\n            boruta_web: :permanent\n          ]\n        ],\n        boruta_admin: [\n          include_executables_for: [:unix],\n          applications: [\n            boruta_admin: :permanent\n          ]\n        ]\n      ]\n    ]\n  end\n\n  # Dependencies can be Hex packages:\n  #\n  #   {:mydep, \"~> 0.3.0\"}\n  #\n  # Or git/path repositories:\n  #\n  #   {:mydep, git: \"https://github.com/elixir-lang/mydep.git\", tag: \"0.1.0\"}\n  #\n  # Type \"mix help deps\" for more examples and options.\n  #\n  # Dependencies listed here are available only for this project\n  # and cannot be accessed from applications inside the apps folder\n  defp deps do\n    [\n      {:dialyxir, \"~> 1.0\", only: [:dev], runtime: false},\n      {:credo, \"~> 1.4\", only: [:dev, :test], runtime: false}\n    ]\n  end\n\n  defp aliases do\n    [\n      test: \"cmd mix test\"\n    ]\n  end\nend\n"
  },
  {
    "path": "rel/env.bat.eex",
    "content": "@echo off\nrem Set the release to work across nodes. If using the long name format like\nrem the one below (my_app@127.0.0.1), you need to also uncomment the\nrem RELEASE_DISTRIBUTION variable below. Must be \"sname\", \"name\" or \"none\".\nrem set RELEASE_DISTRIBUTION=name\nrem set RELEASE_NODE=<%= @release.name %>@127.0.0.1\n"
  },
  {
    "path": "rel/env.sh.eex",
    "content": "#!/bin/sh\n\n# Sets and enables heart (recommended only in daemon mode)\n# case $RELEASE_COMMAND in\n#   daemon*)\n#     HEART_COMMAND=\"$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND\"\n#     export HEART_COMMAND\n#     export ELIXIR_ERL_OPTIONS=\"-heart\"\n#     ;;\n#   *)\n#     ;;\n# esac\n\n# Set the release to work across nodes. If using the long name format like\n# the one below (my_app@127.0.0.1), you need to also uncomment the\n# RELEASE_DISTRIBUTION variable below. Must be \"sname\", \"name\" or \"none\".\n\n# NOTE node name in accordance with libcluster\nexport RELEASE_DISTRIBUTION=name\nexport RELEASE_NODE=<%= @release.name %>@$(hostname -i)\n"
  },
  {
    "path": "rel/vm.args.eex",
    "content": "## Customize flags given to the VM: http://erlang.org/doc/man/erl.html\n## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here\n\n## Number of dirty schedulers doing IO work (file, sockets, and others)\n##+SDio 5\n\n## Increase number of concurrent ports/sockets\n##+Q 65536\n\n## Tweak GC to run more often\n##-env ERL_FULLSWEEP_AFTER 10\n"
  },
  {
    "path": "scripts/prepare_assets.sh",
    "content": "set -e\ncd apps/boruta_admin/assets\nnpm ci\nnpm run build\ncd ../\nmix phx.digest\ncd ../boruta_identity\nmix phx.digest\ncd ../boruta_web\nmix phx.digest\ncd ../..\n"
  },
  {
    "path": "scripts/setup.debian.sh",
    "content": "#!/bin/sh\n\n##\n# Dependencies\n# - systemd\necho '# Boruta server setup'\necho '## install dependencies'\napt-get -q update\napt-get -q install -y libssl-dev wget vim postgresql postgresql-client\n\necho '## install boruta'\ncd /opt\nwget -O boruta.tar.gz https://github.com/malach-it/boruta-server/releases/download/0.4.0/boruta.tar.gz\nrm -rf ./boruta/\ntar xf boruta.tar.gz\n\nwget -q -O /opt/boruta/.env.production https://raw.githubusercontent.com/malach-it/boruta-server/0.4.0/.env.example\nvim /opt/boruta/.env.production\n\ncat > /etc/systemd/system/boruta.service <<- EOF\n[Unit]\nDescription=Boruta server\nAfter=network.target\n\n[Service]\nType=simple\nUser=root\nWorkingDirectory=/opt/boruta\nEnvironmentFile=/opt/boruta/.env.production\nExecStartPre=-su -w POSTGRES_USER,POSTGRES_PASSWORD - postgres -c \"psql -c \\\"CREATE USER \\${POSTGRES_USER} WITH CREATEDB PASSWORD '\\${POSTGRES_PASSWORD}'\\\"\"\nExecStart=/opt/boruta/bin/boruta start\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\nEOF\nchmod +x /etc/systemd/system/boruta.service\n\n\necho '## Enable boruta service'\nsystemctl daemon-reload\nsystemctl enable boruta\nsystemctl restart boruta\n\nexit 0\n"
  },
  {
    "path": "static_config/example-gateway-configuration.yml",
    "content": "---\nconfiguration:\n  gateway:\n    - authorize: false\n      error_content_type: \"text/plain\"\n      forbidden_response: \"forbidden\"\n      unauthorized_response: \"unauthorized\"\n      forwarded_token_secret: \"this is a secret\"\n      forwarded_token_signature_alg: \"HS512\"\n      pool_count: 1\n      pool_size: 10\n      max_idle_time: 10\n      scheme: \"https\"\n      host: \"httpbin.boruta.patatoid.fr\"\n      port: 443\n      uris: [\"/httpbin\"]\n      strip_uri: true\n    - authorize: false\n      error_content_type: \"text/plain\"\n      forbidden_response: \"forbidden\"\n      unauthorized_response: \"unauthorized\"\n      forwarded_token_secret: \"this is a secret\"\n      forwarded_token_signature_alg: \"HS512\"\n      pool_count: 1\n      pool_size: 10\n      max_idle_time: 10\n      scheme: \"https\"\n      host: \"protected-httpbin.boruta.patatoid.fr\"\n      port: 443\n      uris: [\"/protected-httpbin\"]\n      strip_uri: true\n"
  },
  {
    "path": "static_config/example-httpbin-configuration.yml",
    "content": "---\nconfiguration:\n  node_name: \"httpbin\"\n  microgateway:\n    - authorize: false\n      error_content_type: \"text/plain\"\n      forbidden_response: \"forbidden\"\n      unauthorized_response: \"unauthorized\"\n      forwarded_token_secret: \"this is a secret\"\n      forwarded_token_signature_alg: \"HS512\"\n      pool_count: 1\n      pool_size: 10\n      max_idle_time: 10\n      scheme: \"http\"\n      host: \"httpbin.patatoid.fr\"\n      port: 80\n      uris: [\"/\"]\n      strip_uri: false\n"
  },
  {
    "path": "static_config/example-protected-httpbin-configuration.yml",
    "content": "---\nconfiguration:\n  node_name: \"protected-httpbin\"\n  microgateway:\n    - authorize: true\n      error_content_type: \"text/plain\"\n      forbidden_response: \"forbidden\"\n      unauthorized_response: \"unauthorized\"\n      forwarded_token_secret: \"this is a secret\"\n      forwarded_token_signature_alg: \"HS512\"\n      pool_count: 1\n      pool_size: 10\n      max_idle_time: 10\n      required_scopes:\n        GET: [\"test\"]\n      scheme: \"http\"\n      host: \"httpbin.patatoid.fr\"\n      port: 80\n      uris: [\"/\"]\n      strip_uri: false\n"
  },
  {
    "path": "vetur.config.js",
    "content": "module.exports = {\n  projects: [\n    './app/boruta_admin/assets'\n  ]\n}\n"
  }
]